Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 190 additions & 0 deletions .github/workflows/signed-macos-canary.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
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

Check warning

Code scanning / zizmor

detects commit SHAs that don't match their version comment tags Warning

detects commit SHAs that don't match their version comment tags
with:
persist-credentials: false

- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1

- 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: 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-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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep signed canaries out of public artifacts

Because block/buzz is the OSS/public source repo, uploading the signed DMG with actions/upload-artifact does not make it private: GitHub's artifact docs say workflow artifacts can be downloaded by signed-in users with repository read access, which in a public repo is effectively any GitHub user. That exposes production-signed -test builds for the full retention window instead of limiting them to explicit testers, so this should use a genuinely access-controlled store or environment rather than a workflow artifact.

Useful? React with 👍 / 👎.

with:
name: buzz-macos-canary-${{ github.sha }}
path: ${{ steps.artifact.outputs.path }}
if-no-files-found: error
retention-days: 7
36 changes: 31 additions & 5 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,13 +150,39 @@ regenerates `mobile/pubspec.lock`.

---

## Manual Fallback
## Signed macOS Canary

If the automated flow isn't suitable (e.g., building from a non-main ref):
Use the manual **Signed macOS Canary** workflow when you need an Apple Silicon
build of current `main` for explicit testing without publishing a release:

1. Go to **Actions > Release** in the GitHub UI
2. Click **Run workflow**
3. Provide the semver version (no `v` prefix) and the ref to build from
```sh
gh workflow run signed-macos-canary.yml --repo block/buzz --ref main
```

The workflow derives a `-test.<run-number>` version, signs and notarizes the
DMG, verifies it with Gatekeeper, and uploads it as a short-lived Actions
artifact with seven-day retention. Because this is a public repository, any
signed-in GitHub user can download that artifact while it exists; it is
unpublished, not private. The workflow has no release permissions, does not
create or move tags, and cannot update `buzz-desktop-latest` or `latest.json`.

Download the artifact from the completed run:

```sh
gh run download <run-id> --repo block/buzz --name <artifact-name>
```

The workflow intentionally accepts only `main`. Use the normal release process
for distributable builds or builds from an immutable release tag.

---

## Manual Release Retry

The **Release** workflow's manual dispatch is only a retry mechanism for an
existing immutable `v<version>` tag. Select that tag in the ref picker and
provide the matching semver version without the `v` prefix. It cannot build
from `main` or another caller-selected source ref.

---

Expand Down
1 change: 1 addition & 0 deletions scripts/test-release-ref-contract.sh
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ fi
grep -q 'verify-release-ref\.sh' "$repo_root/.github/workflows/release.yml"
grep -q 'verify-release-ref\.sh' "$repo_root/.github/workflows/docker.yml"
grep -q 'test-release-ref-contract\.sh' "$repo_root/.github/workflows/ci.yml"
"$repo_root/scripts/test-signed-canary-contract.sh"
auto_tag="$repo_root/.github/workflows/auto-tag-on-release-pr-merge.yml"
grep -q 'actions/create-github-app-token@' "$auto_tag"
grep -q 'client-id:.*vars\.BUZZ_RELEASE_TAGGER_CLIENT_ID' "$auto_tag"
Expand Down
40 changes: 40 additions & 0 deletions scripts/test-signed-canary-contract.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
set -euo pipefail

repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
workflow="$repo_root/.github/workflows/signed-macos-canary.yml"

grep -Fq 'workflow_dispatch:' "$workflow"
# The literal GitHub expression is the contract we are checking.
# shellcheck disable=SC2016
grep -Fq 'SOURCE_REF: ${{ github.ref }}' "$workflow"
grep -Fq '"refs/heads/main"' "$workflow"
grep -Fq 'contents: read' "$workflow"
grep -Fq 'id-token: write' "$workflow"
grep -Fq 'block/apple-codesign-action@' "$workflow"
grep -Fq 'actions/upload-artifact@' "$workflow"
grep -Fq 'retention-days: 7' "$workflow"
grep -Fq '"createUpdaterArtifacts": false' "$workflow"

if grep -Eq 'contents: write|gh release|buzz-desktop-latest|latest\.json|TAURI_SIGNING_PRIVATE_KEY|verify-release-ref\.sh|refs/tags/' "$workflow"; then
echo "signed canary workflow gained a release or publishing capability" >&2
exit 1
fi

on_block=$(
awk '
/^on:$/ { in_on = 1; next }
in_on && /^[^[:space:]#]/ { exit }
in_on && NF && $0 !~ /^[[:space:]]*#/ {
gsub(/[[:space:]]/, "")
print
}
' "$workflow"
)
if [[ "$on_block" != "workflow_dispatch:" ]]; then
echo "signed canary workflow must have workflow_dispatch as its only trigger" >&2
printf 'found on block:\n%s\n' "$on_block" >&2
exit 1
fi

echo "signed canary contract passed"
Loading