Re-sign embedded sidecar binaries for notarization #14
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |