name: Release on: push: tags: - 'v[0-9]*' workflow_dispatch: inputs: version: description: "Semver version (e.g. 0.4.0) — only for manual runs" required: true ref: description: "Branch, tag, or SHA to build — only for manual runs" default: main required: true jobs: release: name: Release runs-on: macos-latest timeout-minutes: 60 permissions: contents: write id-token: write # required by block/apple-codesign-action for OIDC outputs: version: ${{ steps.version.outputs.version }} steps: - name: Determine version id: version env: EVENT_NAME: ${{ github.event_name }} INPUT_VERSION: ${{ inputs.version }} run: | if [[ "$EVENT_NAME" == "push" ]]; then VERSION="${GITHUB_REF_NAME#v}" else VERSION="$INPUT_VERSION" fi echo "version=$VERSION" >> "$GITHUB_OUTPUT" - name: Validate version env: VERSION: ${{ steps.version.outputs.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: ${{ github.event_name == 'push' && github.ref || inputs.ref }} persist-credentials: false - uses: cashapp/activate-hermit@e49f5cb4dd64ff0b0b659d1d8df499595451155a # v1 - name: Install desktop dependencies run: just desktop-install-ci - name: Patch version env: VERSION: ${{ steps.version.outputs.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 sprout-agent -p sprout-dev-mcp -p git-credential-nostr -p sprout-cli ./scripts/bundle-sidecars.sh - name: Build unsigned Tauri app run: cd desktop && pnpm tauri build --verbose --no-sign --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 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: Locate unsigned DMG id: unsigned run: | BUNDLE_DIR="desktop/src-tauri/target/release/bundle" DMG=$(find "$BUNDLE_DIR/dmg" -name '*.dmg' -type f | head -1) if [[ -z "$DMG" ]]; then echo "::error::No DMG found in $BUNDLE_DIR/dmg" exit 1 fi echo "dmg=$DMG" >> "$GITHUB_OUTPUT" - name: Codesign and Notarize 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: ${{ steps.unsigned.outputs.dmg }} entitlements-plist-path: desktop/src-tauri/Entitlements.plist artifact-name: sprout-${{ github.sha }}-${{ github.run_id }}-arm64 - name: Replace DMG and rebuild updater archive env: SIGNED_DMG: ${{ steps.codesign.outputs.signed-dmg-path }} SIGNED_APP_ZIP: ${{ steps.codesign.outputs.signed-artifact-path }} UNSIGNED_DMG: ${{ steps.unsigned.outputs.dmg }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} run: | set -euo pipefail BUNDLE_DIR="desktop/src-tauri/target/release/bundle" APP_DIR="${BUNDLE_DIR}/macos" # Replace unsigned DMG with the signed/notarized one. cp "$SIGNED_DMG" "$UNSIGNED_DMG" # Swap the unsigned .app for the signed .app extracted from the action's zip. EXTRACT_DIR="${RUNNER_TEMP}/signed-app-extract" rm -rf "$EXTRACT_DIR" && mkdir -p "$EXTRACT_DIR" ditto -x -k "$SIGNED_APP_ZIP" "$EXTRACT_DIR" rm -rf "${APP_DIR}/Sprout.app" cp -R "${EXTRACT_DIR}/Sprout.app" "${APP_DIR}/Sprout.app" # Rebuild the updater archive from the signed .app and re-sign it with the Tauri updater key. rm -f "${APP_DIR}/Sprout.app.tar.gz" "${APP_DIR}/Sprout.app.tar.gz.sig" (cd "$APP_DIR" && tar -czf Sprout.app.tar.gz Sprout.app) TARBALL_ABS="$(pwd)/${APP_DIR}/Sprout.app.tar.gz" (cd desktop && pnpm tauri signer sign "$TARBALL_ABS") - name: Verify code signature run: | codesign --verify --deep --strict --verbose=2 \ desktop/src-tauri/target/release/bundle/macos/Sprout.app spctl --assess --type execute --verbose=4 \ desktop/src-tauri/target/release/bundle/macos/Sprout.app - name: Locate build artifacts id: artifacts run: | BUNDLE_DIR="desktop/src-tauri/target/release/bundle" # 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" 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" - 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: VERSION: ${{ steps.version.outputs.version }} SIG_PATH: ${{ steps.artifacts.outputs.sig }} ARCHIVE_NAME: ${{ steps.artifacts.outputs.archive_name }} - name: Create versioned GitHub release env: VERSION: ${{ steps.version.outputs.version }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} DMG_PATH: ${{ steps.artifacts.outputs.dmg }} run: | RELEASE_SHA=$(git rev-parse HEAD) NOTES="" if [[ -f CHANGELOG.md ]]; then NOTES=$(awk "/^## v${VERSION}$/,/^## v/" CHANGELOG.md | sed '$d') fi if [[ -z "$NOTES" ]]; then NOTES="Sprout Desktop v${VERSION}" fi gh release create "v${VERSION}" \ --target "$RELEASE_SHA" \ --title "Sprout Desktop v${VERSION}" \ --notes "$NOTES" \ "$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.artifacts.outputs.archive }} SIG_PATH: ${{ steps.artifacts.outputs.sig }} release-linux: name: Release Linux runs-on: ubuntu-latest needs: release timeout-minutes: 60 permissions: contents: write steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: ref: ${{ github.event_name == 'push' && github.ref || inputs.ref }} persist-credentials: false - uses: cashapp/activate-hermit@e49f5cb4dd64ff0b0b659d1d8df499595451155a # v1 - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 with: workspaces: desktop/src-tauri lookup-only: true - name: Install Tauri dependencies (Linux) env: DEBIAN_FRONTEND: noninteractive run: | sudo apt-get update \ -o Acquire::Retries=3 \ -o Acquire::http::Timeout=30 \ -o Acquire::https::Timeout=30 sudo apt-get install -y --no-install-recommends \ -o Acquire::Retries=3 \ -o Acquire::http::Timeout=30 \ -o Acquire::https::Timeout=30 \ -o DPkg::Lock::Timeout=120 \ build-essential \ curl \ file \ libasound2-dev \ libayatana-appindicator3-dev \ libgtk-3-dev \ librsvg2-dev \ libssl-dev \ libwebkit2gtk-4.1-dev \ libxdo-dev \ patchelf \ wget - name: Install desktop dependencies run: just desktop-install-ci - name: Patch version env: VERSION: ${{ needs.release.outputs.version }} run: | cd desktop && node scripts/set-version-from-tag.mjs "$VERSION" cd src-tauri && cargo update --workspace - name: Build sidecars run: | cargo build --release -p sprout-acp -p sprout-mcp -p sprout-agent -p sprout-dev-mcp -p git-credential-nostr -p sprout-cli ./scripts/bundle-sidecars.sh - name: Build Linux Tauri app run: cd desktop && pnpm tauri build --verbose --ci --bundles deb,appimage env: CMAKE_POLICY_VERSION_MINIMUM: "3.5" - name: Locate Linux build artifacts id: linux-artifacts run: | BUNDLE_DIR="desktop/src-tauri/target/release/bundle" DEB=$(find "$BUNDLE_DIR/deb" -name '*.deb' -type f | head -1) if [[ -z "$DEB" ]]; then echo "::error::No DEB found in $BUNDLE_DIR/deb" exit 1 fi echo "deb=$DEB" >> "$GITHUB_OUTPUT" APPIMAGE=$(find "$BUNDLE_DIR/appimage" -name '*.AppImage' -type f | head -1) if [[ -z "$APPIMAGE" ]]; then echo "::error::No AppImage found in $BUNDLE_DIR/appimage" exit 1 fi echo "appimage=$APPIMAGE" >> "$GITHUB_OUTPUT" - name: Upload Linux artifacts to versioned GitHub release env: VERSION: ${{ needs.release.outputs.version }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} DEB_PATH: ${{ steps.linux-artifacts.outputs.deb }} APPIMAGE_PATH: ${{ steps.linux-artifacts.outputs.appimage }} run: | gh release upload "v$VERSION" \ "$DEB_PATH" \ "$APPIMAGE_PATH" \ --clobber