Files
buzz/.github/workflows/release.yml
1b3dbcaaea ci(release): remove desktop smoke gate (#5914)
## Summary

- remove the GitHub-hosted desktop smoke job from the desktop release
workflow
- remove the smoke result from manifest assembly dependencies and
promotion conditions
- retain the local smoke tooling for future repair and targeted
validation

The first release execution of this gate spent its full 10-minute
Playwright timeout traversing the 10,000-row fixture, then produced a
987 MB diagnostics upload. All signed platform builds succeeded, but the
smoke prevented manifest publication. This restores the previously
established release boundary while the harness is made suitable for CI
separately.

### Testing

- parsed `.github/workflows/release.yml` with Ruby Psych and asserted
the smoke job/dependencies are absent
- `scripts/test-release-ref-contract.sh`
- exact pushed commit passed the repository pre-push hook

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-14 17:04:37 -07:00

950 lines
41 KiB
YAML

name: Release
concurrency:
group: desktop-release-${{ github.ref }}
cancel-in-progress: false
on:
push:
tags:
- 'desktop-v[0-9]*'
jobs:
# Shared setup: verify the immutable release tag, determine the version, and
# create the release objects all four platform jobs upload into.
setup:
name: Setup
if: github.repository == 'block/buzz'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
outputs:
version: ${{ steps.version.outputs.version }}
source_sha: ${{ steps.source.outputs.source_sha }}
steps:
- name: Determine version
id: version
run: echo "version=${GITHUB_REF_NAME#desktop-v}" >> "$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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
persist-credentials: false
- name: Verify tag-bound release source
id: source
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
scripts/verify-release-ref.sh desktop-v "$VERSION"
echo "source_sha=$(git rev-parse 'HEAD^{commit}')" >> "$GITHUB_OUTPUT"
release:
name: Release
if: github.repository == 'block/buzz'
runs-on: macos-latest
needs: setup
timeout-minutes: 60
permissions:
contents: read
id-token: write # required by block/apple-codesign-action for OIDC
outputs:
archive_name: ${{ steps.artifacts.outputs.archive_name }}
sig: ${{ steps.read-sig.outputs.sig }}
env:
VERSION: ${{ needs.setup.outputs.version }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ needs.setup.outputs.source_sha }}
fetch-depth: 0
persist-credentials: false
- name: Verify tag-bound release source
run: scripts/verify-release-ref.sh desktop-v "$VERSION"
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # 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:
BUZZ_UPDATER_PUBLIC_KEY: ${{ secrets.BUZZ_UPDATER_PUBLIC_KEY || secrets.SPROUT_UPDATER_PUBLIC_KEY }}
BUZZ_UPDATER_ENDPOINT: https://github.com/block/buzz/releases/download/buzz-desktop-latest/latest.json
- name: Build sidecars
run: |
cargo build --release -p buzz-acp -p buzz-agent -p buzz-backend-kubernetes -p buzz-dev-mcp -p git-credential-nostr -p buzz-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=$(python3 -c 'import tomllib; d=tomllib.load(open("Cargo.lock", "rb")); p=next(p for p in d["package"] if p["name"] == "mesh-llm-sdk"); print(p["source"].rsplit("#", 1)[1])')
[[ -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@caa296126883cff596d87d8935842f9db880ef25 # 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@caa296126883cff596d87d8935842f9db880ef25 # 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 --features mesh-llm --config src-tauri/tauri.release.conf.json
env:
BUZZ_UPDATER_PUBLIC_KEY: ${{ secrets.BUZZ_UPDATER_PUBLIC_KEY || secrets.SPROUT_UPDATER_PUBLIC_KEY }}
BUZZ_UPDATER_ENDPOINT: https://github.com/block/buzz/releases/download/buzz-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"
TAURI_BUNDLER_DMG_IGNORE_CI: "true"
- 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: Set DMG Finder label text size
env:
DMG_PATH: ${{ steps.unsigned.outputs.dmg }}
run: desktop/scripts/set-dmg-finder-text-size.sh "$DMG_PATH" 14
# mdx-ios-codesign-helper discovers this file by its exact lowercase basename.
- name: Stage signing entitlements
run: cp desktop/src-tauri/Entitlements.plist "${RUNNER_TEMP}/entitlements.plist"
- 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: ${{ runner.temp }}/entitlements.plist
artifact-name: buzz-${{ 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}/Buzz.app"
cp -R "${EXTRACT_DIR}/Buzz.app" "${APP_DIR}/Buzz.app"
# Rebuild the updater archive from the signed .app and re-sign it with the Tauri updater key.
rm -f "${APP_DIR}/Buzz.app.tar.gz" "${APP_DIR}/Buzz.app.tar.gz.sig"
(cd "$APP_DIR" && tar -czf Buzz.app.tar.gz Buzz.app)
TARBALL_ABS="$(pwd)/${APP_DIR}/Buzz.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/Buzz.app
spctl --assess --type execute --verbose=4 \
desktop/src-tauri/target/release/bundle/macos/Buzz.app
desktop/scripts/verify-macos-entitlements.sh \
desktop/src-tauri/target/release/bundle/macos/Buzz.app
- name: Locate build artifacts
id: artifacts
run: |
BUNDLE_DIR="desktop/src-tauri/target/release/bundle"
# Find the DMG (Tauri names it Buzz_<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. Give each architecture a unique
# release basename before artifacts are merged by the final writer.
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
RENAMED="$(dirname "$ARCHIVE")/Buzz_${VERSION}_aarch64.app.tar.gz"
mv "$ARCHIVE" "$RENAMED"
mv "$SIG" "${RENAMED}.sig"
ARCHIVE="$RENAMED"
SIG="${RENAMED}.sig"
echo "archive=$ARCHIVE" >> "$GITHUB_OUTPUT"
echo "archive_name=$(basename "$ARCHIVE")" >> "$GITHUB_OUTPUT"
echo "sig=$SIG" >> "$GITHUB_OUTPUT"
- name: Read updater signature
id: read-sig
run: echo "sig=$(cat "$SIG_PATH")" >> "$GITHUB_OUTPUT"
env:
SIG_PATH: ${{ steps.artifacts.outputs.sig }}
- name: Stage Apple Silicon release artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: desktop-release-macos-arm64
if-no-files-found: error
path: |
${{ steps.artifacts.outputs.dmg }}
${{ steps.artifacts.outputs.archive }}
${{ steps.artifacts.outputs.sig }}
release-macos-x64:
name: Release macOS (Intel)
if: github.repository == 'block/buzz'
runs-on: macos-latest
needs: setup
timeout-minutes: 60
permissions:
contents: read
id-token: write # required by block/apple-codesign-action for OIDC
outputs:
archive_name: ${{ steps.artifacts.outputs.archive_name }}
sig: ${{ steps.read-sig.outputs.sig }}
env:
VERSION: ${{ needs.setup.outputs.version }}
TARGET: x86_64-apple-darwin
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ needs.setup.outputs.source_sha }}
fetch-depth: 0
persist-credentials: false
- name: Verify tag-bound release source
run: scripts/verify-release-ref.sh desktop-v "$VERSION"
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
- name: Install desktop dependencies
run: just desktop-install-ci
- name: Add Rust target
run: rustup target add "$TARGET"
- 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:
BUZZ_UPDATER_PUBLIC_KEY: ${{ secrets.BUZZ_UPDATER_PUBLIC_KEY || secrets.SPROUT_UPDATER_PUBLIC_KEY }}
BUZZ_UPDATER_ENDPOINT: https://github.com/block/buzz/releases/download/buzz-desktop-latest/latest.json
- name: Build sidecars
run: |
cargo build --release --target "$TARGET" -p buzz-acp -p buzz-agent -p buzz-backend-kubernetes -p buzz-dev-mcp -p git-credential-nostr -p buzz-cli
./scripts/bundle-sidecars.sh "$TARGET"
- name: Build unsigned Tauri app
run: cd desktop && pnpm tauri build --verbose --no-sign --target "$TARGET" --config src-tauri/tauri.release.conf.json
env:
BUZZ_UPDATER_PUBLIC_KEY: ${{ secrets.BUZZ_UPDATER_PUBLIC_KEY || secrets.SPROUT_UPDATER_PUBLIC_KEY }}
BUZZ_UPDATER_ENDPOINT: https://github.com/block/buzz/releases/download/buzz-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"
TAURI_BUNDLER_DMG_IGNORE_CI: "true"
- name: Locate unsigned DMG
id: unsigned
run: |
BUNDLE_DIR="desktop/src-tauri/target/${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: Set DMG Finder label text size
env:
DMG_PATH: ${{ steps.unsigned.outputs.dmg }}
run: desktop/scripts/set-dmg-finder-text-size.sh "$DMG_PATH" 14
# mdx-ios-codesign-helper discovers this file by its exact lowercase basename.
- name: Stage signing entitlements
run: cp desktop/src-tauri/Entitlements.plist "${RUNNER_TEMP}/entitlements.plist"
- 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: ${{ runner.temp }}/entitlements.plist
artifact-name: buzz-${{ github.sha }}-${{ github.run_id }}-x64
- 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
APP_DIR="desktop/src-tauri/target/${TARGET}/release/bundle/macos"
# Replace the unsigned DMG with the signed/notarized one.
cp "$SIGNED_DMG" "$UNSIGNED_DMG"
# Swap the unsigned .app for the signed .app from the action's zip.
EXTRACT_DIR="${RUNNER_TEMP}/signed-app-extract-x64"
rm -rf "$EXTRACT_DIR" && mkdir -p "$EXTRACT_DIR"
ditto -x -k "$SIGNED_APP_ZIP" "$EXTRACT_DIR"
rm -rf "${APP_DIR}/Buzz.app"
cp -R "${EXTRACT_DIR}/Buzz.app" "${APP_DIR}/Buzz.app"
# Rebuild the updater archive from the signed .app and re-sign with the Tauri updater key.
rm -f "${APP_DIR}/Buzz.app.tar.gz" "${APP_DIR}/Buzz.app.tar.gz.sig"
(cd "$APP_DIR" && tar -czf Buzz.app.tar.gz Buzz.app)
TARBALL_ABS="$(pwd)/${APP_DIR}/Buzz.app.tar.gz"
(cd desktop && pnpm tauri signer sign "$TARBALL_ABS")
- name: Verify code signature
run: |
APP_DIR="desktop/src-tauri/target/${TARGET}/release/bundle/macos/Buzz.app"
codesign --verify --deep --strict --verbose=2 "$APP_DIR"
spctl --assess --type execute --verbose=4 "$APP_DIR"
desktop/scripts/verify-macos-entitlements.sh "$APP_DIR"
- name: Locate updater archive
id: artifacts
run: |
BUNDLE_DIR="desktop/src-tauri/target/${TARGET}/release/bundle"
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
RENAMED="$(dirname "$ARCHIVE")/Buzz_${VERSION}_x64.app.tar.gz"
mv "$ARCHIVE" "$RENAMED"
mv "$SIG" "${RENAMED}.sig"
ARCHIVE="$RENAMED"
SIG="${RENAMED}.sig"
echo "archive=$ARCHIVE" >> "$GITHUB_OUTPUT"
echo "archive_name=$(basename "$ARCHIVE")" >> "$GITHUB_OUTPUT"
echo "sig=$SIG" >> "$GITHUB_OUTPUT"
- name: Read updater signature
id: read-sig
run: echo "sig=$(cat "$SIG_PATH")" >> "$GITHUB_OUTPUT"
env:
SIG_PATH: ${{ steps.artifacts.outputs.sig }}
- name: Stage Intel macOS release artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: desktop-release-macos-x64
if-no-files-found: error
path: |
${{ steps.unsigned.outputs.dmg }}
${{ steps.artifacts.outputs.archive }}
${{ steps.artifacts.outputs.sig }}
release-linux:
name: Release Linux
if: github.repository == 'block/buzz'
runs-on: ubuntu-latest
# Digest-pinned like the SHA-pinned actions below; Renovate keeps it fresh.
container: ubuntu:24.04@sha256:4fbb8e6a8395de5a7550b33509421a2bafbc0aab6c06ba2cef9ebffbc7092d90
needs: setup
timeout-minutes: 60
permissions:
contents: read
env:
# AppImage tools (linuxdeploy, appimagetool) are themselves AppImages.
# Containers lack FUSE, so we must use the extract-and-run fallback.
APPIMAGE_EXTRACT_AND_RUN: "1"
# This job runs in a container where the default run shell is dash;
# the AppImage steps below use bash-only syntax ([[ ]], mapfile, arrays).
defaults:
run:
shell: bash
outputs:
archive_name: ${{ steps.linux-artifacts.outputs.archive_name }}
sig: ${{ steps.read-sig.outputs.sig }}
steps:
- name: Install system dependencies
env:
DEBIAN_FRONTEND: noninteractive
run: |
# Must run first: bare ubuntu:24.04 ships without curl, wget, git, or
# ca-certificates. activate-hermit bootstraps via curl+HTTPS (needs
# both), and actions/checkout falls back to a REST tarball without git.
# Running as root — no sudo needed.
apt-get update \
-o Acquire::Retries=3 \
-o Acquire::http::Timeout=30 \
-o Acquire::https::Timeout=30
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 \
ca-certificates \
curl \
desktop-file-utils \
file \
git \
libasound2-dev \
libayatana-appindicator3-dev \
libgtk-3-dev \
librsvg2-dev \
libssl-dev \
libwebkit2gtk-4.1-dev \
libxdo-dev \
patchelf \
pkg-config \
squashfs-tools \
wget \
xdg-utils
# Install GitHub CLI — preinstalled on runners but absent in containers.
# wget and ca-certificates are now available from the step above.
mkdir -p -m 755 /etc/apt/keyrings
wget -q --tries=3 --timeout=30 -O /usr/share/keyrings/githubcli-archive-keyring.gpg \
https://cli.github.com/packages/githubcli-archive-keyring.gpg
# Pin the keyring like appimagetool below. If GitHub rotates the
# keyring this fails loudly — recompute and update the hash.
echo "6084d5d7bd8e288441e0e94fc6275570895da18e6751f70f057485dc2d1a811b /usr/share/keyrings/githubcli-archive-keyring.gpg" | sha256sum -c
chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
> /etc/apt/sources.list.d/github-cli.list
apt-get update
apt-get install -y --no-install-recommends gh
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ needs.setup.outputs.source_sha }}
fetch-depth: 0
persist-credentials: false
- name: Mark workspace safe for git (containerized job)
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
- name: Verify tag-bound release source
env:
VERSION: ${{ needs.setup.outputs.version }}
run: scripts/verify-release-ref.sh desktop-v "$VERSION"
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
workspaces: desktop/src-tauri
lookup-only: true
- name: Install appimagetool
run: |
# Pin to an immutable release tag to avoid supply-chain drift from the
# mutable `continuous` tag. Tag: 1.9.1, asset: appimagetool-<arch>.AppImage
# (https://github.com/AppImage/appimagetool/releases/tag/1.9.1)
case "$(uname -m)" in
x86_64) ARCH_SUFFIX="x86_64" ;;
aarch64) ARCH_SUFFIX="aarch64" ;;
*)
echo "::error::Unsupported architecture: $(uname -m)"
exit 1
;;
esac
wget -q --tries=3 --timeout=30 -O /tmp/appimagetool \
"https://github.com/AppImage/appimagetool/releases/download/1.9.1/appimagetool-${ARCH_SUFFIX}.AppImage"
# SHA256 integrity check. Refuse to run an unverified binary: if a new
# arch (e.g. aarch64) is enabled in CI, compute its hash and add it here.
if [[ "$ARCH_SUFFIX" == "x86_64" ]]; then
echo "ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0 /tmp/appimagetool" | sha256sum -c
else
echo "::error::No pinned SHA256 for appimagetool-${ARCH_SUFFIX} — add it before enabling this architecture"
exit 1
fi
install -m 755 /tmp/appimagetool /usr/local/bin/appimagetool
# appimagetool otherwise fetches the AppImage type2 runtime from the
# MUTABLE `continuous` tag at repack time — the runtime is the first
# code users execute, so pin it too. Tag: 20251108, hash is for the
# x86_64 asset (non-x86_64 already hard-fails above).
# (https://github.com/AppImage/type2-runtime/releases/tag/20251108)
wget -q --tries=3 --timeout=30 -O /tmp/appimage-runtime \
"https://github.com/AppImage/type2-runtime/releases/download/20251108/runtime-${ARCH_SUFFIX}"
echo "2fca8b443c92510f1483a883f60061ad09b46b978b2631c807cd873a47ec260d /tmp/appimage-runtime" | sha256sum -c
install -D -m 644 /tmp/appimage-runtime /usr/local/lib/appimage-runtime
echo "APPIMAGETOOL_RUNTIME_FILE=/usr/local/lib/appimage-runtime" >> "$GITHUB_ENV"
- name: Install desktop dependencies
run: just desktop-install-ci
- name: Patch version
env:
VERSION: ${{ needs.setup.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 buzz-acp -p buzz-agent -p buzz-backend-kubernetes -p buzz-dev-mcp -p git-credential-nostr -p buzz-cli
./scripts/bundle-sidecars.sh
- name: Generate release config
run: cd desktop && node scripts/build-release-config.mjs
env:
BUZZ_UPDATER_PUBLIC_KEY: ${{ secrets.BUZZ_UPDATER_PUBLIC_KEY || secrets.SPROUT_UPDATER_PUBLIC_KEY }}
BUZZ_UPDATER_ENDPOINT: https://github.com/block/buzz/releases/download/buzz-desktop-latest/latest.json
- name: Build Linux Tauri app
run: cd desktop && pnpm tauri build --verbose --ci --bundles deb,appimage --features mesh-llm --config src-tauri/tauri.release.conf.json
env:
BUZZ_UPDATER_PUBLIC_KEY: ${{ secrets.BUZZ_UPDATER_PUBLIC_KEY || secrets.SPROUT_UPDATER_PUBLIC_KEY }}
BUZZ_UPDATER_ENDPOINT: https://github.com/block/buzz/releases/download/buzz-desktop-latest/latest.json
CMAKE_POLICY_VERSION_MINIMUM: "3.5"
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
- name: Fix AppImage (remove infra libs, shim host GStreamer)
run: |
mapfile -t APPIMAGES < <(find desktop/src-tauri/target/release/bundle/appimage -name '*.AppImage' -type f)
if [[ ${#APPIMAGES[@]} -eq 0 ]]; then
echo "::error::No AppImage found to post-process"
exit 1
fi
if [[ ${#APPIMAGES[@]} -gt 1 ]]; then
echo "::error::Expected exactly one AppImage, found ${#APPIMAGES[@]}: ${APPIMAGES[*]}"
exit 1
fi
bash desktop/scripts/fix-appimage.sh "${APPIMAGES[0]}"
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
- 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"
# Updater archive: Tauri 2.11+ with createUpdaterArtifacts signs the
# AppImage directly (*.AppImage + *.AppImage.sig). Earlier versions
# wrapped it in a tar.gz. Try the new format first, fall back to legacy.
ARCHIVE=$(find "$BUNDLE_DIR/appimage" -name '*.AppImage.tar.gz' ! -name '*.sig' -type f | head -1)
if [[ -n "$ARCHIVE" ]]; then
SIG="${ARCHIVE}.sig"
else
ARCHIVE="$APPIMAGE"
SIG="${APPIMAGE}.sig"
fi
if [[ -z "$ARCHIVE" || ! -f "$SIG" ]]; then
echo "::error::AppImage updater archive or signature not found in $BUNDLE_DIR/appimage"
exit 1
fi
echo "archive=$ARCHIVE" >> "$GITHUB_OUTPUT"
echo "archive_name=$(basename "$ARCHIVE")" >> "$GITHUB_OUTPUT"
echo "sig=$SIG" >> "$GITHUB_OUTPUT"
- name: Read updater signature
id: read-sig
run: echo "sig=$(cat "$SIG_PATH")" >> "$GITHUB_OUTPUT"
env:
SIG_PATH: ${{ steps.linux-artifacts.outputs.sig }}
# NOTE: .deb is NOT auto-updatable (Tauri updater constraint — only AppImage supports it on Linux)
- name: Stage Linux release artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: desktop-release-linux-x64
if-no-files-found: error
path: |
${{ steps.linux-artifacts.outputs.deb }}
${{ steps.linux-artifacts.outputs.appimage }}
${{ steps.linux-artifacts.outputs.archive }}
${{ steps.linux-artifacts.outputs.sig }}
release-windows:
name: Release Windows
runs-on: windows-latest
needs: setup
timeout-minutes: 60
permissions:
contents: read
outputs:
archive_name: ${{ steps.artifacts.outputs.archive_name }}
sig: ${{ steps.read-sig.outputs.sig }}
env:
VERSION: ${{ needs.setup.outputs.version }}
TARGET: x86_64-pc-windows-msvc
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ needs.setup.outputs.source_sha }}
fetch-depth: 0
persist-credentials: false
- name: Verify tag-bound release source
shell: bash
run: scripts/verify-release-ref.sh desktop-v "$VERSION"
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
with:
targets: ${{ env.TARGET }}
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 24.14.1
# Disable dependency caching: a writable cache in this release workflow
# (contents: read, feeds a signed installer) is a poisoning vector. pnpm
# install runs uncached below.
package-manager-cache: false
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0
with:
version: 11.4.0
- name: Install desktop dependencies
shell: bash
run: pnpm install --frozen-lockfile
- name: Patch version
shell: bash
run: |
cd desktop && node scripts/set-version-from-tag.mjs "$VERSION"
cd src-tauri && cargo update --workspace
- name: Generate release config
shell: bash
run: cd desktop && node scripts/build-release-config.mjs
env:
BUZZ_UPDATER_PUBLIC_KEY: ${{ secrets.BUZZ_UPDATER_PUBLIC_KEY || secrets.SPROUT_UPDATER_PUBLIC_KEY }}
BUZZ_UPDATER_ENDPOINT: https://github.com/block/buzz/releases/download/buzz-desktop-latest/latest.json
- name: Build sidecars
shell: bash
run: |
cargo build --release --target "$TARGET" -p buzz-acp -p buzz-agent -p buzz-dev-mcp -p git-credential-nostr -p buzz-cli
./scripts/bundle-sidecars.sh "$TARGET"
- name: Build Windows NSIS installer (unsigned)
shell: bash
run: cd desktop && pnpm tauri build --verbose --target "$TARGET" --bundles nsis --config src-tauri/tauri.release.conf.json
env:
BUZZ_UPDATER_PUBLIC_KEY: ${{ secrets.BUZZ_UPDATER_PUBLIC_KEY || secrets.SPROUT_UPDATER_PUBLIC_KEY }}
BUZZ_UPDATER_ENDPOINT: https://github.com/block/buzz/releases/download/buzz-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 Windows build artifacts
id: artifacts
shell: bash
run: |
BUNDLE_DIR="desktop/src-tauri/target/${TARGET}/release/bundle"
# Find the NSIS installer .exe
EXE=$(find "$BUNDLE_DIR/nsis" -name '*.exe' -type f | head -1)
if [[ -z "$EXE" ]]; then
echo "::error::No NSIS installer found in $BUNDLE_DIR/nsis"
exit 1
fi
# Tauri 2.x with createUpdaterArtifacts: true signs the NSIS
# installer in place (<name>-setup.exe + <name>-setup.exe.sig).
SIG="${EXE}.sig"
if [[ ! -f "$SIG" ]]; then
echo "::error::NSIS installer signature not found: $SIG"
exit 1
fi
# Rename with _alpha-unsigned marker, keeping the detached signature
# in lockstep so latest.json matches the uploaded updater artifact.
EXE_DIR=$(dirname "$EXE")
EXE_BASE=$(basename "$EXE" .exe)
MARKED_EXE="${EXE_DIR}/${EXE_BASE}_alpha-unsigned.exe"
MARKED_SIG="${MARKED_EXE}.sig"
mv "$EXE" "$MARKED_EXE"
mv "$SIG" "$MARKED_SIG"
echo "exe=$MARKED_EXE" >> "$GITHUB_OUTPUT"
echo "archive=$MARKED_EXE" >> "$GITHUB_OUTPUT"
echo "archive_name=$(basename "$MARKED_EXE")" >> "$GITHUB_OUTPUT"
echo "sig=$MARKED_SIG" >> "$GITHUB_OUTPUT"
- name: Read updater signature
id: read-sig
shell: bash
run: echo "sig=$(cat "$SIG_PATH")" >> "$GITHUB_OUTPUT"
env:
SIG_PATH: ${{ steps.artifacts.outputs.sig }}
- name: Stage Windows release artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: desktop-release-windows-x64
if-no-files-found: error
path: |
${{ steps.artifacts.outputs.exe }}
${{ steps.artifacts.outputs.sig }}
assemble-manifest:
name: Assemble multi-platform latest.json
# Only the tag-bound setup path can reach this job.
if: |
always() &&
needs.setup.result == 'success' &&
needs.release.result == 'success' &&
needs.release-macos-x64.result == 'success' &&
needs.release-linux.result == 'success' &&
needs.release-windows.result == 'success' &&
github.ref == format('refs/tags/desktop-v{0}', needs.setup.outputs.version)
runs-on: ubuntu-latest
needs: [setup, release, release-macos-x64, release-linux, release-windows]
timeout-minutes: 10
permissions:
contents: write
env:
VERSION: ${{ needs.setup.outputs.version }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ needs.setup.outputs.source_sha }}
fetch-depth: 0
persist-credentials: false
- name: Verify tag-bound release source
run: scripts/verify-release-ref.sh desktop-v "$VERSION"
- name: Download staged release artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: desktop-release-*
path: staged-by-platform
- name: Flatten staged artifacts without basename collisions
run: |
set -euo pipefail
mkdir staged
while IFS= read -r -d '' file; do
name="$(basename "$file")"
[[ ! -e "staged/$name" ]] || {
echo "::error::release artifact basename collision: $name"
exit 1
}
cp "$file" "staged/$name"
done < <(find staged-by-platform -type f -print0)
- name: Write signature files
env:
RESULT_ARM64: ${{ needs.release.result }}
RESULT_X64: ${{ needs.release-macos-x64.result }}
RESULT_LINUX: ${{ needs.release-linux.result }}
RESULT_WIN: ${{ needs.release-windows.result }}
SIG_ARM64: ${{ needs.release.outputs.sig }}
SIG_X64: ${{ needs.release-macos-x64.outputs.sig }}
SIG_LINUX: ${{ needs.release-linux.outputs.sig }}
SIG_WIN: ${{ needs.release-windows.outputs.sig }}
run: |
set -euo pipefail
mkdir -p /tmp/sigs
write_sig() {
local result="$1" platform="$2" sig="$3"
if [[ "$result" == "success" ]]; then
[[ -n "$sig" ]] || { echo "::error::Missing signature for successful platform: $platform"; exit 1; }
printf '%s' "$sig" > "/tmp/sigs/${platform}.sig"
fi
}
write_sig "$RESULT_ARM64" darwin-aarch64 "$SIG_ARM64"
write_sig "$RESULT_X64" darwin-x86_64 "$SIG_X64"
write_sig "$RESULT_LINUX" linux-x86_64 "$SIG_LINUX"
write_sig "$RESULT_WIN" windows-x86_64 "$SIG_WIN"
- name: Verify draft release has every updater archive
env:
RESULT_ARM64: ${{ needs.release.result }}
RESULT_X64: ${{ needs.release-macos-x64.result }}
RESULT_LINUX: ${{ needs.release-linux.result }}
RESULT_WIN: ${{ needs.release-windows.result }}
ARCHIVE_ARM64: ${{ needs.release.outputs.archive_name }}
ARCHIVE_X64: ${{ needs.release-macos-x64.outputs.archive_name }}
ARCHIVE_LINUX: ${{ needs.release-linux.outputs.archive_name }}
ARCHIVE_WIN: ${{ needs.release-windows.outputs.archive_name }}
run: |
set -euo pipefail
assets=$(find staged -type f -exec basename {} \;)
for spec in \
"$RESULT_ARM64:$ARCHIVE_ARM64" \
"$RESULT_X64:$ARCHIVE_X64" \
"$RESULT_LINUX:$ARCHIVE_LINUX" \
"$RESULT_WIN:$ARCHIVE_WIN"; do
result="${spec%%:*}"
archive="${spec#*:}"
if [[ "$result" == success ]]; then
[[ -n "$archive" ]] || { echo "::error::successful platform has no archive"; exit 1; }
grep -Fxq "$archive" <<<"$assets" || { echo "::error::draft release missing $archive"; exit 1; }
fi
done
- name: Generate unified latest.json
env:
RESULT_ARM64: ${{ needs.release.result }}
RESULT_X64: ${{ needs.release-macos-x64.result }}
RESULT_LINUX: ${{ needs.release-linux.result }}
RESULT_WIN: ${{ needs.release-windows.result }}
ARCHIVE_ARM64: ${{ needs.release.outputs.archive_name }}
ARCHIVE_X64: ${{ needs.release-macos-x64.outputs.archive_name }}
ARCHIVE_LINUX: ${{ needs.release-linux.outputs.archive_name }}
ARCHIVE_WIN: ${{ needs.release-windows.outputs.archive_name }}
run: |
set -euo pipefail
BASE="https://github.com/block/buzz/releases/download/desktop-v${VERSION}"
TRIPLES=()
add_triple() {
local result="$1" platform="$2" archive="$3"
if [[ "$result" == "success" ]]; then
[[ -n "$archive" ]] || { echo "::error::Missing archive name for successful platform: $platform"; exit 1; }
TRIPLES+=("${platform}:/tmp/sigs/${platform}.sig:${BASE}/${archive}")
fi
}
add_triple "$RESULT_ARM64" darwin-aarch64 "$ARCHIVE_ARM64"
add_triple "$RESULT_X64" darwin-x86_64 "$ARCHIVE_X64"
add_triple "$RESULT_LINUX" linux-x86_64 "$ARCHIVE_LINUX"
add_triple "$RESULT_WIN" windows-x86_64 "$ARCHIVE_WIN"
[ "${#TRIPLES[@]}" -ge 3 ] || { echo "::error::too few platforms (${#TRIPLES[@]})"; exit 1; }
bash desktop/scripts/generate-oss-latest-json.sh "$VERSION" "${TRIPLES[@]}" > latest.json
cat latest.json
cp latest.json staged/updater-manifest.json
- name: Create or verify versioned draft
run: |
set -euo pipefail
NOTES_FILE="${RUNNER_TEMP}/release-notes.md"
awk "/^## v${VERSION}\$/{found=1; next} found && /^## v/{exit} found" CHANGELOG.md > "$NOTES_FILE"
[[ -s "$NOTES_FILE" ]] || { echo "::error::missing non-empty changelog block for v${VERSION}"; exit 1; }
PRERELEASE_FLAGS=()
if [[ "$VERSION" == *-* ]]; then
PRERELEASE_FLAGS=(--prerelease --latest=false)
fi
if gh release view "desktop-v${VERSION}" >/dev/null 2>&1; then
EXISTING_SHA=$(gh release view "desktop-v${VERSION}" --json targetCommitish --jq .targetCommitish)
IS_DRAFT=$(gh release view "desktop-v${VERSION}" --json isDraft --jq .isDraft)
[[ "$EXISTING_SHA" == "${{ needs.setup.outputs.source_sha }}" ]] || {
echo "::error::existing release targets $EXISTING_SHA, not the immutable source"; exit 1;
}
if [[ "$IS_DRAFT" != true ]]; then
echo "already_published=true" >> "$GITHUB_ENV"
fi
else
gh release create "desktop-v${VERSION}" \
--draft \
--target "${{ needs.setup.outputs.source_sha }}" \
--title "Buzz Desktop v${VERSION}" \
--notes-file "$NOTES_FILE" \
"${PRERELEASE_FLAGS[@]}"
fi
- name: Upload complete artifact set to versioned draft
if: env.already_published != 'true'
run: |
mapfile -t files < <(find staged -type f -print)
[[ "${#files[@]}" -gt 0 ]] || { echo "::error::no staged release artifacts"; exit 1; }
gh release upload "desktop-v${VERSION}" "${files[@]}" --clobber
- name: Publish complete versioned release
if: env.already_published != 'true'
run: gh release edit "desktop-v${VERSION}" --draft=false