Package macOS apps with GitHub Actions


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 czf instead of ditto.

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.



Please support this site and join our Discord!