feat: import Chinese-localized Buzz source snapshot
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Signed-off-by: cls_宁波本机 <908705107@qq.com>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
* @block/buzz-oss-team
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Report a reproducible bug in Buzz
|
||||
labels: bug
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Steps to reproduce**
|
||||
1. Go to '...'
|
||||
2. Click on '...'
|
||||
3. See error
|
||||
|
||||
**Expected behavior**
|
||||
What did you expect to happen?
|
||||
|
||||
**Version and platform**
|
||||
Find your version at the bottom of the Settings sidebar. Write "unknown" if you can't determine it.
|
||||
- Buzz version:
|
||||
- OS:
|
||||
|
||||
**Logs / additional context**
|
||||
Paste any relevant logs, error messages, or screenshots here.
|
||||
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: true
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Propose a new feature or improvement
|
||||
labels: enhancement
|
||||
---
|
||||
|
||||
**Motivation**
|
||||
What problem does this solve? Who runs into it and when?
|
||||
|
||||
**Proposed solution**
|
||||
Describe what you'd like to see.
|
||||
|
||||
**Alternatives considered**
|
||||
What other approaches did you consider?
|
||||
|
||||
**Additional context**
|
||||
Anything else that helps — links, screenshots, prior art.
|
||||
|
||||
---
|
||||
|
||||
Before opening: please [search open issues and PRs](https://github.com/block/buzz/issues?q=is%3Aopen) for duplicates — link the closest one, or say "none found".
|
||||
@@ -0,0 +1,8 @@
|
||||
## Summary
|
||||
<!-- What does this change and why? -->
|
||||
|
||||
### Related issue
|
||||
<!-- Fixes #1234, or N/A. Before opening: search existing issues/PRs for duplicates — link the closest one, or say "none found". -->
|
||||
|
||||
### Testing
|
||||
<!-- How was this verified? UI change? Include before/after screenshots (or a short recording). -->
|
||||
@@ -0,0 +1,163 @@
|
||||
name: Auto-tag on Release PR Merge
|
||||
|
||||
# Four PR-driven release lanes share this workflow. Each uses an explicit branch
|
||||
# prefix; the main chart lane also auto-detects a Chart.yaml version bump so
|
||||
# a chart feature PR can publish its own new version when merged:
|
||||
#
|
||||
# version-bump/<v> → tag desktop-v<v> → release.yml (desktop app)
|
||||
# relay-release/<v> → tag relay-v<v> → docker.yml (relay image)
|
||||
# chart-release/<v> → tag chart-v<v> → helm-chart.yml (main helm chart)
|
||||
# push-chart-release/<v> → tag push-chart-v<v> → push-gateway-helm-chart.yml
|
||||
# any internal PR that bumps deploy/charts/buzz/Chart.yaml `version`
|
||||
# → tag chart-v<v> → helm-chart.yml (helm chart)
|
||||
#
|
||||
# Mobile candidate tags do not come from merged PRs. Operators create immutable
|
||||
# mobile-v<v>-rc.N tags directly from remote main with scripts/mobile-release.sh,
|
||||
# then hand the exact tag to buzz-releases.
|
||||
#
|
||||
# Release tags are created with a short-lived token from the dedicated
|
||||
# buzz-release-bot GitHub App. GitHub attributes the ref creation to that
|
||||
# App, so the consumer's `on.push.tags` trigger runs normally. The workflow's
|
||||
# default GITHUB_TOKEN remains read-only and is never used to create a tag.
|
||||
#
|
||||
# Mobile is manual-only by infosec necessity: OSS `block/buzz` CI must
|
||||
# not trigger CI in the private `buzz-releases` repo. A human feeds the exact
|
||||
# mobile candidate tag to the private Buildkite pipeline, which builds and
|
||||
# ships mobile.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
auto-tag:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
checks: read
|
||||
statuses: read
|
||||
if: >
|
||||
github.event.pull_request.merged == true &&
|
||||
github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.merge_commit_sha }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Resolve release lane and version
|
||||
id: release
|
||||
env:
|
||||
BRANCH: ${{ github.event.pull_request.head.ref }}
|
||||
run: |
|
||||
# Explicit release branches retain their established behavior. For an
|
||||
# ordinary internal PR, publish only when Chart.yaml itself changed
|
||||
# and its version differs from the PR's base commit.
|
||||
case "$BRANCH" in
|
||||
version-bump/*)
|
||||
VERSION="${BRANCH#version-bump/}"
|
||||
TAG_PREFIX="desktop-v" ;;
|
||||
relay-release/*)
|
||||
VERSION="${BRANCH#relay-release/}"
|
||||
TAG_PREFIX="relay-v" ;;
|
||||
chart-release/*)
|
||||
VERSION="${BRANCH#chart-release/}"
|
||||
TAG_PREFIX="chart-v" ;;
|
||||
push-chart-release/*)
|
||||
VERSION="${BRANCH#push-chart-release/}"
|
||||
TAG_PREFIX="push-chart-v" ;;
|
||||
*)
|
||||
parent_sha="$(git rev-parse HEAD^)"
|
||||
old_version="$(git show "${parent_sha}:deploy/charts/buzz/Chart.yaml" 2>/dev/null | awk '/^version:/ {print $2}')"
|
||||
VERSION="$(awk '/^version:/ {print $2}' deploy/charts/buzz/Chart.yaml)"
|
||||
if [ -z "$old_version" ] || [ -z "$VERSION" ] || [ "$old_version" = "$VERSION" ]; then
|
||||
echo "No release branch or chart version bump — nothing to tag"
|
||||
echo "enabled=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
TAG_PREFIX="chart-v" ;;
|
||||
esac
|
||||
if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'; then
|
||||
echo "::error::Invalid release version: '$VERSION'"
|
||||
exit 1
|
||||
fi
|
||||
{
|
||||
echo "enabled=true"
|
||||
echo "tag=${TAG_PREFIX}${VERSION}"
|
||||
if [[ "$TAG_PREFIX" == desktop-v ]]; then
|
||||
echo "target_sha=${{ github.event.pull_request.head.sha }}"
|
||||
echo "desktop=true"
|
||||
else
|
||||
echo "target_sha=$GITHUB_SHA"
|
||||
echo "desktop=false"
|
||||
fi
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
echo "Tagging ${TAG_PREFIX}${VERSION}"
|
||||
|
||||
|
||||
- name: Verify immutable reviewed desktop candidate
|
||||
if: steps.release.outputs.desktop == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
VERSION: ${{ steps.release.outputs.tag }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}
|
||||
PR_BASE_REF: ${{ github.event.pull_request.base.ref }}
|
||||
PR_HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }}
|
||||
MERGED_AT: ${{ github.event.pull_request.merged_at }}
|
||||
run: |
|
||||
VERSION="${VERSION#desktop-v}"
|
||||
export VERSION
|
||||
scripts/verify-desktop-release-merge.sh
|
||||
|
||||
- name: Create release tagger token
|
||||
if: steps.release.outputs.enabled == 'true'
|
||||
id: release-tagger
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
with:
|
||||
client-id: ${{ vars.BUZZ_RELEASE_TAGGER_CLIENT_ID }}
|
||||
private-key: ${{ secrets.BUZZ_RELEASE_TAGGER_PRIVATE_KEY }}
|
||||
permission-contents: write
|
||||
|
||||
- name: Create and push tag
|
||||
if: steps.release.outputs.enabled == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.release-tagger.outputs.token }}
|
||||
TAG: ${{ steps.release.outputs.tag }}
|
||||
TARGET_SHA: ${{ steps.release.outputs.target_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Check gh's exit status, not its output. A missing ref returns a 404
|
||||
# JSON body on stdout, which must not be mistaken for an existing tag.
|
||||
if gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$TAG" --silent 2>/dev/null; then
|
||||
EXISTING_SHA="$(gh api "repos/$GITHUB_REPOSITORY/commits/$TAG" --jq .sha)"
|
||||
if [ "$EXISTING_SHA" = "$TARGET_SHA" ]; then
|
||||
echo "Tag $TAG already exists at $TARGET_SHA — skipping tag creation"
|
||||
exit 0
|
||||
else
|
||||
echo "::error::Tag $TAG already exists at $EXISTING_SHA (expected $TARGET_SHA)"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
if ! gh api --method POST "repos/$GITHUB_REPOSITORY/git/refs" \
|
||||
-f ref="refs/tags/$TAG" \
|
||||
-f sha="$TARGET_SHA" \
|
||||
--silent; then
|
||||
# Ref creation is atomic. A concurrent retry may have won the race;
|
||||
# accept that only when it created the exact immutable ref.
|
||||
EXISTING_SHA="$(gh api "repos/$GITHUB_REPOSITORY/commits/$TAG" --jq .sha)"
|
||||
if [ "$EXISTING_SHA" = "$TARGET_SHA" ]; then
|
||||
echo "Tag $TAG was concurrently created at $TARGET_SHA"
|
||||
exit 0
|
||||
fi
|
||||
echo "::error::Tag creation failed and $TAG resolves to $EXISTING_SHA (expected $TARGET_SHA)"
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,39 @@
|
||||
name: Harbor Buzz Orchestra
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "benchmarks/harbor-buzz-orchestra/**"
|
||||
- ".github/workflows/benchmark-harbor.yml"
|
||||
pull_request:
|
||||
paths:
|
||||
- "benchmarks/harbor-buzz-orchestra/**"
|
||||
- ".github/workflows/benchmark-harbor.yml"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Python tests and lint
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: pip
|
||||
- name: Test adapter
|
||||
working-directory: benchmarks/harbor-buzz-orchestra
|
||||
run: |
|
||||
python -m pip install --disable-pip-version-check -e ".[dev]"
|
||||
pytest -q
|
||||
ruff check .
|
||||
- name: Test provisioner
|
||||
working-directory: benchmarks/harbor-buzz-orchestra/testbed
|
||||
run: |
|
||||
python -m pip install --disable-pip-version-check -e ".[dev]"
|
||||
pytest -q
|
||||
ruff check .
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,164 @@
|
||||
name: Desktop release cache tag-scope proof
|
||||
|
||||
# Dispatch from a cache-proof-* tag at the same trusted-main SHA warmed by all
|
||||
# four canaries. Every job restores only and requires an exact cache hit.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
macos:
|
||||
name: Prove macOS ${{ matrix.target }} cache visibility
|
||||
if: github.repository == 'block/buzz'
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 15
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- target: aarch64-apple-darwin
|
||||
features: mesh-llm
|
||||
- target: x86_64-apple-darwin
|
||||
features: default
|
||||
steps:
|
||||
- name: Require cache proof tag
|
||||
run: '[[ "$GITHUB_REF" == refs/tags/cache-proof-* ]] || { echo "::error::Expected cache-proof-* tag; got $GITHUB_REF"; exit 1; }'
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
|
||||
- name: Patch proof dependency graph
|
||||
run: |
|
||||
cd desktop && node scripts/set-version-from-tag.mjs "0.0.0-cache-proof"
|
||||
cd src-tauri && cargo update --workspace
|
||||
- name: Resolve native toolchain identity
|
||||
id: native_toolchain
|
||||
run: echo "id=$(scripts/desktop-native-toolchain-id.sh macos)" >> "$GITHUB_OUTPUT"
|
||||
- name: Compute exact release cache key
|
||||
id: rust_cache_key
|
||||
env:
|
||||
CACHE_TARGET: ${{ matrix.target }}
|
||||
CACHE_FEATURES: ${{ matrix.features }}
|
||||
NATIVE_TOOLCHAIN_ID: ${{ steps.native_toolchain.outputs.id }}
|
||||
run: |
|
||||
KEY=$(scripts/desktop-release-cache-key.py --platform "$RUNNER_OS" --target "$CACHE_TARGET" --features "$CACHE_FEATURES" --native-inputs "$NATIVE_TOOLCHAIN_ID")
|
||||
echo "key=$KEY" >> "$GITHUB_OUTPUT"
|
||||
- name: Restore exact default-branch cache from tag
|
||||
id: rust_cache
|
||||
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
desktop/src-tauri/target
|
||||
!desktop/src-tauri/target/**/release/bundle
|
||||
key: ${{ steps.rust_cache_key.outputs.key }}
|
||||
- name: Require exact cache hit
|
||||
env:
|
||||
CACHE_HIT: ${{ steps.rust_cache.outputs.cache-hit }}
|
||||
CACHE_KEY: ${{ steps.rust_cache.outputs.cache-primary-key }}
|
||||
EXPECTED_KEY: ${{ steps.rust_cache_key.outputs.key }}
|
||||
run: '[[ "$CACHE_HIT" == true && "$CACHE_KEY" == "$EXPECTED_KEY" ]] || { echo "::error::Exact tag cache miss (hit=$CACHE_HIT restored=$CACHE_KEY expected=$EXPECTED_KEY)"; exit 1; }'
|
||||
|
||||
linux:
|
||||
name: Prove Linux cache visibility
|
||||
if: github.repository == 'block/buzz'
|
||||
runs-on: ubuntu-latest
|
||||
container: ubuntu:24.04@sha256:4fbb8e6a8395de5a7550b33509421a2bafbc0aab6c06ba2cef9ebffbc7092d90
|
||||
timeout-minutes: 15
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Require cache proof tag and install release native tools
|
||||
run: |
|
||||
[[ "$GITHUB_REF" == refs/tags/cache-proof-* ]] || { echo "::error::Expected cache-proof-* tag; got $GITHUB_REF"; exit 1; }
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends build-essential ca-certificates curl git libasound2-dev libayatana-appindicator3-dev libgtk-3-dev librsvg2-dev libssl-dev libwebkit2gtk-4.1-dev libxdo-dev patchelf pkg-config
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
|
||||
- name: Patch proof dependency graph
|
||||
run: |
|
||||
cd desktop && node scripts/set-version-from-tag.mjs "0.0.0-cache-proof"
|
||||
cd src-tauri && cargo update --workspace
|
||||
- name: Resolve native toolchain identity
|
||||
id: native_toolchain
|
||||
run: echo "id=$(scripts/desktop-native-toolchain-id.sh linux)" >> "$GITHUB_OUTPUT"
|
||||
- name: Compute exact release cache key
|
||||
id: rust_cache_key
|
||||
env:
|
||||
NATIVE_TOOLCHAIN_ID: ${{ steps.native_toolchain.outputs.id }}
|
||||
run: |
|
||||
KEY=$(scripts/desktop-release-cache-key.py --platform "$RUNNER_OS" --target x86_64-unknown-linux-gnu --features mesh-llm --native-inputs "$NATIVE_TOOLCHAIN_ID")
|
||||
echo "key=$KEY" >> "$GITHUB_OUTPUT"
|
||||
- name: Restore exact default-branch cache from tag
|
||||
id: rust_cache
|
||||
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
desktop/src-tauri/target
|
||||
!desktop/src-tauri/target/**/release/bundle
|
||||
key: ${{ steps.rust_cache_key.outputs.key }}
|
||||
- name: Require exact cache hit
|
||||
env:
|
||||
CACHE_HIT: ${{ steps.rust_cache.outputs.cache-hit }}
|
||||
CACHE_KEY: ${{ steps.rust_cache.outputs.cache-primary-key }}
|
||||
EXPECTED_KEY: ${{ steps.rust_cache_key.outputs.key }}
|
||||
run: '[[ "$CACHE_HIT" == true && "$CACHE_KEY" == "$EXPECTED_KEY" ]] || { echo "::error::Exact tag cache miss (hit=$CACHE_HIT restored=$CACHE_KEY expected=$EXPECTED_KEY)"; exit 1; }'
|
||||
|
||||
windows:
|
||||
name: Prove Windows cache visibility
|
||||
if: github.repository == 'block/buzz'
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Require cache proof tag
|
||||
shell: bash
|
||||
run: '[[ "$GITHUB_REF" == refs/tags/cache-proof-* ]] || { echo "::error::Expected cache-proof-* tag; got $GITHUB_REF"; exit 1; }'
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Patch proof dependency graph
|
||||
shell: bash
|
||||
run: |
|
||||
cd desktop && node scripts/set-version-from-tag.mjs "0.0.0-cache-proof"
|
||||
cd src-tauri && cargo update --workspace
|
||||
- name: Resolve native toolchain identity
|
||||
id: native_toolchain
|
||||
shell: bash
|
||||
run: echo "id=$(scripts/desktop-native-toolchain-id.sh windows)" >> "$GITHUB_OUTPUT"
|
||||
- name: Compute exact release cache key
|
||||
id: rust_cache_key
|
||||
shell: bash
|
||||
env:
|
||||
NATIVE_TOOLCHAIN_ID: ${{ steps.native_toolchain.outputs.id }}
|
||||
run: |
|
||||
KEY=$(scripts/desktop-release-cache-key.py --platform "$RUNNER_OS" --target x86_64-pc-windows-msvc --features default --native-inputs "$NATIVE_TOOLCHAIN_ID")
|
||||
echo "key=$KEY" >> "$GITHUB_OUTPUT"
|
||||
- name: Restore exact default-branch cache from tag
|
||||
id: rust_cache
|
||||
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
desktop/src-tauri/target
|
||||
!desktop/src-tauri/target/**/release/bundle
|
||||
key: ${{ steps.rust_cache_key.outputs.key }}
|
||||
- name: Require exact cache hit
|
||||
shell: bash
|
||||
env:
|
||||
CACHE_HIT: ${{ steps.rust_cache.outputs.cache-hit }}
|
||||
CACHE_KEY: ${{ steps.rust_cache.outputs.cache-primary-key }}
|
||||
EXPECTED_KEY: ${{ steps.rust_cache_key.outputs.key }}
|
||||
run: '[[ "$CACHE_HIT" == true && "$CACHE_KEY" == "$EXPECTED_KEY" ]] || { echo "::error::Exact tag cache miss (hit=$CACHE_HIT restored=$CACHE_KEY expected=$EXPECTED_KEY)"; exit 1; }'
|
||||
@@ -0,0 +1,28 @@
|
||||
name: Desktop Release Candidate
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
name: Desktop Release Candidate
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Validate immutable desktop candidate
|
||||
if: startsWith(github.event.pull_request.head.ref, 'version-bump/')
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
VERSION: ${{ github.event.pull_request.head.ref }}
|
||||
run: |
|
||||
VERSION="${VERSION#version-bump/}"
|
||||
scripts/desktop_release.py validate --candidate HEAD --version "$VERSION" --repo "$GITHUB_REPOSITORY"
|
||||
@@ -0,0 +1,495 @@
|
||||
name: Docker image
|
||||
|
||||
# Builds and publishes the public Buzz relay images as ghcr.io/block/buzz.
|
||||
# Normal tags contain stripped binaries; matching debug-* tags contain the same
|
||||
# optimized binaries with line-table debug information for native profilers.
|
||||
#
|
||||
# Strategy: each architecture builds on its native runner (ubuntu-24.04 for
|
||||
# amd64, ubuntu-24.04-arm for arm64), pushes to GHCR by digest, then a final
|
||||
# job stitches the per-arch digests into a single multi-arch manifest.
|
||||
# This avoids QEMU emulation (~10× slower for Rust) at zero cost on free
|
||||
# GitHub-hosted runners.
|
||||
#
|
||||
# Versioning: the relay is versioned independently of the desktop app via
|
||||
# its own `relay-v*` tags (see `just release-relay`). Desktop `v*` tags and
|
||||
# agent `sprig-v*` tags do NOT publish this image — only `relay-v*` does, so
|
||||
# the relay image version tracks crates/buzz-relay/Cargo.toml, never desktop.
|
||||
#
|
||||
# Triggers:
|
||||
# - push to main → :main + :sha-<7>
|
||||
# + :debug-main + :debug-sha-<7>
|
||||
# - push tags relay-v*.*.* → :{version} + :{major}.{minor} + :{major}
|
||||
# + matching :debug-* tags
|
||||
# (+ :latest/:debug-latest for stable releases)
|
||||
# - pull_request → build only (no push), cache stays warm
|
||||
# - workflow_dispatch → manual relay-tag rescue at the tag itself
|
||||
#
|
||||
# Why workflow_dispatch carries a version input:
|
||||
# Normal releases arrive through the push:tags trigger above. The input is
|
||||
# retained only for an operator to rerun publication manually at an immutable
|
||||
# relay tag. The workflow rejects a dispatch whose github.ref, checked-out
|
||||
# HEAD, and relay-v tag do not resolve to one commit.
|
||||
# On the rescue path inputs.version is already bare (e.g. 0.3.0), so the
|
||||
# match=^relay-v(.*)$ regex simply no-ops (it warns, leaving the value
|
||||
# intact) and the bare version flows straight to the semver parser. On a
|
||||
# real push event value= is empty and the match strips relay-v from the ref.
|
||||
#
|
||||
# The :latest tag tracks the latest STABLE relay release: metadata-action's
|
||||
# `flavor.latest=auto` (its default) emits :latest only for non-prerelease
|
||||
# semver, so relay-v0.3.0-rc.1 publishes :0.3.0-rc.1 without moving :latest,
|
||||
# and main pushes (no semver tag) never produce :latest.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ["relay-v[0-9]*"]
|
||||
pull_request:
|
||||
paths:
|
||||
- "Dockerfile"
|
||||
- "Dockerfile.push-gateway"
|
||||
- ".dockerignore"
|
||||
- ".github/workflows/docker.yml"
|
||||
- "Cargo.toml"
|
||||
- "Cargo.lock"
|
||||
- "rust-toolchain.toml"
|
||||
- "crates/**"
|
||||
- "web/**"
|
||||
- "package.json"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
- "patches/**"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Semver version e.g. 0.3.0 (no relay-v prefix) — for relay-tag rescue dispatch"
|
||||
required: true
|
||||
|
||||
# One image build per ref; cancel superseded PR builds, but never cancel
|
||||
# tag/main builds (publishing must not be aborted mid-flight).
|
||||
concurrency:
|
||||
group: docker-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref_type == 'branch' && github.event_name == 'pull_request' }}
|
||||
|
||||
permissions: {}
|
||||
|
||||
env:
|
||||
# Single source of truth for the image name. Set GHCR_IMAGE as a repo
|
||||
# variable to override (e.g., for forks that want to push to their own
|
||||
# namespace without forking this file).
|
||||
IMAGE_NAME: ${{ vars.GHCR_IMAGE != '' && vars.GHCR_IMAGE || 'ghcr.io/block/buzz' }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build (${{ matrix.platform }})
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write # push to GHCR
|
||||
id-token: write # OIDC for build provenance attestation
|
||||
attestations: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-24.04
|
||||
arch: amd64
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
arch: arm64
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Verify tag-bound release source
|
||||
if: github.ref_type == 'tag' || github.event_name == 'workflow_dispatch'
|
||||
env:
|
||||
INPUT_VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
VERSION="${INPUT_VERSION:-${GITHUB_REF_NAME#relay-v}}"
|
||||
scripts/verify-release-ref.sh relay-v "$VERSION"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||
with:
|
||||
# Default parallelism of 4 OOMs the 7GB GitHub runner during Rust
|
||||
# compiles (see moby/buildkit#3969). Vaultwarden hit this; we will
|
||||
# too without the cap.
|
||||
buildkitd-config-inline: |
|
||||
[worker.oci]
|
||||
max-parallelism = 2
|
||||
|
||||
- name: Log in to GHCR
|
||||
# Skip on pull_request from forks — no GHCR creds, build-only.
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||
with:
|
||||
images: ${{ env.IMAGE_NAME }}
|
||||
# Tag matrix — every main commit gets sha-<7>, relay releases get the
|
||||
# full semver family. The semver entries carry match=^relay-v(.*)$
|
||||
# because metadata-action does NOT strip a `relay-v` prefix on its
|
||||
# own — it only strips refs/tags/, then runs the raw ref through
|
||||
# semver.valid(), which rejects "relay-v0.3.0". The match capture
|
||||
# group feeds the bare version to the semver parser. value= supplies
|
||||
# the version on a manual rescue dispatch (github.ref is `main`
|
||||
# there, not the tag): it is already bare, so match no-ops (warns,
|
||||
# value intact) and the bare version validates as-is. On push value=
|
||||
# is empty, so the ref drives it and match strips relay-v — push
|
||||
# behavior is unchanged. Pull requests get nothing (push: false
|
||||
# below). :latest is intentionally absent — flavor.latest defaults to
|
||||
# `auto`, which adds :latest for stable semver tags only (not
|
||||
# prereleases, not main pushes).
|
||||
tags: |
|
||||
type=ref,event=branch,enable=${{ github.event_name != 'workflow_dispatch' || inputs.version == '' }}
|
||||
type=sha,prefix=sha-,format=short,enable=${{ github.event_name != 'workflow_dispatch' || inputs.version == '' }}
|
||||
type=semver,pattern={{version}},match=^relay-v(.*)$,value=${{ inputs.version }}
|
||||
type=semver,pattern={{major}}.{{minor}},match=^relay-v(.*)$,value=${{ inputs.version }}
|
||||
type=semver,pattern={{major}},match=^relay-v(.*)$,value=${{ inputs.version }}
|
||||
labels: |
|
||||
org.opencontainers.image.title=Buzz
|
||||
org.opencontainers.image.description=WebSocket relay server for the Buzz communications platform
|
||||
org.opencontainers.image.licenses=Apache-2.0
|
||||
|
||||
- name: Build and push release image by digest
|
||||
id: build-release
|
||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
target: runtime
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
# Push by digest, not by tag — the merge job assembles the tags
|
||||
# into one multi-arch manifest. This is what makes the native-arm
|
||||
# matrix possible.
|
||||
outputs: type=image,name=${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }}
|
||||
cache-from: |
|
||||
type=registry,ref=${{ env.IMAGE_NAME }}-buildcache:${{ matrix.arch }}
|
||||
cache-to: |
|
||||
${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && format('type=registry,ref={0}-buildcache:{1},mode=max,compression=zstd', env.IMAGE_NAME, matrix.arch) || '' }}
|
||||
|
||||
- name: Build and push debug image by digest
|
||||
id: build-debug
|
||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
target: runtime-debug
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }}
|
||||
cache-from: |
|
||||
type=registry,ref=${{ env.IMAGE_NAME }}-buildcache:${{ matrix.arch }}
|
||||
|
||||
- name: Export release and debug digests
|
||||
if: github.event_name != 'pull_request'
|
||||
env:
|
||||
RELEASE_DIGEST: ${{ steps.build-release.outputs.digest }}
|
||||
DEBUG_DIGEST: ${{ steps.build-debug.outputs.digest }}
|
||||
run: |
|
||||
mkdir -p /tmp/digests-release /tmp/digests-debug
|
||||
touch "/tmp/digests-release/${RELEASE_DIGEST#sha256:}"
|
||||
touch "/tmp/digests-debug/${DEBUG_DIGEST#sha256:}"
|
||||
|
||||
- name: Upload release digest
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: digests-release-${{ matrix.arch }}
|
||||
path: /tmp/digests-release/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload debug digest
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: digests-debug-${{ matrix.arch }}
|
||||
path: /tmp/digests-debug/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge:
|
||||
name: Merge ${{ matrix.variant }} multi-arch manifest
|
||||
if: github.event_name != 'pull_request'
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write # push the merged manifest
|
||||
id-token: write # OIDC for provenance attestation on the manifest
|
||||
attestations: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- variant: release
|
||||
tag_prefix: ""
|
||||
- variant: debug
|
||||
tag_prefix: debug-
|
||||
|
||||
steps:
|
||||
- name: Download all per-arch digests
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-${{ matrix.variant }}-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||
with:
|
||||
images: ${{ env.IMAGE_NAME }}
|
||||
# Must mirror the build job's tag matrix exactly — the merge job
|
||||
# re-derives tags to stamp them onto the multi-arch manifest. See
|
||||
# the build job's `meta` step for why match=^relay-v(.*)$, why
|
||||
# value=${{ inputs.version }} carries the rescue-dispatch version,
|
||||
# and why :latest is left to flavor.latest=auto.
|
||||
flavor: |
|
||||
latest=auto
|
||||
prefix=${{ matrix.tag_prefix }},onlatest=true
|
||||
tags: |
|
||||
type=ref,event=branch,enable=${{ github.event_name != 'workflow_dispatch' || inputs.version == '' }}
|
||||
type=sha,prefix=${{ matrix.tag_prefix }}sha-,format=short,enable=${{ github.event_name != 'workflow_dispatch' || inputs.version == '' }}
|
||||
type=semver,pattern={{version}},match=^relay-v(.*)$,value=${{ inputs.version }}
|
||||
type=semver,pattern={{major}}.{{minor}},match=^relay-v(.*)$,value=${{ inputs.version }}
|
||||
type=semver,pattern={{major}},match=^relay-v(.*)$,value=${{ inputs.version }}
|
||||
|
||||
- name: Create and push manifest list
|
||||
id: manifest
|
||||
working-directory: /tmp/digests
|
||||
env:
|
||||
IMAGE_NAME: ${{ env.IMAGE_NAME }}
|
||||
META_TAGS: ${{ steps.meta.outputs.tags }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Build -t flags from the metadata-action output.
|
||||
tags=()
|
||||
while IFS= read -r tag; do
|
||||
[ -n "$tag" ] && tags+=("-t" "$tag")
|
||||
done <<< "$META_TAGS"
|
||||
|
||||
# Build the digest refs from the per-arch artifacts.
|
||||
digests=()
|
||||
for digest in *; do
|
||||
digests+=("${IMAGE_NAME}@sha256:${digest}")
|
||||
done
|
||||
|
||||
docker buildx imagetools create "${tags[@]}" "${digests[@]}"
|
||||
|
||||
# Capture the merged manifest digest for the attestation step.
|
||||
first_tag=$(echo "$META_TAGS" | head -n1)
|
||||
merged_digest=$(docker buildx imagetools inspect "$first_tag" \
|
||||
--format '{{json .Manifest}}' | jq -r '.digest')
|
||||
echo "digest=${merged_digest}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Attest provenance for the merged image
|
||||
# Sigstore-signed in-toto attestation, verifiable with:
|
||||
# gh attestation verify oci://ghcr.io/block/buzz:<tag> --owner block
|
||||
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
|
||||
with:
|
||||
subject-name: ${{ env.IMAGE_NAME }}
|
||||
subject-digest: ${{ steps.manifest.outputs.digest }}
|
||||
push-to-registry: true
|
||||
|
||||
- name: Summary
|
||||
env:
|
||||
IMAGE_NAME: ${{ env.IMAGE_NAME }}
|
||||
VARIANT: ${{ matrix.variant }}
|
||||
MERGED_DIGEST: ${{ steps.manifest.outputs.digest }}
|
||||
META_TAGS: ${{ steps.meta.outputs.tags }}
|
||||
run: |
|
||||
{
|
||||
echo "### Published \`${IMAGE_NAME}\` (${VARIANT})"
|
||||
echo
|
||||
echo "**Digest:** \`${MERGED_DIGEST}\`"
|
||||
echo
|
||||
echo "**Tags:**"
|
||||
echo '```'
|
||||
echo "${META_TAGS}"
|
||||
echo '```'
|
||||
echo
|
||||
echo "Verify provenance:"
|
||||
echo '```'
|
||||
echo "gh attestation verify oci://${IMAGE_NAME}@${MERGED_DIGEST} --owner block"
|
||||
echo '```'
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
push-gateway-build:
|
||||
name: Build public push gateway (${{ matrix.platform }})
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-24.04
|
||||
arch: amd64
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
arch: arm64
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Verify tag-bound release source
|
||||
if: github.ref_type == 'tag' || github.event_name == 'workflow_dispatch'
|
||||
env:
|
||||
INPUT_VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
VERSION="${INPUT_VERSION:-${GITHUB_REF_NAME#relay-v}}"
|
||||
scripts/verify-release-ref.sh relay-v "$VERSION"
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||
with:
|
||||
buildkitd-config-inline: |
|
||||
[worker.oci]
|
||||
max-parallelism = 2
|
||||
- name: Log in to GHCR
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||
with:
|
||||
images: ghcr.io/block/buzz-push-gateway
|
||||
labels: |
|
||||
org.opencontainers.image.title=Buzz Push Gateway
|
||||
org.opencontainers.image.description=Capability-gated APNs last hop for Buzz
|
||||
org.opencontainers.image.licenses=Apache-2.0
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.push-gateway
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=ghcr.io/block/buzz-push-gateway,push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }}
|
||||
cache-from: type=registry,ref=ghcr.io/block/buzz-push-gateway-buildcache:${{ matrix.arch }}
|
||||
cache-to: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && format('type=registry,ref=ghcr.io/block/buzz-push-gateway-buildcache:{0},mode=max,compression=zstd', matrix.arch) || '' }}
|
||||
- name: Export digest
|
||||
if: github.event_name != 'pull_request'
|
||||
env:
|
||||
DIGEST: ${{ steps.build.outputs.digest }}
|
||||
run: mkdir -p /tmp/gateway-digests && touch "/tmp/gateway-digests/${DIGEST#sha256:}"
|
||||
- name: Upload digest
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: gateway-digests-${{ matrix.arch }}
|
||||
path: /tmp/gateway-digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
push-gateway-merge:
|
||||
name: Publish public push gateway image
|
||||
if: github.event_name != 'pull_request'
|
||||
runs-on: ubuntu-24.04
|
||||
needs: push-gateway-build
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
steps:
|
||||
- name: Download per-arch digests
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
path: /tmp/gateway-digests
|
||||
pattern: gateway-digests-*
|
||||
merge-multiple: true
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||
with:
|
||||
images: ghcr.io/block/buzz-push-gateway
|
||||
tags: |
|
||||
type=ref,event=branch,enable=${{ github.event_name != 'workflow_dispatch' || inputs.version == '' }}
|
||||
type=sha,prefix=sha-,format=short,enable=${{ github.event_name != 'workflow_dispatch' || inputs.version == '' }}
|
||||
type=semver,pattern={{version}},match=^relay-v(.*)$,value=${{ inputs.version }}
|
||||
type=semver,pattern={{major}}.{{minor}},match=^relay-v(.*)$,value=${{ inputs.version }}
|
||||
- name: Merge and publish manifest
|
||||
id: manifest
|
||||
working-directory: /tmp/gateway-digests
|
||||
env:
|
||||
META_TAGS: ${{ steps.meta.outputs.tags }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
tags=(); while IFS= read -r tag; do [ -n "$tag" ] && tags+=("-t" "$tag"); done <<< "$META_TAGS"
|
||||
digests=(); for digest in *; do digests+=("ghcr.io/block/buzz-push-gateway@sha256:${digest}"); done
|
||||
docker buildx imagetools create "${tags[@]}" "${digests[@]}"
|
||||
first_tag=$(echo "$META_TAGS" | head -n1)
|
||||
digest=$(docker buildx imagetools inspect "$first_tag" --format '{{json .Manifest}}' | jq -r '.digest')
|
||||
echo "digest=${digest}" >> "$GITHUB_OUTPUT"
|
||||
- name: Attest gateway image provenance
|
||||
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
|
||||
with:
|
||||
subject-name: ghcr.io/block/buzz-push-gateway
|
||||
subject-digest: ${{ steps.manifest.outputs.digest }}
|
||||
push-to-registry: true
|
||||
- name: Gateway publication summary
|
||||
env:
|
||||
GATEWAY_DIGEST: ${{ steps.manifest.outputs.digest }}
|
||||
GATEWAY_TAGS: ${{ steps.meta.outputs.tags }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
{
|
||||
echo "### Published \`ghcr.io/block/buzz-push-gateway\`"
|
||||
echo
|
||||
printf "**Digest:** \`%s\`\n" "$GATEWAY_DIGEST"
|
||||
echo
|
||||
echo '**Tags:**'
|
||||
echo "\`\`\`"
|
||||
printf '%s\n' "$GATEWAY_TAGS"
|
||||
echo "\`\`\`"
|
||||
echo
|
||||
echo 'Verify provenance before deployment:'
|
||||
echo "\`\`\`"
|
||||
printf 'gh attestation verify oci://ghcr.io/block/buzz-push-gateway@%s --owner block\n' "$GATEWAY_DIGEST"
|
||||
echo "\`\`\`"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -0,0 +1,256 @@
|
||||
name: helm chart
|
||||
|
||||
# Lints + unit-tests + render-checks the chart on every PR/main push, and
|
||||
# PUBLISHES it to GHCR as an OCI artifact on `chart-v*` tags.
|
||||
#
|
||||
# Publishing mirrors the relay image (see docker.yml): the chart is versioned
|
||||
# independently of the desktop app and the relay via its own `chart-v*` tags
|
||||
# (Chart.yaml `version`, cut by merging a `chart-release/<version>` PR). Only
|
||||
# `chart-v*` tags publish — `main` pushes and PRs stay lint/render-only so we
|
||||
# never overwrite a released chart version from an in-progress `main`.
|
||||
#
|
||||
# Why workflow_dispatch carries version/ref inputs:
|
||||
# Normal releases arrive through the `push.tags` trigger below. The inputs are
|
||||
# retained only for an operator to rerun publication manually at an immutable
|
||||
# chart tag; the publish job checks out inputs.ref and packages at
|
||||
# inputs.version.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Chart semver e.g. 0.1.0 (no chart-v prefix) — for chart-tag rescue dispatch"
|
||||
required: false
|
||||
ref:
|
||||
description: "Chart tag ref to publish, e.g. chart-v0.1.0 (required when version is set)"
|
||||
required: false
|
||||
default: main
|
||||
push:
|
||||
# No `paths` filter here: GitHub applies a push `paths` filter to tag
|
||||
# pushes too, so a `chart-v*` tag whose commit didn't touch a chart file
|
||||
# would be filtered out and never publish. docker.yml / release.yml / sprig
|
||||
# all keep `paths` out of a tag-carrying `push` for exactly this reason —
|
||||
# PR runs stay scoped via `pull_request.paths` below; main pushes lint
|
||||
# unconditionally (cheap), and tag pushes always run so publish can fire.
|
||||
branches: [main]
|
||||
tags: ["chart-v[0-9]*"]
|
||||
pull_request:
|
||||
paths:
|
||||
- "deploy/charts/buzz/**"
|
||||
- "deploy/charts/buzz-push-gateway/**"
|
||||
- ".github/workflows/helm-chart.yml"
|
||||
- "ct.yaml"
|
||||
|
||||
# Match docker.yml: deny-by-default, each job grants only what it needs.
|
||||
permissions: {}
|
||||
|
||||
env:
|
||||
# Single source of truth for the OCI chart repository (helm appends the chart
|
||||
# name `buzz`, yielding oci://ghcr.io/block/buzz/charts/buzz, which is exactly
|
||||
# the install ref documented in deploy/charts/buzz/README.md). Set
|
||||
# GHCR_CHART_REPO as a repo variable to override (e.g., forks pushing to their
|
||||
# own namespace without editing this file) — mirrors docker.yml's GHCR_IMAGE.
|
||||
CHART_REPO: ${{ vars.GHCR_CHART_REPO != '' && vars.GHCR_CHART_REPO || 'oci://ghcr.io/block/buzz/charts' }}
|
||||
|
||||
jobs:
|
||||
lint-and-unittest:
|
||||
name: lint + unittest + render matrix
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
# On chart-tag rescue dispatch, lint/render the tagged commit that the
|
||||
# publish job will package, not whatever `main` is when the dispatch
|
||||
# runs. Empty string = default ref for push/PR events.
|
||||
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || '' }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1
|
||||
with:
|
||||
version: v3.16.4
|
||||
|
||||
- name: Install helm-unittest plugin
|
||||
run: helm plugin install --version 0.8.2 https://github.com/helm-unittest/helm-unittest
|
||||
|
||||
- name: Set up Python (for chart-testing)
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Set up chart-testing
|
||||
uses: helm/chart-testing-action@0d28d3144d3a25ea2cc349d6e59901c4ff469b3b # v2.7.0
|
||||
|
||||
- name: Build chart dependencies
|
||||
run: helm dependency build deploy/charts/buzz
|
||||
|
||||
- name: Push gateway chart lint + render guard
|
||||
run: deploy/charts/buzz-push-gateway/tests/render.sh
|
||||
|
||||
- name: ct lint
|
||||
run: ct lint --config ct.yaml --all
|
||||
|
||||
- name: helm-unittest
|
||||
run: helm unittest deploy/charts/buzz
|
||||
|
||||
- name: helm template (render every fixture)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for f in deploy/charts/buzz/ci/*-values.yaml deploy/charts/buzz/tests/fixtures/*-values.yaml; do
|
||||
echo "::group::render $f"
|
||||
helm template buzz deploy/charts/buzz -f "$f"
|
||||
echo "::endgroup::"
|
||||
done
|
||||
|
||||
install-on-kind:
|
||||
# Full end-to-end install requires the public ghcr.io/block/buzz image to
|
||||
# exist and to embed Max's startup migrations. Runs only after Sami's
|
||||
# image PR merges (`workflow_dispatch`) or on a schedule once main carries
|
||||
# both prerequisites. Render/lint above is the per-PR signal.
|
||||
#
|
||||
# `inputs.version == ''` excludes the chart-tag rescue dispatch (which
|
||||
# carries a version): that path only packages+publishes, it does not install.
|
||||
name: install on kind (gated)
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.version == ''
|
||||
runs-on: ubuntu-latest
|
||||
needs: lint-and-unittest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1
|
||||
with:
|
||||
version: v3.16.4
|
||||
|
||||
- name: Set up Python (for chart-testing)
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Set up chart-testing
|
||||
uses: helm/chart-testing-action@0d28d3144d3a25ea2cc349d6e59901c4ff469b3b # v2.7.0
|
||||
|
||||
- name: Create kind cluster
|
||||
uses: helm/kind-action@0025e74a8c7512023d06dc019c617aa3cf561fde # v1.10.0
|
||||
with:
|
||||
version: v0.24.0
|
||||
node_image: kindest/node:v1.31.0
|
||||
|
||||
- name: Build chart dependencies
|
||||
run: helm dependency build deploy/charts/buzz
|
||||
|
||||
- name: ct install (quickstart profile)
|
||||
run: ct install --config ct.yaml --charts deploy/charts/buzz --helm-extra-args "--timeout 600s"
|
||||
|
||||
publish:
|
||||
# Packages the chart and pushes it to GHCR as an OCI artifact. Fires only on
|
||||
# a `chart-v*` tag push or a manual rescue dispatch (which
|
||||
# carries inputs.version) — never on `main` pushes or PRs, so an in-progress
|
||||
# `main` can never overwrite a released chart version. Mirrors docker.yml's
|
||||
# GHCR publish (login with GITHUB_TOKEN, packages: write, fork override var).
|
||||
name: publish chart to GHCR
|
||||
if: >
|
||||
startsWith(github.ref, 'refs/tags/chart-v') ||
|
||||
(github.event_name == 'workflow_dispatch' && inputs.version != '')
|
||||
runs-on: ubuntu-latest
|
||||
needs: lint-and-unittest
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write # push the chart to GHCR
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
# On the rescue dispatch, build the tagged commit (github.ref is
|
||||
# `main` there); on a tag push, the default ref is already the tag.
|
||||
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || '' }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1
|
||||
with:
|
||||
version: v3.16.4
|
||||
|
||||
- name: Resolve chart version
|
||||
id: ver
|
||||
env:
|
||||
# Bare on the rescue dispatch; empty on a tag push (derive from ref).
|
||||
INPUT_VERSION: ${{ inputs.version }}
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -n "$INPUT_VERSION" ]; then
|
||||
version="$INPUT_VERSION"
|
||||
tag_ref="refs/tags/chart-v${version}"
|
||||
tag_sha="$(git rev-parse -q --verify "${tag_ref}^{commit}" || true)"
|
||||
head_sha="$(git rev-parse HEAD)"
|
||||
if [ -z "$tag_sha" ] || [ "$head_sha" != "$tag_sha" ]; then
|
||||
echo "::error::workflow_dispatch with version '$version' must check out matching tag 'chart-v${version}' (HEAD=$head_sha, tag=${tag_sha:-missing})"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
# refs/tags/chart-v0.1.0 → github.ref_name is `chart-v0.1.0`.
|
||||
version="${REF_NAME#chart-v}"
|
||||
fi
|
||||
if ! echo "$version" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'; then
|
||||
echo "::error::Resolved chart version '$version' is not valid semver"
|
||||
exit 1
|
||||
fi
|
||||
# The tag is the source of truth, but the published artifact's version
|
||||
# comes from Chart.yaml — they must agree or we'd publish a mislabeled
|
||||
# chart. Fail loudly on drift rather than silently shipping a mismatch.
|
||||
chart_version="$(helm show chart deploy/charts/buzz | awk '/^version:/ {print $2}')"
|
||||
if [ "$chart_version" != "$version" ]; then
|
||||
echo "::error::Tag version '$version' != Chart.yaml version '$chart_version'. Bump Chart.yaml to match the tag."
|
||||
exit 1
|
||||
fi
|
||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||
echo "Publishing chart version $version"
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build chart dependencies
|
||||
run: helm dependency build deploy/charts/buzz
|
||||
|
||||
- name: Package chart
|
||||
run: helm package deploy/charts/buzz --destination dist
|
||||
|
||||
- name: Push chart to GHCR
|
||||
env:
|
||||
CHART_REPO: ${{ env.CHART_REPO }}
|
||||
VERSION: ${{ steps.ver.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
helm push "dist/buzz-${VERSION}.tgz" "$CHART_REPO"
|
||||
|
||||
- name: Summary
|
||||
env:
|
||||
CHART_REPO: ${{ env.CHART_REPO }}
|
||||
VERSION: ${{ steps.ver.outputs.version }}
|
||||
run: |
|
||||
# CHART_REPO is oci://ghcr.io/block/buzz/charts; helm push appends the
|
||||
# chart name, so the install ref is .../charts/buzz.
|
||||
INSTALL_REF="${CHART_REPO}/buzz"
|
||||
{
|
||||
echo "### Published chart \`buzz\` \`${VERSION}\`"
|
||||
echo
|
||||
echo "**OCI ref:** \`${INSTALL_REF}:${VERSION}\`"
|
||||
echo
|
||||
echo "Install:"
|
||||
echo '```'
|
||||
echo "helm install buzz ${INSTALL_REF} --version ${VERSION}"
|
||||
echo '```'
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -0,0 +1,265 @@
|
||||
name: Linux Canary
|
||||
|
||||
# Produces unsigned Linux .deb and .AppImage packages from main without
|
||||
# creating a tag, GitHub Release, or auto-updater artifact. Artifacts are
|
||||
# available as a short-lived GitHub Actions artifact for explicit testing.
|
||||
#
|
||||
# Design notes vs. signed-macos-canary.yml:
|
||||
# - fix-appimage.sh is run without signing env vars; the script detects
|
||||
# their absence and skips re-signing, repacking only (documented inline).
|
||||
# - Build tools match release.yml; cache keys derive the concrete linker and
|
||||
# native library identity rather than assuming the moving runner image.
|
||||
# - pnpm store restore/save pattern mirrors ci.yml:149-196.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Linux canary
|
||||
if: github.repository == 'block/buzz'
|
||||
runs-on: ubuntu-latest
|
||||
container: ubuntu:24.04@sha256:4fbb8e6a8395de5a7550b33509421a2bafbc0aab6c06ba2cef9ebffbc7092d90
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: read
|
||||
env:
|
||||
# AppImage tools are themselves AppImages; containers lack FUSE so we
|
||||
# must use the extract-and-run fallback.
|
||||
APPIMAGE_EXTRACT_AND_RUN: "1"
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Require main
|
||||
env:
|
||||
SOURCE_REF: ${{ github.ref }}
|
||||
run: |
|
||||
if [[ "$SOURCE_REF" != "refs/heads/main" ]]; then
|
||||
echo "::error::Canary builds must run from main; got $SOURCE_REF"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Install system dependencies
|
||||
env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
run: |
|
||||
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
|
||||
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Mark workspace safe for git (containerized job)
|
||||
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
|
||||
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
|
||||
|
||||
- name: Install appimagetool
|
||||
run: |
|
||||
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"
|
||||
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
|
||||
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: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: echo "STORE_PATH=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Restore pnpm store cache
|
||||
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: pnpm-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: pnpm-${{ runner.os }}-
|
||||
|
||||
- name: Install desktop dependencies
|
||||
run: just desktop-install-ci
|
||||
|
||||
- name: Derive canary version
|
||||
id: version
|
||||
run: |
|
||||
set -euo pipefail
|
||||
BASE_VERSION=$(node -p "require('./desktop/package.json').version")
|
||||
if ! [[ "$BASE_VERSION" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)(-[0-9A-Za-z.-]+)?$ ]]; then
|
||||
echo "::error::Desktop version '$BASE_VERSION' is not semver"
|
||||
exit 1
|
||||
fi
|
||||
VERSION="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.$((BASH_REMATCH[3] + 1))-test.${GITHUB_RUN_NUMBER}"
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "Building canary version $VERSION from $GITHUB_SHA"
|
||||
|
||||
- name: Patch canary 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: Resolve native toolchain identity
|
||||
id: native_toolchain
|
||||
run: echo "id=$(scripts/desktop-native-toolchain-id.sh linux)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Compute this after cargo update so the key describes the graph that is
|
||||
# actually compiled. The helper normalizes only Buzz Desktop's release
|
||||
# version, allowing a canary to warm an otherwise identical tag build.
|
||||
- name: Compute exact release cache key
|
||||
id: rust_cache_key
|
||||
env:
|
||||
NATIVE_TOOLCHAIN_ID: ${{ steps.native_toolchain.outputs.id }}
|
||||
run: |
|
||||
KEY=$(scripts/desktop-release-cache-key.py \
|
||||
--platform "$RUNNER_OS" \
|
||||
--target x86_64-unknown-linux-gnu \
|
||||
--features mesh-llm \
|
||||
--native-inputs "$NATIVE_TOOLCHAIN_ID")
|
||||
echo "key=$KEY" >> "$GITHUB_OUTPUT"
|
||||
echo "Release cache key: $KEY"
|
||||
|
||||
- name: Restore exact release Cargo cache
|
||||
id: rust_cache
|
||||
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
desktop/src-tauri/target
|
||||
!desktop/src-tauri/target/**/release/bundle
|
||||
key: ${{ steps.rust_cache_key.outputs.key }}
|
||||
|
||||
- name: Generate non-updating bundle config
|
||||
run: |
|
||||
cat > desktop/src-tauri/tauri.canary.conf.json <<'JSON'
|
||||
{
|
||||
"bundle": {
|
||||
"createUpdaterArtifacts": false
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
- name: Build Linux Tauri app
|
||||
run: cd desktop && pnpm tauri build --ci --bundles deb,appimage --features mesh-llm --config src-tauri/tauri.canary.conf.json
|
||||
env:
|
||||
CMAKE_POLICY_VERSION_MINIMUM: "3.5"
|
||||
|
||||
- name: Fix AppImage (remove infra libs, symlink system GStreamer)
|
||||
# fix-appimage.sh checks for TAURI_SIGNING_PRIVATE_KEY and skips
|
||||
# re-signing when absent, so no signing env vars are needed here.
|
||||
# Repacking still runs, removing the Mesa/GLib/GStreamer conflict libs.
|
||||
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]}"
|
||||
|
||||
- name: Measure release Cargo cache inputs
|
||||
if: always()
|
||||
run: du -sh ~/.cargo/registry ~/.cargo/git target desktop/src-tauri/target 2>/dev/null || true
|
||||
|
||||
# Only this trusted, main-bound canary writes the cache. Excluding bundle
|
||||
# output prevents installers from entering it.
|
||||
- name: Save exact release Cargo cache
|
||||
if: steps.rust_cache.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
desktop/src-tauri/target
|
||||
!desktop/src-tauri/target/**/release/bundle
|
||||
key: ${{ steps.rust_cache_key.outputs.key }}
|
||||
|
||||
- name: Save pnpm store cache
|
||||
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: pnpm-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- name: Locate Linux build artifacts
|
||||
id: artifacts
|
||||
run: |
|
||||
set -euo pipefail
|
||||
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 canary packages
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: buzz-linux-canary-${{ github.sha }}
|
||||
path: |
|
||||
${{ steps.artifacts.outputs.deb }}
|
||||
${{ steps.artifacts.outputs.appimage }}
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
@@ -0,0 +1,126 @@
|
||||
name: macOS Intel Canary
|
||||
|
||||
# Produces an unsigned Intel DMG from trusted main. Its release-equivalent
|
||||
# Cargo state warms the distinct x86_64 release target without signing or
|
||||
# publishing anything.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build macOS Intel canary
|
||||
if: github.repository == 'block/buzz'
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
TARGET: x86_64-apple-darwin
|
||||
steps:
|
||||
- name: Require main
|
||||
env:
|
||||
SOURCE_REF: ${{ github.ref }}
|
||||
run: |
|
||||
if [[ "$SOURCE_REF" != "refs/heads/main" ]]; then
|
||||
echo "::error::Canary builds must run from main; got $SOURCE_REF"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
|
||||
|
||||
- name: Add Rust target
|
||||
run: rustup target add "$TARGET"
|
||||
|
||||
- name: Install desktop dependencies
|
||||
run: just desktop-install-ci
|
||||
|
||||
- name: Derive and patch canary version
|
||||
run: |
|
||||
BASE_VERSION=$(node -p "require('./desktop/package.json').version")
|
||||
VERSION="${BASE_VERSION%%-*}-intel-test.${GITHUB_RUN_NUMBER}"
|
||||
cd desktop && node scripts/set-version-from-tag.mjs "$VERSION"
|
||||
cd src-tauri && cargo update --workspace
|
||||
|
||||
- name: Resolve native toolchain identity
|
||||
id: native_toolchain
|
||||
run: echo "id=$(scripts/desktop-native-toolchain-id.sh macos)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Compute exact release cache key
|
||||
id: rust_cache_key
|
||||
env:
|
||||
NATIVE_TOOLCHAIN_ID: ${{ steps.native_toolchain.outputs.id }}
|
||||
run: |
|
||||
KEY=$(scripts/desktop-release-cache-key.py \
|
||||
--platform "$RUNNER_OS" \
|
||||
--target "$TARGET" \
|
||||
--features default \
|
||||
--native-inputs "$NATIVE_TOOLCHAIN_ID")
|
||||
echo "key=$KEY" >> "$GITHUB_OUTPUT"
|
||||
echo "Release cache key: $KEY"
|
||||
|
||||
- name: Restore exact release Cargo cache
|
||||
id: rust_cache
|
||||
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
desktop/src-tauri/target
|
||||
!desktop/src-tauri/target/**/release/bundle
|
||||
key: ${{ steps.rust_cache_key.outputs.key }}
|
||||
|
||||
- name: Generate non-updating bundle config
|
||||
run: |
|
||||
cat > desktop/src-tauri/tauri.canary.conf.json <<'JSON'
|
||||
{"bundle":{"createUpdaterArtifacts":false,"macOS":{"minimumSystemVersion":"10.15"}}}
|
||||
JSON
|
||||
|
||||
- name: Build Intel 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 Intel DMG
|
||||
run: cd desktop && pnpm tauri build --verbose --no-sign --target "$TARGET" --bundles dmg --config src-tauri/tauri.canary.conf.json
|
||||
env:
|
||||
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 fresh Intel DMG
|
||||
id: artifact
|
||||
run: |
|
||||
DMG=$(find "desktop/src-tauri/target/${TARGET}/release/bundle/dmg" -name '*.dmg' -type f | head -1)
|
||||
[[ -n "$DMG" ]] || { echo "::error::No Intel DMG found"; exit 1; }
|
||||
echo "dmg=$DMG" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload Intel canary
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: buzz-macos-intel-canary-${{ github.sha }}
|
||||
path: ${{ steps.artifact.outputs.dmg }}
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
- name: Measure release Cargo cache inputs
|
||||
if: always()
|
||||
run: du -sh ~/.cargo/registry ~/.cargo/git target desktop/src-tauri/target 2>/dev/null || true
|
||||
|
||||
- name: Save exact release Cargo cache
|
||||
if: steps.rust_cache.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
desktop/src-tauri/target
|
||||
!desktop/src-tauri/target/**/release/bundle
|
||||
key: ${{ steps.rust_cache_key.outputs.key }}
|
||||
@@ -0,0 +1,111 @@
|
||||
name: Mesh Lifecycle
|
||||
# Relay-driven mesh lifecycle smoke: membership → signed discovery notes →
|
||||
# relay-derived allowlist → join → CPU inference over QUIC → stranger denied
|
||||
# (relay membership rejection + no routed inference, with a differential
|
||||
# trusted-inference health proof so a dead serve node can't fake a denial).
|
||||
# Runs the full Buzz "shared compute" join story with three real mesh-llm
|
||||
# node processes on one runner, using the Buzz relay as the control plane
|
||||
# (no hand-carried invite tokens). Mirrors the shape mesh-llm's own CI uses
|
||||
# for its two-node smokes (tiny CPU model, one runner, real QUIC mesh).
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'crates/buzz-relay/examples/mesh_*.rs'
|
||||
- 'crates/buzz-relay/Cargo.toml'
|
||||
- 'crates/buzz-admin/**'
|
||||
- 'crates/buzz-test-client/**'
|
||||
- 'crates/buzz-ws-client/**'
|
||||
- 'Cargo.lock'
|
||||
- 'desktop/src-tauri/src/mesh_llm/**'
|
||||
- 'scripts/ci-mesh-lifecycle-smoke.sh'
|
||||
- 'scripts/start-relay-for-tests.sh'
|
||||
- '.github/workflows/mesh-lifecycle.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'crates/buzz-relay/examples/mesh_*.rs'
|
||||
- 'crates/buzz-relay/Cargo.toml'
|
||||
- 'crates/buzz-admin/**'
|
||||
- 'crates/buzz-test-client/**'
|
||||
- 'crates/buzz-ws-client/**'
|
||||
- 'Cargo.lock'
|
||||
- 'desktop/src-tauri/src/mesh_llm/**'
|
||||
- 'scripts/ci-mesh-lifecycle-smoke.sh'
|
||||
- 'scripts/start-relay-for-tests.sh'
|
||||
- '.github/workflows/mesh-lifecycle.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: mesh-lifecycle-${{ github.event_name == 'pull_request' && github.ref || github.sha }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
lifecycle-smoke:
|
||||
name: Relay-Driven Mesh Lifecycle Smoke
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
|
||||
|
||||
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
||||
with:
|
||||
save-if: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
# The mesh-llm SDK downloads a signed native runtime (llama.cpp CPU
|
||||
# build) on first init, and the serve node downloads the smoke model
|
||||
# from HuggingFace on first run. Key on the lockfile so a mesh pin bump
|
||||
# rolls the runtime cache; the model ref is stable.
|
||||
- name: Restore mesh runtime + model caches
|
||||
id: mesh-caches
|
||||
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
|
||||
with:
|
||||
path: |
|
||||
~/.cache/mesh-llm/native-runtimes
|
||||
~/.cache/huggingface/hub
|
||||
key: mesh-lifecycle-${{ runner.os }}-smollm2-135m-${{ hashFiles('Cargo.lock') }}
|
||||
restore-keys: |
|
||||
mesh-lifecycle-${{ runner.os }}-smollm2-135m-
|
||||
|
||||
- name: Start integration services
|
||||
run: |
|
||||
for attempt in 1 2 3; do
|
||||
if docker compose up -d postgres redis minio minio-init; then
|
||||
break
|
||||
fi
|
||||
if [ "$attempt" -eq 3 ]; then
|
||||
echo "docker compose up failed after 3 attempts" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "docker compose up failed (attempt $attempt), retrying in $((attempt * 5))s..." >&2
|
||||
sleep $((attempt * 5))
|
||||
done
|
||||
|
||||
- name: Run relay-driven mesh lifecycle smoke
|
||||
run: ./scripts/ci-mesh-lifecycle-smoke.sh 2>&1 | tee /tmp/mesh-lifecycle-harness.log
|
||||
|
||||
- name: Save mesh runtime + model caches
|
||||
if: github.ref == 'refs/heads/main' && steps.mesh-caches.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5
|
||||
with:
|
||||
path: |
|
||||
~/.cache/mesh-llm/native-runtimes
|
||||
~/.cache/huggingface/hub
|
||||
key: mesh-lifecycle-${{ runner.os }}-smollm2-135m-${{ hashFiles('Cargo.lock') }}
|
||||
|
||||
- name: Upload relay + harness logs
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: mesh-lifecycle-logs
|
||||
path: |
|
||||
/tmp/buzz-relay.log
|
||||
/tmp/mesh-lifecycle-harness.log
|
||||
if-no-files-found: ignore
|
||||
@@ -0,0 +1,69 @@
|
||||
name: Publish Mobile Release Candidate
|
||||
run-name: Publish mobile-v${{ inputs.version }}-rc.${{ inputs.candidate_number }}
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: Mobile marketing version (X.Y.Z)
|
||||
required: true
|
||||
type: string
|
||||
candidate_number:
|
||||
description: Expected next release-candidate number
|
||||
required: true
|
||||
type: string
|
||||
target_sha:
|
||||
description: Exact current block/buzz main commit
|
||||
required: true
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: mobile-release-candidate-${{ inputs.version }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Require the reviewed workflow from main
|
||||
env:
|
||||
DISPATCH_REF: ${{ github.ref }}
|
||||
run: |
|
||||
if [ "$DISPATCH_REF" != "refs/heads/main" ]; then
|
||||
echo "::error::Mobile candidates must be dispatched from main, not $DISPATCH_REF"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Require canonical repository
|
||||
env:
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
run: |
|
||||
if [ "$REPOSITORY" != "block/buzz" ]; then
|
||||
echo "::error::Mobile candidate publication is restricted to block/buzz"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Create release tagger token
|
||||
id: release-tagger
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
with:
|
||||
client-id: ${{ vars.BUZZ_RELEASE_TAGGER_CLIENT_ID }}
|
||||
private-key: ${{ secrets.BUZZ_RELEASE_TAGGER_PRIVATE_KEY }}
|
||||
permission-contents: write
|
||||
|
||||
- name: Publish annotated candidate tag
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.release-tagger.outputs.token }}
|
||||
MOBILE_VERSION: ${{ inputs.version }}
|
||||
CANDIDATE_NUMBER: ${{ inputs.candidate_number }}
|
||||
TARGET_SHA: ${{ inputs.target_sha }}
|
||||
run: scripts/publish-mobile-release-candidate.sh "$MOBILE_VERSION" "$CANDIDATE_NUMBER" "$TARGET_SHA"
|
||||
@@ -0,0 +1,68 @@
|
||||
name: push gateway helm chart
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Chart semver (without push-chart-v prefix)"
|
||||
required: true
|
||||
ref:
|
||||
description: "Matching push-chart-v tag"
|
||||
required: true
|
||||
push:
|
||||
tags: ["push-chart-v[0-9]*"]
|
||||
pull_request:
|
||||
paths:
|
||||
- "deploy/charts/buzz-push-gateway/**"
|
||||
- ".github/workflows/push-gateway-helm-chart.yml"
|
||||
permissions: {}
|
||||
env:
|
||||
CHART_REPO: oci://ghcr.io/block/buzz/charts
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
permissions: { contents: read }
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||
with:
|
||||
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || '' }}
|
||||
fetch-depth: 0
|
||||
- uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4
|
||||
with: { version: v3.16.4 }
|
||||
- run: deploy/charts/buzz-push-gateway/tests/render.sh
|
||||
- run: deploy/charts/buzz-push-gateway/tests/release-contract.sh
|
||||
publish:
|
||||
if: github.event_name != 'pull_request'
|
||||
needs: validate
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||
with:
|
||||
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || '' }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4
|
||||
with: { version: v3.16.4 }
|
||||
- name: Verify release tag and chart version
|
||||
env:
|
||||
INPUT_VERSION: ${{ inputs.version }}
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
version="${INPUT_VERSION:-${REF_NAME#push-chart-v}}"
|
||||
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]
|
||||
test "$(helm show chart deploy/charts/buzz-push-gateway | awk '/^version:/ {print $2}')" = "$version"
|
||||
if [ -n "$INPUT_VERSION" ]; then
|
||||
test "$(git rev-parse HEAD)" = "$(git rev-parse "refs/tags/push-chart-v${version}^{commit}")"
|
||||
fi
|
||||
echo "VERSION=$version" >> "$GITHUB_ENV"
|
||||
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- run: helm package deploy/charts/buzz-push-gateway --destination dist
|
||||
- run: helm push "dist/buzz-push-gateway-${VERSION}.tgz" "$CHART_REPO"
|
||||
@@ -0,0 +1,952 @@
|
||||
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
|
||||
|
||||
- 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
|
||||
|
||||
- name: Upload latest.json to rolling release last
|
||||
if: ${{ !contains(needs.setup.outputs.version, '-') }}
|
||||
run: gh release upload buzz-desktop-latest latest.json --clobber
|
||||
@@ -0,0 +1,257 @@
|
||||
name: Signed macOS Canary
|
||||
|
||||
# Produces a signed and notarized Apple Silicon DMG from main without creating
|
||||
# a tag, GitHub Release, or auto-updater artifact. The DMG is available only as
|
||||
# a short-lived GitHub Actions artifact for explicit testing.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build signed macOS canary
|
||||
if: github.repository == 'block/buzz'
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write # required by block/apple-codesign-action for OIDC
|
||||
steps:
|
||||
- name: Require main
|
||||
env:
|
||||
SOURCE_REF: ${{ github.ref }}
|
||||
run: |
|
||||
if [[ "$SOURCE_REF" != "refs/heads/main" ]]; then
|
||||
echo "::error::Signed canary builds must run from main; got $SOURCE_REF"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: echo "STORE_PATH=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Restore pnpm store cache
|
||||
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: pnpm-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: pnpm-${{ runner.os }}-
|
||||
|
||||
- name: Install desktop dependencies
|
||||
run: just desktop-install-ci
|
||||
|
||||
- name: Derive canary version
|
||||
id: version
|
||||
run: |
|
||||
set -euo pipefail
|
||||
BASE_VERSION=$(node -p "require('./desktop/package.json').version")
|
||||
if ! [[ "$BASE_VERSION" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)(-[0-9A-Za-z.-]+)?$ ]]; then
|
||||
echo "::error::Desktop version '$BASE_VERSION' is not semver"
|
||||
exit 1
|
||||
fi
|
||||
VERSION="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.$((BASH_REMATCH[3] + 1))-test.${GITHUB_RUN_NUMBER}"
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "Building canary version $VERSION from $GITHUB_SHA"
|
||||
|
||||
- name: Patch canary 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: Resolve native toolchain identity
|
||||
id: native_toolchain
|
||||
run: echo "id=$(scripts/desktop-native-toolchain-id.sh macos)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Compute this after cargo update so the key describes the graph that is
|
||||
# actually compiled. The helper normalizes only Buzz Desktop's release
|
||||
# version, allowing a canary to warm an otherwise identical tag build.
|
||||
- name: Compute exact release cache key
|
||||
id: rust_cache_key
|
||||
env:
|
||||
NATIVE_TOOLCHAIN_ID: ${{ steps.native_toolchain.outputs.id }}
|
||||
run: |
|
||||
KEY=$(scripts/desktop-release-cache-key.py \
|
||||
--platform "$RUNNER_OS" \
|
||||
--target aarch64-apple-darwin \
|
||||
--features mesh-llm \
|
||||
--native-inputs "$NATIVE_TOOLCHAIN_ID")
|
||||
echo "key=$KEY" >> "$GITHUB_OUTPUT"
|
||||
echo "Release cache key: $KEY"
|
||||
|
||||
- name: Restore exact release Cargo cache
|
||||
id: rust_cache
|
||||
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
desktop/src-tauri/target
|
||||
!desktop/src-tauri/target/**/release/bundle
|
||||
key: ${{ steps.rust_cache_key.outputs.key }}
|
||||
|
||||
- name: Generate non-updating bundle config
|
||||
run: |
|
||||
cat > desktop/src-tauri/tauri.canary.conf.json <<'JSON'
|
||||
{
|
||||
"bundle": {
|
||||
"macOS": {
|
||||
"minimumSystemVersion": "10.15"
|
||||
},
|
||||
"createUpdaterArtifacts": false
|
||||
}
|
||||
}
|
||||
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
|
||||
MESH_ROOT=$(find "${CARGO_HOME:-$HOME/.cargo}/git/checkouts" -path "*/$MESH_REV_SHORT" -type d -name "$MESH_REV_SHORT" | head -1)
|
||||
if [[ -z "$MESH_ROOT" ]]; then
|
||||
echo "::error::mesh-llm checkout for $MESH_REV_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.canary.conf.json
|
||||
env:
|
||||
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: |
|
||||
DMG=$(find desktop/src-tauri/target/release/bundle/dmg -name '*.dmg' -type f | head -1)
|
||||
if [[ -z "$DMG" ]]; then
|
||||
echo "::error::No DMG found"
|
||||
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-canary-${{ github.sha }}-${{ github.run_id }}-arm64
|
||||
|
||||
- name: Verify signed app
|
||||
env:
|
||||
SIGNED_APP_ZIP: ${{ steps.codesign.outputs.signed-artifact-path }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
EXTRACT_DIR="${RUNNER_TEMP}/signed-app-extract"
|
||||
rm -rf "$EXTRACT_DIR" && mkdir -p "$EXTRACT_DIR"
|
||||
ditto -x -k "$SIGNED_APP_ZIP" "$EXTRACT_DIR"
|
||||
codesign --verify --deep --strict --verbose=2 "$EXTRACT_DIR/Buzz.app"
|
||||
spctl --assess --type execute --verbose=4 "$EXTRACT_DIR/Buzz.app"
|
||||
desktop/scripts/verify-macos-entitlements.sh "$EXTRACT_DIR/Buzz.app"
|
||||
|
||||
- name: Stage signed DMG
|
||||
id: artifact
|
||||
env:
|
||||
SIGNED_DMG: ${{ steps.codesign.outputs.signed-dmg-path }}
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
NAME="Buzz_${VERSION}_aarch64-signed.dmg"
|
||||
cp "$SIGNED_DMG" "$RUNNER_TEMP/$NAME"
|
||||
echo "path=$RUNNER_TEMP/$NAME" >> "$GITHUB_OUTPUT"
|
||||
echo "name=$NAME" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload signed canary
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: buzz-macos-canary-${{ github.sha }}
|
||||
path: ${{ steps.artifact.outputs.path }}
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
- name: Measure release Cargo cache inputs
|
||||
if: always()
|
||||
run: du -sh ~/.cargo/registry ~/.cargo/git target desktop/src-tauri/target 2>/dev/null || true
|
||||
|
||||
# Only this trusted, main-bound canary writes the cache. Excluding bundle
|
||||
# output prevents installers or signed artifacts from entering it.
|
||||
- name: Save exact release Cargo cache
|
||||
if: steps.rust_cache.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
desktop/src-tauri/target
|
||||
!desktop/src-tauri/target/**/release/bundle
|
||||
key: ${{ steps.rust_cache_key.outputs.key }}
|
||||
|
||||
- name: Save pnpm store cache
|
||||
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: pnpm-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
@@ -0,0 +1,233 @@
|
||||
name: Sprig image
|
||||
|
||||
# Builds and publishes the public agent container image as
|
||||
# ghcr.io/block/buzz-sprig — the digest-pinned box the Kubernetes backend
|
||||
# deploys agents into (see Dockerfile.sprig and docs/remote-agents.md).
|
||||
#
|
||||
# Strategy mirrors docker.yml (the relay image): each architecture builds on
|
||||
# its native runner, pushes to GHCR by digest, then a merge job stitches the
|
||||
# per-arch digests into one multi-arch manifest and attests provenance.
|
||||
# No QEMU emulation.
|
||||
#
|
||||
# Triggers:
|
||||
# - push to main (paths-filtered) → :main + :sha-<7>
|
||||
# - tag sprig-v* → semver family (shared with sprig.yml's
|
||||
# binary release — one tag versions both)
|
||||
# - pull_request (paths-filtered) → build only, no push
|
||||
# - workflow_dispatch → manual publish at the current ref
|
||||
#
|
||||
# NOTE: the first push creates the GHCR package PRIVATE by default. An org
|
||||
# admin must flip ghcr.io/block/buzz-sprig to public once (Package settings →
|
||||
# Change visibility). Subsequent pushes keep the visibility.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ["sprig-v[0-9]*"]
|
||||
paths:
|
||||
- "Dockerfile.sprig"
|
||||
- "scripts/sprig-entrypoint.sh"
|
||||
- ".github/workflows/sprig-image.yml"
|
||||
- "Cargo.toml"
|
||||
- "Cargo.lock"
|
||||
- "rust-toolchain.toml"
|
||||
- "crates/**"
|
||||
pull_request:
|
||||
paths:
|
||||
- "Dockerfile.sprig"
|
||||
- "scripts/sprig-entrypoint.sh"
|
||||
- ".github/workflows/sprig-image.yml"
|
||||
workflow_dispatch: {}
|
||||
|
||||
concurrency:
|
||||
group: sprig-image-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref_type == 'branch' && github.event_name == 'pull_request' }}
|
||||
|
||||
permissions: {}
|
||||
|
||||
env:
|
||||
# Single source of truth for the image name; override with the
|
||||
# GHCR_SPRIG_IMAGE repo variable (same pattern as docker.yml).
|
||||
IMAGE_NAME: ${{ vars.GHCR_SPRIG_IMAGE != '' && vars.GHCR_SPRIG_IMAGE || 'ghcr.io/block/buzz-sprig' }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build (${{ matrix.platform }})
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-24.04
|
||||
arch: amd64
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
arch: arm64
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||
with:
|
||||
# Same OOM cap as docker.yml — Rust compiles blow the 7GB runner
|
||||
# at buildkit's default parallelism of 4.
|
||||
buildkitd-config-inline: |
|
||||
[worker.oci]
|
||||
max-parallelism = 2
|
||||
|
||||
- name: Log in to GHCR
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||
with:
|
||||
images: ${{ env.IMAGE_NAME }}
|
||||
# match=^sprig-v(.*)$ strips the tag prefix for the semver parser,
|
||||
# exactly as docker.yml does for relay-v. :latest comes from
|
||||
# flavor.latest=auto — stable semver only, never main pushes.
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=sha,prefix=sha-,format=short
|
||||
type=semver,pattern={{version}},match=^sprig-v(.*)$
|
||||
type=semver,pattern={{major}}.{{minor}},match=^sprig-v(.*)$
|
||||
labels: |
|
||||
org.opencontainers.image.title=Buzz Sprig
|
||||
org.opencontainers.image.description=Agent runtime image for Buzz remote agents (buzz-acp multicall + git + curl)
|
||||
org.opencontainers.image.licenses=Apache-2.0
|
||||
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.sprig
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }}
|
||||
cache-from: |
|
||||
type=registry,ref=${{ env.IMAGE_NAME }}-buildcache:${{ matrix.arch }}
|
||||
cache-to: |
|
||||
${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && format('type=registry,ref={0}-buildcache:{1},mode=max,compression=zstd', env.IMAGE_NAME, matrix.arch) || '' }}
|
||||
|
||||
- name: Export digest
|
||||
if: github.event_name != 'pull_request'
|
||||
env:
|
||||
DIGEST: ${{ steps.build.outputs.digest }}
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
touch "/tmp/digests/${DIGEST#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: sprig-digest-${{ matrix.arch }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge:
|
||||
name: Merge multi-arch manifest
|
||||
if: github.event_name != 'pull_request'
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
|
||||
steps:
|
||||
- name: Download per-arch digests
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: sprig-digest-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||
with:
|
||||
images: ${{ env.IMAGE_NAME }}
|
||||
# Must mirror the build job's tag matrix exactly (see docker.yml).
|
||||
flavor: |
|
||||
latest=auto
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=sha,prefix=sha-,format=short
|
||||
type=semver,pattern={{version}},match=^sprig-v(.*)$
|
||||
type=semver,pattern={{major}}.{{minor}},match=^sprig-v(.*)$
|
||||
|
||||
- name: Create and push manifest list
|
||||
id: manifest
|
||||
working-directory: /tmp/digests
|
||||
env:
|
||||
IMAGE_NAME: ${{ env.IMAGE_NAME }}
|
||||
META_TAGS: ${{ steps.meta.outputs.tags }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
tags=()
|
||||
while IFS= read -r tag; do
|
||||
[ -n "$tag" ] && tags+=("-t" "$tag")
|
||||
done <<< "$META_TAGS"
|
||||
|
||||
digests=()
|
||||
for digest in *; do
|
||||
digests+=("${IMAGE_NAME}@sha256:${digest}")
|
||||
done
|
||||
|
||||
docker buildx imagetools create "${tags[@]}" "${digests[@]}"
|
||||
|
||||
first_tag=$(echo "$META_TAGS" | head -n1)
|
||||
merged_digest=$(docker buildx imagetools inspect "$first_tag" \
|
||||
--format '{{json .Manifest}}' | jq -r '.digest')
|
||||
echo "digest=${merged_digest}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Attest provenance for the merged image
|
||||
# Verify with: gh attestation verify oci://ghcr.io/block/buzz-sprig:<tag> --owner block
|
||||
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
|
||||
with:
|
||||
subject-name: ${{ env.IMAGE_NAME }}
|
||||
subject-digest: ${{ steps.manifest.outputs.digest }}
|
||||
push-to-registry: true
|
||||
|
||||
- name: Summary
|
||||
env:
|
||||
IMAGE_NAME: ${{ env.IMAGE_NAME }}
|
||||
DIGEST: ${{ steps.manifest.outputs.digest }}
|
||||
run: |
|
||||
{
|
||||
echo "### Sprig image published"
|
||||
echo '```'
|
||||
echo "${IMAGE_NAME}@${DIGEST}"
|
||||
echo '```'
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -0,0 +1,184 @@
|
||||
name: Sprig
|
||||
|
||||
# Builds and publishes Sprig — one deploy-anywhere Linux multicall binary for:
|
||||
# buzz-acp ACP harness that bridges Buzz events to the LLM agent
|
||||
# buzz-agent ACP-compliant agent (spawns MCP, calls LLMs)
|
||||
# buzz-dev-mcp Developer MCP server (multicall: rg, tree, buzz,
|
||||
# git-credential-nostr, git-sign-nostr)
|
||||
#
|
||||
# Targets: x86_64-unknown-linux-musl and aarch64-unknown-linux-musl (static
|
||||
# musl so the tarball runs on any modern Linux without libc surprises).
|
||||
#
|
||||
# Triggers:
|
||||
# - push to main → updates rolling `sprig-latest` release
|
||||
# - tag `sprig-v*` → versioned release
|
||||
# - workflow_dispatch → manual canary build (no release publish unless asked)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ["sprig-v*"]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
publish:
|
||||
description: "Publish to the rolling release (otherwise artifacts only)"
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build (${{ matrix.target }})
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
permissions:
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target:
|
||||
- x86_64-unknown-linux-musl
|
||||
- aarch64-unknown-linux-musl
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
|
||||
|
||||
- name: Install cross
|
||||
uses: taiki-e/install-action@0fd46367812ee04360509b4169d9f659d6892bb2 # v2.79.15
|
||||
with:
|
||||
tool: cross@0.2.5
|
||||
|
||||
- name: Resolve version
|
||||
id: ver
|
||||
run: |
|
||||
set -euo pipefail
|
||||
WORKSPACE_VERSION=$(cargo metadata --no-deps --format-version=1 \
|
||||
| jq -r '.packages[] | select(.name=="sprig") | .version')
|
||||
|
||||
REF="${GITHUB_REF#refs/tags/}"
|
||||
if [[ "$GITHUB_REF" == refs/tags/sprig-v* ]]; then
|
||||
VERSION="${REF#sprig-v}"
|
||||
CHANNEL="tag"
|
||||
else
|
||||
SHORT_SHA="${GITHUB_SHA::7}"
|
||||
VERSION="${WORKSPACE_VERSION}+git.${SHORT_SHA}"
|
||||
CHANNEL="rolling"
|
||||
fi
|
||||
{
|
||||
echo "workspace_version=$WORKSPACE_VERSION"
|
||||
echo "version=$VERSION"
|
||||
echo "channel=$CHANNEL"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
echo "Resolved version=$VERSION channel=$CHANNEL"
|
||||
|
||||
- name: Build & package Sprig
|
||||
id: pkg
|
||||
env:
|
||||
TARGET: ${{ matrix.target }}
|
||||
VERSION: ${{ steps.ver.outputs.version }}
|
||||
CHANNEL: ${{ steps.ver.outputs.channel }}
|
||||
GIT_SHA: ${{ github.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ "$CHANNEL" == "tag" ]]; then
|
||||
ARCHIVE_BASENAME="sprig-${VERSION}-${TARGET}"
|
||||
else
|
||||
ARCHIVE_BASENAME="sprig-${TARGET}"
|
||||
fi
|
||||
ARCHIVE_BASENAME="$ARCHIVE_BASENAME" \
|
||||
./scripts/build-sprig.sh "$VERSION" "$TARGET"
|
||||
ARCHIVE="dist/${ARCHIVE_BASENAME}.tar.gz"
|
||||
test -f "$ARCHIVE"
|
||||
echo "archive=$ARCHIVE" >> "$GITHUB_OUTPUT"
|
||||
echo "archive_name=${ARCHIVE_BASENAME}.tar.gz" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload workflow artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: sprig-${{ matrix.target }}
|
||||
path: |
|
||||
${{ steps.pkg.outputs.archive }}
|
||||
${{ steps.pkg.outputs.archive }}.sha256
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
|
||||
publish:
|
||||
name: Publish rolling release
|
||||
needs: build
|
||||
if: |
|
||||
github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
|| (github.event_name == 'workflow_dispatch' && inputs.publish)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Download all Sprig artifacts
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
path: dist
|
||||
pattern: sprig-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: List assets
|
||||
run: ls -lh dist/
|
||||
|
||||
- name: Update rolling release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
SHA: ${{ github.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG="sprig-latest"
|
||||
TITLE="Sprig (rolling)"
|
||||
NOTES="Rolling Linux build of Sprig (all-in-one buzz-acp + buzz-agent + buzz-dev-mcp), tracking \`main\` (\`${SHA}\`)."
|
||||
|
||||
gh release edit "$TAG" \
|
||||
--prerelease \
|
||||
--title "$TITLE" \
|
||||
--notes "$NOTES"
|
||||
gh release upload "$TAG" dist/* --clobber
|
||||
|
||||
publish-tag:
|
||||
name: Publish tagged release
|
||||
needs: build
|
||||
if: startsWith(github.ref, 'refs/tags/sprig-v')
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Download all Sprig artifacts
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
path: dist
|
||||
pattern: sprig-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Resolve tag version
|
||||
id: ver
|
||||
run: |
|
||||
REF="${GITHUB_REF#refs/tags/}"
|
||||
VERSION="${REF#sprig-v}"
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=$REF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create tagged release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG: ${{ steps.ver.outputs.tag }}
|
||||
VERSION: ${{ steps.ver.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh release create "$TAG" \
|
||||
--title "Sprig v${VERSION}" \
|
||||
--notes "Sprig v${VERSION} — Linux all-in-one builds of buzz-acp + buzz-agent + buzz-dev-mcp." \
|
||||
dist/*
|
||||
@@ -0,0 +1,197 @@
|
||||
name: Windows Canary
|
||||
|
||||
# Produces an unsigned Windows NSIS installer from main without creating
|
||||
# a tag, GitHub Release, or auto-updater artifact. The installer is available
|
||||
# only as a short-lived GitHub Actions artifact for explicit testing.
|
||||
#
|
||||
# Design notes vs. signed-macos-canary.yml:
|
||||
# - No mesh-llm: release-windows doesn't build it.
|
||||
# - pnpm store restore/save pattern mirrors ci.yml:149-196.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Windows canary
|
||||
if: github.repository == 'block/buzz'
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: read
|
||||
env:
|
||||
TARGET: x86_64-pc-windows-msvc
|
||||
steps:
|
||||
- name: Require main
|
||||
shell: bash
|
||||
env:
|
||||
SOURCE_REF: ${{ github.ref }}
|
||||
run: |
|
||||
if [[ "$SOURCE_REF" != "refs/heads/main" ]]; then
|
||||
echo "::error::Canary builds must run from main; got $SOURCE_REF"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
# The Windows runner ships with rustup preinstalled; rust-toolchain.toml
|
||||
# in the repo root pins the channel (1.95.0) automatically. We only need
|
||||
# to ensure the cross-compile target is registered; on windows-latest the
|
||||
# host IS x86_64-pc-windows-msvc so this is typically a no-op.
|
||||
- name: Add Rust target
|
||||
shell: bash
|
||||
run: rustup target add "$TARGET"
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 24.14.1
|
||||
package-manager-cache: false
|
||||
|
||||
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0
|
||||
with:
|
||||
version: 11.4.0
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
shell: bash
|
||||
run: echo "STORE_PATH=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Restore pnpm store cache
|
||||
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: pnpm-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: pnpm-${{ runner.os }}-
|
||||
|
||||
- name: Install desktop dependencies
|
||||
shell: bash
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Derive canary version
|
||||
id: version
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
BASE_VERSION=$(node -p "require('./desktop/package.json').version")
|
||||
if ! [[ "$BASE_VERSION" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)(-[0-9A-Za-z.-]+)?$ ]]; then
|
||||
echo "::error::Desktop version '$BASE_VERSION' is not semver"
|
||||
exit 1
|
||||
fi
|
||||
VERSION="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.$((BASH_REMATCH[3] + 1))-test.${GITHUB_RUN_NUMBER}"
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "Building canary version $VERSION from $GITHUB_SHA"
|
||||
|
||||
- name: Patch canary version
|
||||
shell: bash
|
||||
env:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
cd desktop && node scripts/set-version-from-tag.mjs "$VERSION"
|
||||
cd src-tauri && cargo update --workspace
|
||||
|
||||
- name: Resolve native toolchain identity
|
||||
id: native_toolchain
|
||||
shell: bash
|
||||
run: echo "id=$(scripts/desktop-native-toolchain-id.sh windows)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Compute this after cargo update so the key describes the graph that is
|
||||
# actually compiled. The helper normalizes only Buzz Desktop's release
|
||||
# version, allowing a canary to warm an otherwise identical tag build.
|
||||
- name: Compute exact release cache key
|
||||
id: rust_cache_key
|
||||
shell: bash
|
||||
env:
|
||||
NATIVE_TOOLCHAIN_ID: ${{ steps.native_toolchain.outputs.id }}
|
||||
run: |
|
||||
KEY=$(scripts/desktop-release-cache-key.py \
|
||||
--platform "$RUNNER_OS" \
|
||||
--target x86_64-pc-windows-msvc \
|
||||
--features default \
|
||||
--native-inputs "$NATIVE_TOOLCHAIN_ID")
|
||||
echo "key=$KEY" >> "$GITHUB_OUTPUT"
|
||||
echo "Release cache key: $KEY"
|
||||
|
||||
- name: Restore exact release Cargo cache
|
||||
id: rust_cache
|
||||
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
desktop/src-tauri/target
|
||||
!desktop/src-tauri/target/**/release/bundle
|
||||
key: ${{ steps.rust_cache_key.outputs.key }}
|
||||
|
||||
- name: Generate non-updating bundle config
|
||||
shell: bash
|
||||
run: |
|
||||
cat > desktop/src-tauri/tauri.canary.conf.json <<'JSON'
|
||||
{
|
||||
"bundle": {
|
||||
"createUpdaterArtifacts": false
|
||||
}
|
||||
}
|
||||
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 --target "$TARGET" --bundles nsis --config src-tauri/tauri.canary.conf.json
|
||||
env:
|
||||
CMAKE_POLICY_VERSION_MINIMUM: "3.5"
|
||||
|
||||
- name: Locate NSIS installer
|
||||
id: artifact
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
BUNDLE_DIR="desktop/src-tauri/target/${TARGET}/release/bundle"
|
||||
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
|
||||
echo "exe=$EXE" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload Windows canary installer
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: buzz-windows-canary-${{ github.sha }}
|
||||
path: ${{ steps.artifact.outputs.exe }}
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
- name: Measure release Cargo cache inputs
|
||||
if: always()
|
||||
shell: bash
|
||||
run: du -sh ~/.cargo/registry ~/.cargo/git target desktop/src-tauri/target 2>/dev/null || true
|
||||
|
||||
# Only this trusted, main-bound canary writes the cache. Excluding bundle
|
||||
# output prevents installers from entering it.
|
||||
- name: Save exact release Cargo cache
|
||||
if: steps.rust_cache.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
desktop/src-tauri/target
|
||||
!desktop/src-tauri/target/**/release/bundle
|
||||
key: ${{ steps.rust_cache_key.outputs.key }}
|
||||
|
||||
- name: Save pnpm store cache
|
||||
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: pnpm-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
Reference in New Issue
Block a user