Files
buzz/.github/workflows/release.yml
Wes 74b557d47f 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 <noreply@anthropic.com>
2026-05-04 09:08:07 -07:00

155 lines
5.8 KiB
YAML

name: Release
on:
workflow_dispatch:
inputs:
version:
description: "Semver version (e.g. 0.4.0)"
required: true
ref:
description: "Branch, tag, or SHA to build"
default: main
required: true
jobs:
release:
name: Release
runs-on: macos-latest
timeout-minutes: 120
permissions:
contents: write
id-token: write
env:
VERSION: ${{ inputs.version }}
steps:
- name: Validate version
run: |
if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'; then
echo "::error::Invalid version '$VERSION'. Expected semver (e.g. 0.4.0 or 1.0.0-beta.1)"
exit 1
fi
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ inputs.ref }}
persist-credentials: false
- uses: cashapp/activate-hermit@e49f5cb4dd64ff0b0b659d1d8df499595451155a # v1
- name: Install desktop dependencies
run: just desktop-install-ci
- name: Patch version
run: |
cd desktop && node scripts/set-version-from-tag.mjs "$VERSION"
cd src-tauri && cargo update --workspace
- name: Generate release config
run: cd desktop && node scripts/build-release-config.mjs
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
- name: Build sidecars
run: |
cargo build --release -p sprout-acp -p sprout-mcp -p git-credential-nostr
./scripts/bundle-sidecars.sh
- name: Build Tauri app
run: cd desktop && pnpm tauri build --verbose --config src-tauri/tauri.release.conf.json
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
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: |
# 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: Create updater artifacts
id: updater
run: |
SIGNED_APP_DIR="${RUNNER_TEMP}/signed-app"
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 "archive=$ARCHIVE" >> "$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: |
bash desktop/scripts/generate-oss-latest-json.sh \
"$VERSION" \
"$SIG_PATH" \
"https://github.com/block/sprout/releases/download/sprout-desktop-latest/$ARCHIVE_NAME" \
> latest.json
cat latest.json
env:
SIG_PATH: ${{ steps.updater.outputs.sig }}
ARCHIVE_NAME: ${{ steps.updater.outputs.archive_name }}
- name: Create versioned GitHub release
run: |
RELEASE_SHA=$(git rev-parse HEAD)
gh release create "v${VERSION}" \
--target "$RELEASE_SHA" \
--title "Sprout Desktop v${VERSION}" \
--notes "Sprout Desktop v${VERSION}" \
"$DMG_PATH"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DMG_PATH: ${{ steps.codesign.outputs.signed-dmg-path }}
- name: Update rolling release for auto-updater
run: |
gh release create sprout-desktop-latest \
--prerelease \
--title "Sprout Desktop Auto-Update" \
--notes "Rolling release for the Tauri auto-updater. Do not download manually — use the versioned release instead." \
2>/dev/null || true
gh release upload sprout-desktop-latest \
latest.json \
"$ARCHIVE_PATH" \
"$SIG_PATH" \
--clobber
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ARCHIVE_PATH: ${{ steps.updater.outputs.archive }}
SIG_PATH: ${{ steps.updater.outputs.sig }}