Skip to content

Re-sign embedded sidecar binaries for notarization #14

Re-sign embedded sidecar binaries for notarization

Re-sign embedded sidecar binaries for notarization #14

Workflow file for this run

name: Release DMG
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
skip_notarize:
description: Skip notarization (test-only)
required: false
default: false
type: boolean
permissions:
contents: write
jobs:
build-sign-notarize:
runs-on: macos-15
env:
APP_NAME: mac-copilot
PROJECT_PATH: mac-copilot.xcodeproj
SCHEME: mac-copilot
CONFIGURATION: Release
DMG_PATH: dist/mac-copilot.dmg
APPCAST_URL: https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/appcast.xml
APPCAST_SIGNING_PRIVATE_KEY: ${{ secrets.APPCAST_SIGNING_PRIVATE_KEY }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Import Developer ID certificate
env:
MACOS_CERT_BASE64: ${{ secrets.MACOS_CERT_BASE64 }}
MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
set -euo pipefail
CERT_PATH="$RUNNER_TEMP/certificate.p12"
KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db"
echo "$MACOS_CERT_BASE64" | base64 --decode > "$CERT_PATH"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security import "$CERT_PATH" -P "$MACOS_CERT_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
security list-keychains -d user -s "$KEYCHAIN_PATH" "$(security default-keychain -d user | tr -d '\"')"
security default-keychain -d user -s "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security find-identity -v -p codesigning
- name: Install sidecar dependencies
run: |
set -euo pipefail
cd sidecar
if [ -f package-lock.json ]; then
npm ci
else
npm install
fi
npm run build
- name: Build signed app
env:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
SPARKLE_PUBLIC_ED_KEY: ${{ secrets.SPARKLE_PUBLIC_ED_KEY }}
SPARKLE_APPCAST_URL: ${{ env.APPCAST_URL }}
run: |
set -euo pipefail
DERIVED_DATA_PATH="$RUNNER_TEMP/DerivedData"
APP_PATH="$DERIVED_DATA_PATH/Build/Products/Release/mac-copilot.app"
xcodebuild \
-project "$PROJECT_PATH" \
-scheme "$SCHEME" \
-configuration "$CONFIGURATION" \
-derivedDataPath "$DERIVED_DATA_PATH" \
-destination 'platform=macOS' \
MACOSX_DEPLOYMENT_TARGET=15.5 \
CODE_SIGN_STYLE=Manual \
CODE_SIGN_IDENTITY='Developer ID Application' \
DEVELOPMENT_TEAM="$APPLE_TEAM_ID" \
build
test -d "$APP_PATH"
# Re-sign all embedded Mach-O artifacts (native node modules/helpers/binaries)
# that ship inside sidecar resources to satisfy notarization requirements.
while IFS= read -r -d '' candidate; do
if file -b "$candidate" | grep -q "Mach-O"; then
echo "Signing embedded binary: $candidate"
codesign --force --sign 'Developer ID Application' --options runtime --timestamp "$candidate"
fi
done < <(find "$APP_PATH/Contents/Resources/sidecar" -type f -print0)
# Re-sign the outer app after nested code signatures change.
codesign --force \
--sign 'Developer ID Application' \
--options runtime \
--timestamp \
--preserve-metadata=identifier,entitlements,requirements,flags \
"$APP_PATH"
codesign --verify --deep --strict --verbose=2 "$APP_PATH"
echo "APP_PATH=$APP_PATH" >> "$GITHUB_ENV"
- name: Create DMG
run: |
set -euo pipefail
chmod +x scripts/build_dmg.sh
./scripts/build_dmg.sh --app-path "$APP_PATH" --output "$DMG_PATH" --skip-build
- name: Notarize + staple app and DMG
if: ${{ github.event_name != 'workflow_dispatch' || inputs.skip_notarize != true }}
env:
APPLE_NOTARY_KEY_ID: ${{ secrets.APPLE_NOTARY_KEY_ID }}
APPLE_NOTARY_ISSUER_ID: ${{ secrets.APPLE_NOTARY_ISSUER_ID }}
APPLE_NOTARY_PRIVATE_KEY_BASE64: ${{ secrets.APPLE_NOTARY_PRIVATE_KEY_BASE64 }}
run: |
set -euo pipefail
KEY_PATH="$RUNNER_TEMP/AuthKey_${APPLE_NOTARY_KEY_ID}.p8"
echo "$APPLE_NOTARY_PRIVATE_KEY_BASE64" | base64 --decode > "$KEY_PATH"
SUBMIT_JSON_PATH="$RUNNER_TEMP/notary-submit.json"
xcrun notarytool submit "$DMG_PATH" \
--key "$KEY_PATH" \
--key-id "$APPLE_NOTARY_KEY_ID" \
--issuer "$APPLE_NOTARY_ISSUER_ID" \
--wait \
--output-format json > "$SUBMIT_JSON_PATH"
cat "$SUBMIT_JSON_PATH"
STATUS="$(/usr/bin/plutil -extract status raw -o - "$SUBMIT_JSON_PATH" 2>/dev/null || true)"
SUBMISSION_ID="$(/usr/bin/plutil -extract id raw -o - "$SUBMIT_JSON_PATH" 2>/dev/null || true)"
if [ "$STATUS" != "Accepted" ]; then
echo "error: Notarization status is '$STATUS' (submission id: $SUBMISSION_ID)"
if [ -n "$SUBMISSION_ID" ]; then
xcrun notarytool log "$SUBMISSION_ID" \
--key "$KEY_PATH" \
--key-id "$APPLE_NOTARY_KEY_ID" \
--issuer "$APPLE_NOTARY_ISSUER_ID" || true
fi
exit 1
fi
xcrun stapler staple "$DMG_PATH"
- name: Validate DMG notarization staple
if: ${{ github.event_name != 'workflow_dispatch' || inputs.skip_notarize != true }}
run: |
set -euo pipefail
xcrun stapler validate "$DMG_PATH"
- name: Prepare release assets
id: assets
run: |
set -euo pipefail
VERSION="${GITHUB_REF_NAME}"
if [ -z "$VERSION" ] || [ "$VERSION" = "" ]; then
VERSION="manual-${GITHUB_RUN_NUMBER}"
fi
VERSIONED_DMG="dist/${APP_NAME}-${VERSION}.dmg"
cp "$DMG_PATH" "$VERSIONED_DMG"
shasum -a 256 "$VERSIONED_DMG" > dist/SHA256SUMS.txt
echo "versioned_dmg=$VERSIONED_DMG" >> "$GITHUB_OUTPUT"
- name: Upload workflow artifacts
uses: actions/upload-artifact@v4
with:
name: mac-copilot-release-assets
path: |
${{ steps.assets.outputs.versioned_dmg }}
dist/SHA256SUMS.txt
- name: Generate Sparkle appcast
if: startsWith(github.ref, 'refs/tags/v') && env.APPCAST_SIGNING_PRIVATE_KEY != ''
run: |
set -euo pipefail
DERIVED_DATA_PATH="$RUNNER_TEMP/DerivedData"
GENERATE_APPCAST_BIN="$DERIVED_DATA_PATH/SourcePackages/checkouts/Sparkle/bin/generate_appcast"
test -x "$GENERATE_APPCAST_BIN"
APPCAST_DIR="$RUNNER_TEMP/appcast"
mkdir -p "$APPCAST_DIR"
cp "${{ steps.assets.outputs.versioned_dmg }}" "$APPCAST_DIR/"
ED_KEY_PATH="$RUNNER_TEMP/sparkle_ed25519_private_key"
printf '%s' "$APPCAST_SIGNING_PRIVATE_KEY" > "$ED_KEY_PATH"
chmod 600 "$ED_KEY_PATH"
DOWNLOAD_URL_PREFIX="https://github.com/${GITHUB_REPOSITORY}/releases/download/${GITHUB_REF_NAME}/"
"$GENERATE_APPCAST_BIN" "$APPCAST_DIR" \
--ed-key-file "$ED_KEY_PATH" \
--download-url-prefix "$DOWNLOAD_URL_PREFIX"
test -f "$APPCAST_DIR/appcast.xml"
cp "$APPCAST_DIR/appcast.xml" dist/appcast.xml
- name: Upload appcast artifact
if: startsWith(github.ref, 'refs/tags/v') && env.APPCAST_SIGNING_PRIVATE_KEY != ''
uses: actions/upload-artifact@v4
with:
name: mac-copilot-appcast
path: dist/appcast.xml
- name: Publish appcast to gh-pages
if: startsWith(github.ref, 'refs/tags/v') && env.APPCAST_SIGNING_PRIVATE_KEY != ''
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
PAGES_DIR="$RUNNER_TEMP/gh-pages"
REPO_URL="https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
if git ls-remote --exit-code --heads origin gh-pages >/dev/null 2>&1; then
git clone --depth 1 --branch gh-pages "$REPO_URL" "$PAGES_DIR"
else
git clone --depth 1 "$REPO_URL" "$PAGES_DIR"
(
cd "$PAGES_DIR"
git checkout --orphan gh-pages
git rm -rf . || true
touch .nojekyll
git add .nojekyll
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git commit -m "Initialize gh-pages"
git push origin gh-pages
)
rm -rf "$PAGES_DIR"
git clone --depth 1 --branch gh-pages "$REPO_URL" "$PAGES_DIR"
fi
cp dist/appcast.xml "$PAGES_DIR/appcast.xml"
touch "$PAGES_DIR/.nojekyll"
(
cd "$PAGES_DIR"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add appcast.xml .nojekyll
if git diff --cached --quiet; then
echo "No appcast changes to commit."
else
git commit -m "Update Sparkle appcast for ${GITHUB_REF_NAME}"
git push origin gh-pages
fi
)
- name: Publish GitHub release
if: startsWith(github.ref, 'refs/tags/v')
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
files: |
${{ steps.assets.outputs.versioned_dmg }}
dist/SHA256SUMS.txt