From 74b557d47f1784ca278662a780d1e899e9f29d9f Mon Sep 17 00:00:00 2001 From: Wes Date: Mon, 4 May 2026 09:08:07 -0700 Subject: [PATCH] feat(ci): sign and notarize OSS desktop release builds Replace ad-hoc code signing with proper Apple code signing via block/apple-codesign-action. The unsigned DMG from the Tauri build is sent to Block's signing service which signs with a Developer ID certificate, notarizes with Apple, and returns a signed DMG. Updater artifacts (tar.gz + minisign signature) are now created post-signing so the archive contains the properly signed .app. Changes: - Add codesign action step (pinned to v1.1.0 SHA) - Add spctl notarization verification alongside codesign --verify - Move updater artifact creation to post-signing step - Use pnpm exec tauri signer (lockfile-pinned) instead of npx - Name archive with version so GitHub asset filename matches latest.json URL (gh release upload # syntax is label-only) - Add id-token: write permission for AWS OIDC - Update RELEASING.md with new secrets and signed/notarized docs - Remove signingIdentity and createUpdaterArtifacts from release config Co-Authored-By: Claude Opus 4.6 --- .github/workflows/release.yml | 74 +++++++++++++++--------- RELEASING.md | 33 ++++++----- desktop/scripts/build-release-config.mjs | 15 ++--- 3 files changed, 73 insertions(+), 49 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 74e6e1a3be..5f27bfa3ea 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,9 +15,10 @@ jobs: release: name: Release runs-on: macos-latest - timeout-minutes: 60 + timeout-minutes: 120 permissions: contents: write + id-token: write env: VERSION: ${{ inputs.version }} steps: @@ -59,38 +60,57 @@ jobs: env: SPROUT_UPDATER_PUBLIC_KEY: ${{ secrets.SPROUT_UPDATER_PUBLIC_KEY }} SPROUT_UPDATER_ENDPOINT: https://github.com/block/sprout/releases/download/sprout-desktop-latest/latest.json - TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} CMAKE_POLICY_VERSION_MINIMUM: "3.5" + - name: Sign with Apple codesigning service + id: codesign + uses: block/apple-codesign-action@679535d1ab7c5a7c18e6f9afcba3464512cc3dde # v1.1.0 + with: + osx-codesign-role: ${{ secrets.OSX_CODESIGN_ROLE }} + codesign-s3-bucket: ${{ secrets.CODESIGN_S3_BUCKET }} + unsigned-artifact-path: desktop/src-tauri/target/release/bundle/dmg/Sprout_${{ env.VERSION }}_aarch64.dmg + entitlements-plist-path: desktop/src-tauri/Entitlements.plist + - name: Verify code signature run: | - codesign --verify --deep --strict --verbose=2 \ - desktop/src-tauri/target/release/bundle/macos/Sprout.app + # Extract signed .app from the action's output zip + SIGNED_APP_DIR="${RUNNER_TEMP}/signed-app" + mkdir -p "$SIGNED_APP_DIR" + ditto -x -k "${{ steps.codesign.outputs.signed-artifact-path }}" "$SIGNED_APP_DIR" + if [[ ! -d "$SIGNED_APP_DIR/Sprout.app" ]]; then + echo "::error::Sprout.app not found after extracting signed artifact" + exit 1 + fi + codesign --verify --deep --strict --verbose=2 "$SIGNED_APP_DIR/Sprout.app" + spctl -a -t exec -vv "$SIGNED_APP_DIR/Sprout.app" - - name: Locate build artifacts - id: artifacts + - name: Create updater artifacts + id: updater run: | - BUNDLE_DIR="desktop/src-tauri/target/release/bundle" + SIGNED_APP_DIR="${RUNNER_TEMP}/signed-app" - # Find the DMG (Tauri names it Sprout__.dmg) - DMG=$(find "$BUNDLE_DIR/dmg" -name '*.dmg' -type f | head -1) - if [[ -z "$DMG" ]]; then - echo "::error::No DMG found in $BUNDLE_DIR/dmg" + ARCHIVE_NAME="Sprout_${VERSION}_aarch64.app.tar.gz" + + # Create updater tarball from signed .app + ARCHIVE="${RUNNER_TEMP}/${ARCHIVE_NAME}" + (cd "$SIGNED_APP_DIR" && tar -czf "$ARCHIVE" "Sprout.app") + + # Sign the archive with Tauri's minisign key + # tauri signer reads TAURI_SIGNING_PRIVATE_KEY + TAURI_SIGNING_PRIVATE_KEY_PASSWORD from env + cd desktop && pnpm exec tauri signer sign "$ARCHIVE" + + SIGNATURE="${ARCHIVE}.sig" + if [[ ! -f "$SIGNATURE" ]]; then + echo "::error::tauri signer produced no signature file" exit 1 fi - echo "dmg=$DMG" >> "$GITHUB_OUTPUT" - # Find the updater .tar.gz and .sig - ARCHIVE=$(find "$BUNDLE_DIR/macos" -name '*.tar.gz' ! -name '*.sig' -type f | head -1) - SIG="${ARCHIVE}.sig" - if [[ -z "$ARCHIVE" || ! -f "$SIG" ]]; then - echo "::error::Updater archive or signature not found in $BUNDLE_DIR/macos" - exit 1 - fi echo "archive=$ARCHIVE" >> "$GITHUB_OUTPUT" - echo "archive_name=$(basename "$ARCHIVE")" >> "$GITHUB_OUTPUT" - echo "sig=$SIG" >> "$GITHUB_OUTPUT" + echo "archive_name=$ARCHIVE_NAME" >> "$GITHUB_OUTPUT" + echo "sig=$SIGNATURE" >> "$GITHUB_OUTPUT" + env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} - name: Generate latest.json run: | @@ -101,8 +121,8 @@ jobs: > latest.json cat latest.json env: - SIG_PATH: ${{ steps.artifacts.outputs.sig }} - ARCHIVE_NAME: ${{ steps.artifacts.outputs.archive_name }} + SIG_PATH: ${{ steps.updater.outputs.sig }} + ARCHIVE_NAME: ${{ steps.updater.outputs.archive_name }} - name: Create versioned GitHub release run: | @@ -114,7 +134,7 @@ jobs: "$DMG_PATH" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - DMG_PATH: ${{ steps.artifacts.outputs.dmg }} + DMG_PATH: ${{ steps.codesign.outputs.signed-dmg-path }} - name: Update rolling release for auto-updater run: | @@ -130,5 +150,5 @@ jobs: --clobber env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - ARCHIVE_PATH: ${{ steps.artifacts.outputs.archive }} - SIG_PATH: ${{ steps.artifacts.outputs.sig }} + ARCHIVE_PATH: ${{ steps.updater.outputs.archive }} + SIG_PATH: ${{ steps.updater.outputs.sig }} diff --git a/RELEASING.md b/RELEASING.md index f5be698227..b5a376b05e 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -17,6 +17,8 @@ desktop app. | `SPROUT_UPDATER_PUBLIC_KEY` | Tauri updater public key (minisign) | | `TAURI_SIGNING_PRIVATE_KEY` | Tauri updater private key (used to sign the update archive) | | `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` | Password for the private key | + | `OSX_CODESIGN_ROLE` | AWS IAM role ARN for OIDC authentication with the signing service | + | `CODESIGN_S3_BUCKET` | S3 bucket used for artifact transfer during signing | --- @@ -42,8 +44,9 @@ The workflow will: - Patch the version into `package.json`, `tauri.conf.json`, and `Cargo.toml` - Build all sidecar binaries (`sprout-acp`, `sprout-mcp`, `git-credential-nostr`) -- Build the Tauri desktop app with updater signing enabled -- Create a versioned GitHub release (`v0.4.0`) with the `.dmg` installer +- Build the Tauri desktop app +- Sign and notarize the app via Block's Apple code signing service +- Create a versioned GitHub release (`v0.4.0`) with the signed `.dmg` installer - Update the rolling `sprout-desktop-latest` release with the signed update archive and `latest.json` manifest for the auto-updater @@ -71,14 +74,17 @@ adding a matrix build to the workflow. ## Code Signing (macOS) -OSS release builds use **ad-hoc code signing** (`signingIdentity: "-"`) -rather than a Developer ID certificate. This means the app is not -notarized by Apple. +OSS release builds are **signed and notarized** via Block's Apple code +signing service (`block/apple-codesign-action`). The app is signed with a +Developer ID certificate and notarized by Apple, so users will not see +Gatekeeper warnings on first launch. -On first launch, macOS Gatekeeper will block the app with a "damaged" or -"unidentified developer" message. Users can bypass this by -**right-clicking the app > Open** (or via System Settings > Privacy & -Security). After the first launch the app will open normally. +The following additional secrets are required for code signing: + +| Secret | Purpose | +|--------|---------| +| `OSX_CODESIGN_ROLE` | AWS IAM role ARN for OIDC authentication with the signing service | +| `CODESIGN_S3_BUCKET` | S3 bucket used for artifact transfer during signing | --- @@ -104,10 +110,11 @@ The version string must be valid semver: `MAJOR.MINOR.PATCH` with an optional pre-release suffix (e.g. `1.0.0-beta.1`). Do not include a `v` prefix. -### Build fails at "Build Tauri app" -Check that the signing secrets are configured correctly. The build -requires `TAURI_SIGNING_PRIVATE_KEY` and -`TAURI_SIGNING_PRIVATE_KEY_PASSWORD` to be set. +### Build fails at "Sign with Apple codesigning service" +Check that `OSX_CODESIGN_ROLE` and `CODESIGN_S3_BUCKET` secrets are +configured correctly. The signing service requires valid AWS OIDC +credentials. Also verify the repository has `id-token: write` permission +in the workflow. ### Auto-updater reports "no update available" Verify that the `sprout-desktop-latest` release exists and contains a diff --git a/desktop/scripts/build-release-config.mjs b/desktop/scripts/build-release-config.mjs index 00f915bdf9..ccb696c004 100644 --- a/desktop/scripts/build-release-config.mjs +++ b/desktop/scripts/build-release-config.mjs @@ -9,15 +9,14 @@ import { resolve } from "node:path"; // // For OSS release builds this script emits: // 1. bundle.macOS.minimumSystemVersion = "10.15" for broad compatibility. -// 2. bundle.createUpdaterArtifacts = true so Tauri produces the .tar.gz -// archive and .sig signature during the build. -// 3. plugins.updater with the public key and endpoint from env vars. +// 2. plugins.updater with the public key and endpoint from env vars. // Both SPROUT_UPDATER_PUBLIC_KEY and SPROUT_UPDATER_ENDPOINT are required — // the script fails if either is missing (OSS builds always ship with updater). -// 4. bundle.macOS.signingIdentity = "-" for ad-hoc code signing. This -// prevents macOS Gatekeeper from rejecting the app as "damaged". -// Users will see the standard "unidentified developer" dialog on first -// launch, which they can bypass via right-click > Open. +// +// Note: signingIdentity is intentionally omitted — the workflow uses +// block/apple-codesign-action to sign the .app post-build. Updater +// artifacts (tar.gz + minisign signature) are also created post-signing +// so the archive contains the properly signed .app. const outputConfigPath = resolve( process.cwd(), @@ -41,9 +40,7 @@ const releaseConfig = { bundle: { macOS: { minimumSystemVersion: "10.15", - signingIdentity: "-", }, - createUpdaterArtifacts: true, }, plugins: { updater: {