feat(#1109): sign and notarize the macOS release with Developer ID
New reusable macos-signed.yml, gated by the apple-signing Environment: imports the Developer ID Application identity into a temporary keychain, builds, signs every nested Mach-O and framework then the app with Hardened Runtime and its entitlements, notarizes with notarytool using the existing App Store Connect API key, staples, and checks the result with spctl before zipping. release-signed.yml's macos job now calls it, so an unsigned or unnotarized macOS app can never be published. A temporary push trigger on this branch proves the lane before a tag; it is removed before merge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>pull/1138/head
parent
c296e258ea
commit
100290b33c
@ -0,0 +1,172 @@
|
||||
name: macOS signed build
|
||||
|
||||
# Builds the macOS app, signs it with the Developer ID Application identity,
|
||||
# notarizes it with Apple and staples the ticket, so it opens on any Mac with
|
||||
# no Gatekeeper warning (#1109, epic #1107). Called by release-signed.yml.
|
||||
#
|
||||
# SECURITY: this repo is PUBLIC, and this job holds the Developer ID key and
|
||||
# the App Store Connect API key.
|
||||
#
|
||||
# - NEVER call this from a `pull_request` workflow. A PR (incl. from a fork)
|
||||
# must never run a job that can read these secrets.
|
||||
# - Only this job references the `apple-signing` Environment, so only it
|
||||
# receives them, and only after the required reviewer approves.
|
||||
# - Secret files live in RUNNER_TEMP and are removed with `if: always()`.
|
||||
# - Values are passed via env:, never echoed. The identity name holds the
|
||||
# owner's legal name and is masked.
|
||||
#
|
||||
# There is deliberately no skip-notarization input: an unsigned or
|
||||
# unnotarized app must fail this job, never pass it (#904).
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
# Proves the lane before a release tag (#1109). Removed before this branch
|
||||
# merges.
|
||||
push:
|
||||
branches:
|
||||
- feat/1109-macos-signing
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
macos-signed:
|
||||
runs-on: macos-latest
|
||||
environment: apple-signing # required-reviewer gate; do not remove
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Check the signing secrets are present
|
||||
env:
|
||||
MACOS_DEVID_P12_BASE64: ${{ secrets.MACOS_DEVID_P12_BASE64 }}
|
||||
MACOS_DEVID_P12_PASSWORD: ${{ secrets.MACOS_DEVID_P12_PASSWORD }}
|
||||
ASC_API_KEY_P8_BASE64: ${{ secrets.ASC_API_KEY_P8_BASE64 }}
|
||||
ASC_API_KEY_ID: ${{ secrets.ASC_API_KEY_ID }}
|
||||
ASC_API_ISSUER_ID: ${{ secrets.ASC_API_ISSUER_ID }}
|
||||
run: |
|
||||
set -eu
|
||||
missing=0
|
||||
for name in MACOS_DEVID_P12_BASE64 MACOS_DEVID_P12_PASSWORD \
|
||||
ASC_API_KEY_P8_BASE64 ASC_API_KEY_ID ASC_API_ISSUER_ID; do
|
||||
if [ -z "${!name:-}" ]; then
|
||||
echo "::error::Secret ${name} is empty or absent in this run."
|
||||
missing=1
|
||||
fi
|
||||
done
|
||||
exit "${missing}"
|
||||
|
||||
- name: Temporary keychain with the Developer ID identity
|
||||
env:
|
||||
MACOS_DEVID_P12_BASE64: ${{ secrets.MACOS_DEVID_P12_BASE64 }}
|
||||
MACOS_DEVID_P12_PASSWORD: ${{ secrets.MACOS_DEVID_P12_PASSWORD }}
|
||||
run: |
|
||||
set -eu
|
||||
umask 077
|
||||
KC="${RUNNER_TEMP}/signing.keychain-db"
|
||||
KP="$(openssl rand -hex 16)"
|
||||
echo "::add-mask::${KP}"
|
||||
echo "KC=${KC}" >> "$GITHUB_ENV"
|
||||
security create-keychain -p "${KP}" "${KC}"
|
||||
security set-keychain-settings -lut 21600 "${KC}"
|
||||
security unlock-keychain -p "${KP}" "${KC}"
|
||||
P12="${RUNNER_TEMP}/devid.p12"
|
||||
printf '%s' "${MACOS_DEVID_P12_BASE64}" | base64 --decode > "${P12}"
|
||||
security import "${P12}" -k "${KC}" -P "${MACOS_DEVID_P12_PASSWORD}" \
|
||||
-T /usr/bin/codesign -T /usr/bin/security
|
||||
rm -f "${P12}"
|
||||
security set-key-partition-list -S apple-tool:,apple: -s -k "${KP}" "${KC}" > /dev/null
|
||||
security list-keychains -d user -s "${KC}" $(security list-keychains -d user | tr -d '"')
|
||||
IDENTITIES="$(security find-identity -v -p codesigning "${KC}")"
|
||||
COUNT="$(printf '%s\n' "${IDENTITIES}" | grep -c '"Developer ID Application: ' || true)"
|
||||
if [ "${COUNT}" != "1" ]; then
|
||||
echo "::error::Expected one Developer ID Application identity in the keychain, found ${COUNT}."
|
||||
exit 1
|
||||
fi
|
||||
IDENTITY="$(printf '%s\n' "${IDENTITIES}" | sed -n 's/.*"\(Developer ID Application: .*\)".*/\1/p')"
|
||||
echo "::add-mask::${IDENTITY}"
|
||||
echo "IDENTITY=${IDENTITY}" >> "$GITHUB_ENV"
|
||||
echo "Keychain ready with one Developer ID Application identity."
|
||||
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: "3.44.1"
|
||||
cache: true
|
||||
- run: flutter pub get
|
||||
- run: dart run build_runner build --delete-conflicting-outputs
|
||||
- name: Write dart_defines.json
|
||||
env:
|
||||
GIPHY_API_KEY: ${{ secrets.GIPHY_API_KEY }}
|
||||
run: jq -n --arg k "$GIPHY_API_KEY" '{GIPHY_API_KEY:$k}' > dart_defines.json
|
||||
- name: Enable native assets (bundles sqlite3 + llamadart libs, #445)
|
||||
run: flutter config --enable-native-assets
|
||||
- run: flutter build macos --release --no-pub --dart-define-from-file=dart_defines.json
|
||||
|
||||
# Inside-out: every nested Mach-O first, then each framework bundle, then
|
||||
# the app with its entitlements. Hardened Runtime and a secure timestamp
|
||||
# are what notarization requires.
|
||||
- name: Sign with Developer ID
|
||||
run: |
|
||||
set -eu
|
||||
APP=build/macos/Build/Products/Release/offband_meshcore.app
|
||||
echo "APP=${APP}" >> "$GITHUB_ENV"
|
||||
SIGN=(codesign --force --options runtime --timestamp --sign "${IDENTITY}" --keychain "${KC}")
|
||||
while IFS= read -r -d '' f; do
|
||||
if file -b "${f}" | grep -q 'Mach-O'; then
|
||||
"${SIGN[@]}" "${f}"
|
||||
fi
|
||||
done < <(find "${APP}/Contents/Frameworks" -type f -print0)
|
||||
while IFS= read -r -d '' fw; do
|
||||
"${SIGN[@]}" "${fw}"
|
||||
done < <(find "${APP}/Contents/Frameworks" -depth -name '*.framework' -type d -print0)
|
||||
"${SIGN[@]}" --entitlements macos/Runner/Release.entitlements "${APP}"
|
||||
codesign --verify --deep --strict --verbose=2 "${APP}"
|
||||
lipo -archs "${APP}/Contents/MacOS/offband_meshcore"
|
||||
|
||||
- name: Notarize and staple
|
||||
env:
|
||||
ASC_API_KEY_P8_BASE64: ${{ secrets.ASC_API_KEY_P8_BASE64 }}
|
||||
ASC_API_KEY_ID: ${{ secrets.ASC_API_KEY_ID }}
|
||||
ASC_API_ISSUER_ID: ${{ secrets.ASC_API_ISSUER_ID }}
|
||||
run: |
|
||||
set -eu
|
||||
umask 077
|
||||
P8="${RUNNER_TEMP}/AuthKey.p8"
|
||||
printf '%s' "${ASC_API_KEY_P8_BASE64}" | base64 --decode > "${P8}"
|
||||
SUBMIT="${RUNNER_TEMP}/notarize-submit.zip"
|
||||
ditto -c -k --keepParent "${APP}" "${SUBMIT}"
|
||||
OUT="${RUNNER_TEMP}/notarize.json"
|
||||
xcrun notarytool submit "${SUBMIT}" --key "${P8}" --key-id "${ASC_API_KEY_ID}" \
|
||||
--issuer "${ASC_API_ISSUER_ID}" --wait --timeout 30m --output-format json > "${OUT}" || true
|
||||
cat "${OUT}"
|
||||
STATUS="$(jq -r '.status // empty' "${OUT}")"
|
||||
ID="$(jq -r '.id // empty' "${OUT}")"
|
||||
if [ "${STATUS}" != "Accepted" ]; then
|
||||
echo "::error::Notarization status is '${STATUS:-none}', not Accepted."
|
||||
if [ -n "${ID}" ]; then
|
||||
xcrun notarytool log "${ID}" --key "${P8}" --key-id "${ASC_API_KEY_ID}" \
|
||||
--issuer "${ASC_API_ISSUER_ID}" || true
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
xcrun stapler staple "${APP}"
|
||||
xcrun stapler validate "${APP}"
|
||||
spctl --assess --type execute --verbose=4 "${APP}"
|
||||
|
||||
# ditto, not zip: it preserves the .app bundle's symlinks (#1108).
|
||||
- name: Zip macOS app
|
||||
run: |
|
||||
set -eu
|
||||
ditto -c -k --keepParent "${APP}" offband-macos.zip
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-macos
|
||||
path: offband-macos.zip
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
- name: Remove the signing material
|
||||
if: always()
|
||||
run: |
|
||||
set +e
|
||||
if [ -n "${KC:-}" ]; then security delete-keychain "${KC}"; fi
|
||||
rm -f dart_defines.json "${RUNNER_TEMP}/devid.p12" "${RUNNER_TEMP}/AuthKey.p8"
|
||||
Loading…
Reference in new issue