From a080b459290021e8a66452a1f2bc0c04e827c257 Mon Sep 17 00:00:00 2001 From: winebarrel Date: Mon, 13 Jul 2026 13:02:21 +0900 Subject: [PATCH] Add signed & notarized app release workflow Add a Release workflow triggered on v* tags that builds, Developer ID signs, packages as a DMG, notarizes via notarytool (App Store Connect API key), staples, generates a SHA-256 checksum, and attaches both the DMG and checksum to a GitHub Release. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01WSMaW5HzjPiK9nPK8UQuDZ --- .github/workflows/release.yml | 145 ++++++++++++++++++++++++++++++++++ ExportOptions.plist | 14 ++++ 2 files changed, 159 insertions(+) create mode 100644 .github/workflows/release.yml create mode 100644 ExportOptions.plist diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b378549 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,145 @@ +name: Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + release: + name: Build, Sign, Notarize and Release + runs-on: macos-latest + env: + TEAM_ID: 97A8B2WE2P + SCHEME: CursorIME + APP_NAME: CursorIME + steps: + - uses: actions/checkout@v7 + + - name: Select Xcode + run: sudo xcode-select -switch /Applications/Xcode.app + + - name: Resolve version from tag + id: version + run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" + + - name: Import Developer ID certificate + env: + BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} + P12_PASSWORD: ${{ secrets.P12_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 "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o "$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 "$P12_PASSWORD" \ + -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH" + security set-key-partition-list \ + -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security list-keychain -d user -s "$KEYCHAIN_PATH" login.keychain-db + + - name: Archive + run: | + set -euo pipefail + xcodebuild archive \ + -project CursorIME.xcodeproj \ + -scheme "$SCHEME" \ + -configuration Release \ + -destination 'generic/platform=macOS' \ + -archivePath "$RUNNER_TEMP/$APP_NAME.xcarchive" \ + MARKETING_VERSION="${{ steps.version.outputs.version }}" \ + CURRENT_PROJECT_VERSION="${{ github.run_number }}" \ + CODE_SIGN_STYLE=Manual \ + CODE_SIGN_IDENTITY="Developer ID Application" \ + DEVELOPMENT_TEAM="$TEAM_ID" + + - name: Export signed app + run: | + set -euo pipefail + xcodebuild -exportArchive \ + -archivePath "$RUNNER_TEMP/$APP_NAME.xcarchive" \ + -exportPath "$RUNNER_TEMP/export" \ + -exportOptionsPlist ExportOptions.plist + + - name: Verify code signature + run: | + set -euo pipefail + APP="$RUNNER_TEMP/export/$APP_NAME.app" + codesign --verify --deep --strict --verbose=2 "$APP" + codesign --display --verbose=4 "$APP" + + - name: Create DMG + id: dmg + run: | + set -euo pipefail + VERSION="${{ steps.version.outputs.version }}" + DMG_PATH="$RUNNER_TEMP/$APP_NAME-$VERSION.dmg" + STAGE="$RUNNER_TEMP/dmg-stage" + + mkdir -p "$STAGE" + cp -R "$RUNNER_TEMP/export/$APP_NAME.app" "$STAGE/" + ln -s /Applications "$STAGE/Applications" + + hdiutil create \ + -volname "$APP_NAME" \ + -srcfolder "$STAGE" \ + -ov -format UDZO \ + "$DMG_PATH" + + codesign --force --timestamp --sign "Developer ID Application" "$DMG_PATH" + echo "path=$DMG_PATH" >> "$GITHUB_OUTPUT" + + - name: Notarize and staple + env: + AC_API_KEY_ID: ${{ secrets.AC_API_KEY_ID }} + AC_API_ISSUER_ID: ${{ secrets.AC_API_ISSUER_ID }} + AC_API_KEY_BASE64: ${{ secrets.AC_API_KEY_BASE64 }} + run: | + set -euo pipefail + KEY_PATH="$RUNNER_TEMP/AuthKey.p8" + echo "$AC_API_KEY_BASE64" | base64 --decode -o "$KEY_PATH" + + xcrun notarytool submit "${{ steps.dmg.outputs.path }}" \ + --key "$KEY_PATH" \ + --key-id "$AC_API_KEY_ID" \ + --issuer "$AC_API_ISSUER_ID" \ + --wait + + xcrun stapler staple "${{ steps.dmg.outputs.path }}" + xcrun stapler validate "${{ steps.dmg.outputs.path }}" + + - name: Generate checksum + id: checksum + run: | + set -euo pipefail + DMG_PATH="${{ steps.dmg.outputs.path }}" + SUM_PATH="$DMG_PATH.sha256" + # Compute after stapling, since stapler rewrites the DMG. + ( cd "$(dirname "$DMG_PATH")" && shasum -a 256 "$(basename "$DMG_PATH")" > "$SUM_PATH" ) + cat "$SUM_PATH" + echo "path=$SUM_PATH" >> "$GITHUB_OUTPUT" + + - name: Create GitHub Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + gh release create "$GITHUB_REF_NAME" \ + "${{ steps.dmg.outputs.path }}" \ + "${{ steps.checksum.outputs.path }}" \ + --title "$GITHUB_REF_NAME" \ + --generate-notes + + - name: Clean up keychain + if: always() + run: security delete-keychain "$RUNNER_TEMP/app-signing.keychain-db" || true diff --git a/ExportOptions.plist b/ExportOptions.plist new file mode 100644 index 0000000..814e121 --- /dev/null +++ b/ExportOptions.plist @@ -0,0 +1,14 @@ + + + + + method + developer-id + teamID + 97A8B2WE2P + signingStyle + manual + signingCertificate + Developer ID Application + +