CI now builds a macOS zip with an unsigned .app by default. When Apple Developer secrets are configured in the repository, the same workflow will also code-sign, notarize, and staple the app, so users can open it with a double-click instead of using right-click → Open.
This is a one-time setup. After the secrets are in place, every future tag release (
0.4.10,0.4.11, …) will produce a signed macOS build automatically.
| State | Gatekeeper behavior |
|---|---|
| Unsigned / not notarized | Scary dialog, user must right-click → Open, sometimes go to System Settings → Privacy & Security |
| Signed + notarized + stapled | Double-click opens normally, no warnings |
- Apple Developer Program membership — $99/year (required for a Developer ID certificate).
- A Mac (or Xcode Cloud) to create the Certificate Signing Request and export the
.p12. - Owner access to the GitHub repository to add encrypted secrets.
- Open Keychain Access on a Mac → Certificate Assistant → Request a Certificate From a Certificate Authority.
- Use the same email as your Apple Developer account, choose Saved to disk.
- Go to Apple Developer → Certificates, Identifiers & Profiles and create a new certificate:
- Type: Developer ID Application
- Intermediary: G2 Sub-CA (Xcode 11.4.1 or later)
- Upload the
.certSigningRequestfile from step 2.
- Download the certificate and open it in Keychain Access.
- In Keychain Access, select both the certificate and its private key, right-click → Export 2 items….
- Format: Personal Information Exchange (.p12).
- Choose a strong export password and save it.
- Base64-encode the
.p12for GitHub Actions:
base64 -i QueryaDeveloperID.p12 -o QueryaDeveloperID.p12.base64Copy the contents of QueryaDeveloperID.p12.base64. You will paste it into MACOS_CERTIFICATE_P12.
- Note the full certificate name. It looks like:
Developer ID Application: Your Name or Org (ABCD123456)
You will paste it into MACOS_SIGN_IDENTITY.
Using notarytool with an App Store Connect API key is the most reliable method in CI.
- Sign in to App Store Connect with the Apple Developer account.
- Go to Users and Access → Integrations → App Store Connect API.
- Create a new Team Key (or use an existing one) with the Admin or App Manager role.
- Note the Issuer ID.
- Note the Key ID.
- Download the
.p8file (you can only do this once).
- Base64-encode the
.p8file:
base64 -i AuthKey_KEYID.p8 -o AuthKey_KEYID.p8.base64Copy the contents for MACOS_NOTARY_KEY.
Go to Settings → Secrets and variables → Actions → New repository secret and add:
| Secret | Value |
|---|---|
MACOS_CERTIFICATE_P12 |
Base64-encoded .p12 certificate from Step 1 |
MACOS_CERTIFICATE_PASSWORD |
The password you set when exporting the .p12 |
MACOS_SIGN_IDENTITY |
Full certificate name, e.g. Developer ID Application: Querya Team (ABCD123456) |
MACOS_NOTARY_KEY |
Base64-encoded .p8 API key from Step 2 |
MACOS_NOTARY_KEY_ID |
The Key ID from Step 2, e.g. ABC123DEF4 |
MACOS_NOTARY_ISSUER_ID |
The Issuer ID from Step 2, e.g. 12345678-90ab-cdef-1234-567890abcdef |
MACOS_KEYCHAIN_PASSWORD |
A random strong password (it is only used inside the CI runner) |
.github/workflows/release.yml contains a Sign and notarize macOS app step that runs only when MACOS_SIGN_IDENTITY and MACOS_NOTARY_KEY are set:
- Creates a temporary keychain in the runner.
- Imports the Developer ID certificate.
- Signs nested frameworks, dylibs, and the main
.appbundle with the Hardened Runtime. - Applies entitlements from
macos/Runner/Release.entitlements. - Submits the app to Apple notarytool, waits for approval.
- Staples the notarization ticket to the
.appso it works offline. - The final zip is then produced from the signed/stapled bundle. The signing step uses
macos/Runner/ReleaseSigned.entitlements(hardened runtime, no sandbox), while the defaultmacos/Runner/Release.entitlementsis left untouched for unsigned builds.
If the secrets are not set, the step is skipped and the zip is built exactly as before (unsigned). This keeps the release workflow safe for forks and local testing.
After a release, download the macOS zip and run:
# Check the signature
codesign --verify --deep --strict --verbose=2 querya_desktop.app
# Check notarization/staple
spctl --assess --verbose --type execute querya_desktop.app
xcrun stapler validate querya_desktop.appIf all three commands report success, the app should open with a normal double-click.
Two entitlement files are kept side by side:
macos/Runner/Release.entitlements— the original Flutter default, kept as-is for unsigned builds.macos/Runner/ReleaseSigned.entitlements— used only when the CI signs the app. It disables the App Sandbox (the app needs arbitrary network, file picker, and~/.queryaaccess) and enables Hardened Runtime exceptions for Flutter/Dart (allow-jit,allow-unsigned-executable-memory,disable-library-validation).
If you later add plugins that require microphone, camera, or other protected resources, add the corresponding hardened-runtime and/or sandbox entitlements to ReleaseSigned.entitlements and re-sign.
- Flutter: Build and release a macOS app
- Apple: Notarizing macOS software before distribution
- Apple: Hardened Runtime
- Apple: Disable Library Validation Entitlement