This post goes over how to package a prebuilt binary into an unsigned macOS app bundle with GitHub Actions.
Prerequisites
You have a prebuilt macOS binary that’s already compiled. For example:
echo "hello" > my-app && chmod +x my-app
Package macOS app
Use remarkablemark/package-macos-app to wrap the binary in a .app bundle:
# .github/workflows/build.yml
on: push
jobs:
build:
runs-on: macos-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Build binary
run: echo "hello" > my-app && chmod +x my-app
- name: Package macOS app
id: package
uses: remarkablemark/package-macos-app@v1
with:
binary: ./my-app
version: 1.0.0
- name: Zip app bundle
run: ditto -c -k --keepParent "$APP_PATH" my-app.zip
env:
APP_PATH: ${{ steps.package.outputs.app-path }}
- name: Upload app bundle
uses: actions/upload-artifact@v7
with:
name: my-app
path: my-app.zip
Zip the bundle before uploading to preserve the executable bit. On non-macOS runners, use
tar czfinstead ofditto.
Bundle structure
If you download and unzip the artifact, you’ll see the following:
my-app.app/
└── Contents
├── Info.plist
├── MacOS
│ └── my-app
└── Resources
The action generates the Info.plist with your app name, bundle identifier, and version.
Configuration
You can configure the action by passing inputs. For example:
- name: Package macOS app
id: package
uses: remarkablemark/package-macos-app@v1
with:
binary: ./my-app
name: My App
bundle-id: com.example.my-app
version: ${{ github.ref_name }}
icon: ./assets/AppIcon.icns
output: dist
Outputs
The action outputs the path to the created bundle. Reference the packaging step by its id: and use the app-path output:
- name: Package macOS app
id: package
uses: remarkablemark/package-macos-app@v1
with:
binary: ./my-app
version: 1.0.0
- run: ditto -c -k --keepParent "$APP_PATH" my-app.zip
env:
APP_PATH: ${{ steps.package.outputs.app-path }}
Signing
The bundle is unsigned. Sign it with codesign before distribution:
codesign --force --deep --sign - my-app.app
Check out the repository for more details.