mirror of
https://github.com/block/buzz.git
synced 2026-11-04 00:12:02 +00:00
The sync-release-changelog workflow now uses a GitHub App token instead of GITHUB_TOKEN so pushes to the release PR branch re-trigger CI (the repo's ruleset requires 5 status checks to pass). Also hardens PR discovery with --limit 100 --base main, and serializes concurrent runs with cancel-in-progress: false to prevent non-fast-forward push races. release.yml generates GitHub release notes from git log at release time instead of reading from CHANGELOG.md, closing the merge-before-sync race where someone merges the release PR before the changelog sync finishes. auto-tag-on-release-pr-merge.yml now triggers the internal sprout-releases Buildkite pipeline via REST API after pushing the tag, passing version metadata so the pipeline's input step is skipped.
359 lines
14 KiB
YAML
359 lines
14 KiB
YAML
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 }}
|
|
fetch-depth: 0
|
|
fetch-tags: true
|
|
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
|
|
|
|
# Mesh rev derived from Cargo.lock (no lockstep edit on dep bump); cache key tracks it.
|
|
- name: Resolve mesh-llm rev
|
|
id: mesh_rev
|
|
run: |
|
|
set -euo pipefail
|
|
REV=$(grep -oE 'mesh-llm\.git\?rev=[0-9a-f]{40}' Cargo.lock | head -1 | grep -oE '[0-9a-f]{40}')
|
|
[[ -n "$REV" ]] || { echo "::error::could not resolve mesh-llm rev from Cargo.lock"; exit 1; }
|
|
echo "rev=$REV" >> "$GITHUB_OUTPUT"
|
|
echo "short=${REV:0:7}" >> "$GITHUB_OUTPUT"
|
|
- name: Restore mesh llama build cache
|
|
id: llama_cache
|
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
|
with:
|
|
path: ${{ github.workspace }}/.cache/mesh-llama
|
|
key: mesh-llama-${{ runner.os }}-metal-${{ steps.mesh_rev.outputs.rev }}
|
|
- name: Build mesh llama native libraries
|
|
if: steps.llama_cache.outputs.cache-hit != 'true'
|
|
env:
|
|
MESH_REV_SHORT: ${{ steps.mesh_rev.outputs.short }}
|
|
run: |
|
|
set -euo pipefail
|
|
cargo fetch --manifest-path desktop/src-tauri/Cargo.toml
|
|
SHORT="$MESH_REV_SHORT"
|
|
MESH_ROOT=$(find "${CARGO_HOME:-$HOME/.cargo}/git/checkouts" -path "*/$SHORT" -type d -name "$SHORT" | head -1)
|
|
if [[ -z "$MESH_ROOT" ]]; then
|
|
echo "::error::mesh-llm checkout for $SHORT not found after cargo fetch"
|
|
exit 1
|
|
fi
|
|
export LLAMA_STAGE_BACKEND=metal
|
|
export LLAMA_STAGE_BUILD_DIR="$GITHUB_WORKSPACE/.cache/mesh-llama/build-stage-abi-metal"
|
|
export CMAKE_OSX_DEPLOYMENT_TARGET=10.15
|
|
"$MESH_ROOT/scripts/prepare-llama.sh" pinned
|
|
"$MESH_ROOT/scripts/build-llama.sh" -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15
|
|
- name: Save mesh llama build cache
|
|
if: steps.llama_cache.outputs.cache-hit != 'true'
|
|
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
|
with:
|
|
path: ${{ github.workspace }}/.cache/mesh-llama
|
|
key: mesh-llama-${{ runner.os }}-metal-${{ steps.mesh_rev.outputs.rev }}
|
|
|
|
- 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"
|
|
MACOSX_DEPLOYMENT_TARGET: "10.15"
|
|
CMAKE_OSX_DEPLOYMENT_TARGET: "10.15"
|
|
LLAMA_STAGE_BACKEND: metal
|
|
LLAMA_STAGE_BUILD_DIR: ${{ github.workspace }}/.cache/mesh-llama/build-stage-abi-metal
|
|
SKIPPY_LLAMA_AUTO_BUILD: "0"
|
|
|
|
- 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_<version>_<arch>.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)
|
|
PREV_TAG=$(git describe --tags --abbrev=0 --match 'v[0-9]*' HEAD^ 2>/dev/null || echo "")
|
|
if [[ -n "$PREV_TAG" ]]; then
|
|
NOTES=$(git log "${PREV_TAG}..HEAD" --oneline --no-merges)
|
|
else
|
|
NOTES="Initial release"
|
|
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
|