Notarizing macOS Binaries in GitHub Actions

Β· 845 words Β· 4 minute read

This post walks through the GitHub Actions release workflow I use for diff-tgz β€” a Rust CLI tool distributed via Homebrew. The pipeline builds for four targets (macOS arm64/x86_64, Linux arm64/x86_64), signs and notarizes the macOS binaries, publishes a GitHub release, and bumps the Homebrew tap formula automatically.

The full workflow file lives at .github/workflows/release.yml and is triggered on any tag push matching *.*.*.

Secrets you need to configure πŸ”—

Before the workflow can run you have to add seven secrets in your repository settings (Settings β†’ Secrets and variables β†’ Actions).

APPLE_ID πŸ”—

Your Apple ID email address β€” the one associated with your Apple Developer account.

DISTRIBUTION_CERT_BASE_64 πŸ”—

A base64-encoded Developer ID Application certificate exported from Xcode.

To get it:

  1. Open Xcode β†’ Settings β†’ Accounts, select your Apple ID and click Manage Certificates.
  2. Right-click your Developer ID Application certificate and choose Export Certificate….
  3. Save it as a .p12 file and choose a strong export password (this becomes DISTRIBUTION_CERT_PASS).
  4. Base64-encode the file:
1
base64 -i certificate.p12 | pbcopy

Paste the result as the secret value.

DISTRIBUTION_CERT_PASS πŸ”—

The password you chose when exporting the .p12 file above.

NOTARY_TOOL_PASS πŸ”—

An app-specific password for your Apple ID. Create one at appleid.apple.com under Sign-In and Security β†’ App-Specific Passwords. This is what xcrun notarytool uses to authenticate with Apple’s notarization service.

SIGNING_IDENTITY πŸ”—

The full name of your signing identity as it appears in the keychain. Run this locally to find it:

1
security find-identity -v -p codesigning

It comes in the form Developer ID Application: Your Name (CERTID). Copy the whole string including the certificate type prefix.

TEAM_ID πŸ”—

Your 10-character Apple Developer Team ID. You can find it in App Store Connect under Users and Access β†’ Keys, or at developer.apple.com/account in the Membership section.

TAP_REPO_TOKEN πŸ”—

A GitHub Personal Access Token with contents: write access scoped to your Homebrew tap repository. The workflow uses it to check out the tap repo and push the updated formula after each release. Create one at GitHub β†’ Settings β†’ Developer settings β†’ Personal access tokens β†’ Fine-grained tokens and grant it write access to the tap repository only.

How the workflow uses them πŸ”—

Setting up the certificate keychain πŸ”—

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
- name: Configure certificates
  if: runner.os == 'macOS'
  run: >
    echo $DISTRIBUTION_CERT_BASE_64 | base64 --decode > cert.p12 &&
    security create-keychain -p $KEYCHAIN_PASS $KEYCHAIN &&
    security default-keychain -s ~/Library/Keychains/$KEYCHAIN-db &&
    security set-keychain-settings $KEYCHAIN &&
    security list-keychains -s $KEYCHAIN &&
    security unlock-keychain -p $KEYCHAIN_PASS $KEYCHAIN &&
    security import ./cert.p12 -k $KEYCHAIN -P $DISTRIBUTION_CERT_PASS -A -T /usr/bin/codesign -T /usr/bin/security &&
    security set-key-partition-list -S apple-tool:,apple: -s -k $KEYCHAIN_PASS $KEYCHAIN &&
    security find-identity -p codesigning -v    
  env:
    KEYCHAIN: "def.keychain"
    KEYCHAIN_PASS: "hmmmm" # whatever, throwaway keychain
    DISTRIBUTION_CERT_BASE_64: ${{ secrets.DISTRIBUTION_CERT_BASE_64 }}
    DISTRIBUTION_CERT_PASS: ${{ secrets.DISTRIBUTION_CERT_PASS }}

This step decodes the base64 certificate back to a .p12 file, creates a temporary keychain, imports the certificate into it, and grants codesign access to the key without a GUI prompt. The keychain password (KEYCHAIN_PASS) is just a local ephemeral value used within this job β€” it doesn’t need to be a secret.

Storing notarytool credentials πŸ”—

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
- name: Configure notarytool
  if: runner.os == 'macOS'
  run: >
    xcrun notarytool store-credentials notarytool
    --apple-id $APPLE_ID
    --team-id $TEAM_ID
    --password $NOTARY_TOOL_PASS    
  env:
    APPLE_ID: ${{ secrets.APPLE_ID }}
    NOTARY_TOOL_PASS: ${{ secrets.NOTARY_TOOL_PASS }}
    TEAM_ID: ${{ secrets.TEAM_ID }}

notarytool store-credentials saves the credentials under the profile name notarytool in the keychain. Later steps reference this profile by name so the credentials never appear in plain text in the command line.

Signing and notarizing πŸ”—

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
- name: Sign binary
  if: runner.os == 'macOS'
  run: >
    codesign -s "$SIGNING_IDENTITY" --deep -v -f -o runtime
    target/${{ matrix.target }}/release/diff-tgz    
  env:
    SIGNING_IDENTITY: ${{ secrets.SIGNING_IDENTITY }}

- name: Notarize binary
  if: runner.os == 'macOS'
  run: |
    zip notary.zip target/${{ matrix.target }}/release/diff-tgz
    xcrun notarytool submit notary.zip --keychain-profile notarytool --wait    

codesign uses the --options runtime flag (-o runtime), which is required by Apple for notarization. The binary is then zipped β€” notarytool requires an archive β€” and submitted. The --wait flag blocks until Apple returns a verdict, making it easy to catch failures in the CI log.

Bumping the Homebrew tap πŸ”—

The bump_homebrew job runs after the GitHub release is created. It checks out the tap repository using TAP_REPO_TOKEN, computes SHA-256 checksums for all four release archives, runs a Ruby script to update the formula, and pushes the commit back:

1
2
3
4
5
6
- uses: actions/checkout@v6
  with:
    ref: main
    path: tap
    repository: Reeywhaar/homebrew-tap
    token: ${{ secrets.TAP_REPO_TOKEN }}

Without TAP_REPO_TOKEN the checkout would use the default GITHUB_TOKEN, which only has access to the current repository.

Summary πŸ”—

SecretWhere to get it
APPLE_IDYour Apple ID email
DISTRIBUTION_CERT_BASE_64Export from Xcode, base64 -i cert.p12
DISTRIBUTION_CERT_PASSPassword chosen during export
NOTARY_TOOL_PASSApp-specific password from appleid.apple.com
SIGNING_IDENTITYsecurity find-identity -v -p codesigning
TEAM_IDApp Store Connect β†’ Users and Access β†’ Keys
TAP_REPO_TOKENGitHub PAT with contents: write on tap repo