Skip to content
Open
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
72 changes: 50 additions & 22 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,13 @@ concurrency:
jobs:
build:
if: github.repository == 'block/buzz-app' && !inputs.candidates && !inputs.promote_version
name: Build, sign, and notarize
name: Build, sign, and notarize (${{ matrix.arch }})
runs-on: macos-latest
strategy:
matrix:
include:
- { target: aarch64-apple-darwin, arch: aarch64, lipo: arm64 }
- { target: x86_64-apple-darwin, arch: x64, lipo: x86_64 }
timeout-minutes: 120
permissions:
contents: read
Expand All @@ -42,6 +47,7 @@ jobs:
env:
CI: "true"
CARGO_TERM_COLOR: always
TARGET: ${{ matrix.target }}
steps:
- name: Require main and signing configuration
env:
Expand All @@ -59,9 +65,18 @@ jobs:
with:
persist-credentials: false
- uses: ./.github/actions/setup
# Hermit's Rust ships only the runner's (Apple Silicon) standard library.
# Cross-compile with the same pinned release from rustup, as Windows does.
- name: Select pinned Rust with the Intel standard library
if: matrix.arch == 'x64'
run: |
version=$(rustc --version | cut -d' ' -f2)
rustup toolchain install "$version" --profile minimal --target "$TARGET"
echo "RUSTUP_TOOLCHAIN=$version" >> "$GITHUB_ENV"
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
Comment thread
matt2e marked this conversation as resolved.
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: macos-prerelease
key: macos-prerelease-${{ matrix.arch }}
cache-all-crates: true

- name: Set preview version
Expand All @@ -85,14 +100,16 @@ jobs:
env:
TAURI_BUNDLER_DMG_IGNORE_CI: "true"
run: |
rm -rf target/release/bundle
pnpm tauri build --ci --no-sign --bundles dmg --config "$RUNNER_TEMP/release.json" -- --locked
# Fail fast if the selected Rust lacks this target's standard library.
test -d "$(rustc --print target-libdir --target "$TARGET")" || { echo "::error::Rust has no $TARGET standard library"; exit 1; }
rm -rf "target/$TARGET/release/bundle"
pnpm tauri build --ci --no-sign --bundles dmg --target "$TARGET" --config "$RUNNER_TEMP/release.json" -- --locked

- name: Locate unsigned DMG
id: unsigned
run: |
shopt -s nullglob
dmgs=(target/release/bundle/dmg/*.dmg)
dmgs=("target/$TARGET/release/bundle/dmg/"*.dmg)
test "${#dmgs[@]}" -eq 1 || { echo '::error::Expected exactly one DMG'; exit 1; }
echo "path=${dmgs[0]}" >> "$GITHUB_OUTPUT"

Expand All @@ -104,11 +121,12 @@ jobs:
codesign-s3-bucket: ${{ secrets.CODESIGN_S3_BUCKET }}
unsigned-artifact-path: ${{ steps.unsigned.outputs.path }}
entitlements-plist-path: src-tauri/entitlements.plist
artifact-name: buzz-app-${{ github.run_id }}-${{ github.run_attempt }}-arm64
artifact-name: buzz-app-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.lipo }}

- name: Verify release DMG
env:
SIGNED_DMG: ${{ steps.codesign.outputs.signed-dmg-path }}
LIPO_ARCH: ${{ matrix.lipo }}
run: |
app_dir=$(mktemp -d "$RUNNER_TEMP/verify-release.XXXXXX")
hdiutil attach "$SIGNED_DMG" -readonly -nobrowse -mountpoint "$app_dir" >/dev/null
Expand All @@ -118,38 +136,44 @@ jobs:
-R '=anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] exists and certificate leaf[field.1.2.840.113635.100.6.1.13] exists and certificate leaf[subject.OU] = EYF346PHUG' "$app"
spctl --assess --type execute --verbose=4 "$app"
xcrun stapler validate "$app"
test "$(lipo -archs "$app/Contents/MacOS/Buzz")" = "$LIPO_ARCH" || { echo "::error::Buzz is not $LIPO_ARCH"; exit 1; }
# Promotion inspects only the Apple Silicon executable, so check each leg here.
for value in "$BUZZ_UPDATER_PUBLIC_KEY" "$BUZZ_UPDATER_ENDPOINT"; do
grep -aqF "$value" "$app/Contents/MacOS/Buzz" || { echo '::error::Buzz does not embed the updater configuration'; exit 1; }
done
test -n "$(/usr/libexec/PlistBuddy -c 'Print :NSMicrophoneUsageDescription' "$app/Contents/Info.plist")"
codesign --display --entitlements :- "$app" > "$RUNNER_TEMP/signed-entitlements.plist"
test "$(/usr/libexec/PlistBuddy -c 'Print :com.apple.security.device.audio-input' "$RUNNER_TEMP/signed-entitlements.plist")" = true || { echo '::error::Signed app is missing microphone entitlement'; exit 1; }
goose="$app/Contents/Resources/bin/goose-acp"
test -x "$goose" || { echo '::error::Signed app is missing goose-acp'; exit 1; }
test "$(lipo -archs "$goose")" = arm64 || { echo '::error::goose-acp is not arm64'; exit 1; }
test "$(lipo -archs "$goose")" = "$LIPO_ARCH" || { echo "::error::goose-acp is not $LIPO_ARCH"; exit 1; }
codesign --verify --strict --verbose=2 \
-R '=anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] exists and certificate leaf[field.1.2.840.113635.100.6.1.13] exists and certificate leaf[subject.OU] = EYF346PHUG' "$goose"
command="$app/Contents/Resources/bin/buzz-agent-command"
test -x "$command" || { echo '::error::Signed app is missing buzz-agent-command'; exit 1; }
test "$(lipo -archs "$command")" = arm64 || { echo '::error::buzz-agent-command is not arm64'; exit 1; }
test "$(lipo -archs "$command")" = "$LIPO_ARCH" || { echo "::error::buzz-agent-command is not $LIPO_ARCH"; exit 1; }
codesign --verify --strict --verbose=2 \
-R '=anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] exists and certificate leaf[subject.OU] = EYF346PHUG' "$command"

- name: Stage release assets and checksums
env:
SIGNED_DMG: ${{ steps.codesign.outputs.signed-dmg-path }}
VERSION: ${{ steps.version.outputs.version }}
ARCH: ${{ matrix.arch }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: |
set -euo pipefail
mkdir release-assets
cp "$SIGNED_DMG" "release-assets/Buzz_${VERSION}_aarch64.dmg"
cp "$SIGNED_DMG" "release-assets/Buzz_${VERSION}_${ARCH}.dmg"

# Package the verified, Apple-signed and stapled app from the release DMG.
app_dir=$(mktemp -d "$RUNNER_TEMP/updater-app.XXXXXX")
hdiutil attach "$SIGNED_DMG" -readonly -nobrowse -mountpoint "$app_dir" >/dev/null
trap 'hdiutil detach "$app_dir" >/dev/null' EXIT
app="$app_dir/Buzz.app"
test -d "$app" || { echo '::error::Signed DMG contains no app'; exit 1; }
archive="$PWD/release-assets/Buzz_${VERSION}_aarch64.app.tar.gz"
archive="$PWD/release-assets/Buzz_${VERSION}_${ARCH}.app.tar.gz"
# ditto copies the complete signed bundle (including symlinks and metadata).
staged_app=$(mktemp -d "$RUNNER_TEMP/updater-staging.XXXXXX")
ditto "$app" "$staged_app/Buzz.app"
Expand All @@ -161,7 +185,7 @@ jobs:

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: signed-macos-release
name: signed-macos-${{ matrix.arch }}
path: release-assets/
if-no-files-found: error
retention-days: 7
Expand All @@ -176,8 +200,12 @@ jobs:
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: signed-macos-release
path: build-assets/macos
name: signed-macos-aarch64
path: build-assets/macos-aarch64
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: signed-macos-x64
path: build-assets/macos-x64
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: windows-x64-candidate
Expand All @@ -196,15 +224,17 @@ jobs:
SOURCE_SHA: ${{ github.sha }}
run: |
set -euo pipefail
for platform in macos windows linux; do
for platform in macos-aarch64 macos-x64 windows linux; do
(cd "build-assets/$platform" && sha256sum --check SHA256SUMS)
done
# Exact names also reject mixed versions after a partial-job rerun.
# Rerun all jobs so every platform uses the same run attempt/version.
mkdir release-assets
cp "build-assets/macos/Buzz_${VERSION}_aarch64.dmg" release-assets/
cp "build-assets/macos/Buzz_${VERSION}_aarch64.app.tar.gz" release-assets/
cp "build-assets/macos/Buzz_${VERSION}_aarch64.app.tar.gz.sig" release-assets/
for arch in aarch64 x64; do
for asset in dmg app.tar.gz app.tar.gz.sig; do
cp "build-assets/macos-$arch/Buzz_${VERSION}_$arch.$asset" release-assets/
done
done
cp "build-assets/windows/Buzz_${VERSION}_x64_unsigned.exe" release-assets/
cp "build-assets/linux/Buzz_${VERSION}_amd64.deb" release-assets/
cp "build-assets/linux/Buzz_${VERSION}_x86_64.AppImage" release-assets/
Expand All @@ -223,7 +253,7 @@ jobs:
gh release create "v$VERSION" release-assets/* \
--target "$SOURCE_SHA" --prerelease --latest=false \
--title "Buzz $VERSION (desktop preview)" \
--notes "Desktop preview from commit $SOURCE_SHA, built by Actions run $GITHUB_RUN_ID. Includes Apple Silicon macOS (signed and notarized), Windows x64 (unsigned NSIS installer), and Linux x64 (Ubuntu 24.04 deb and AppImage). Windows/Linux still require desktop acceptance. Updater-enabled installs update from the preview feed."
--notes "Desktop preview from commit $SOURCE_SHA, built by Actions run $GITHUB_RUN_ID. Includes Apple Silicon and Intel macOS (signed and notarized), Windows x64 (unsigned NSIS installer), and Linux x64 (Ubuntu 24.04 deb and AppImage). Windows/Linux still require desktop acceptance. Updater-enabled installs update from the preview feed."

promote_preview:
name: Promote verified preview
Expand Down Expand Up @@ -253,13 +283,11 @@ jobs:
archive="Buzz_${VERSION}_aarch64.app.tar.gz"
mkdir -p "$RUNNER_TEMP/candidate" "$RUNNER_TEMP/current" "$RUNNER_TEMP/served"
gh release view "v$VERSION" --json isPrerelease --jq .isPrerelease | grep -qx true
for asset in "$archive" "Buzz_${VERSION}_x64_unsigned.exe" "Buzz_${VERSION}_amd64.deb" "Buzz_${VERSION}_x86_64.AppImage"; do
for asset in "$archive" "Buzz_${VERSION}_x64.app.tar.gz" "Buzz_${VERSION}_x64_unsigned.exe" "Buzz_${VERSION}_amd64.deb" "Buzz_${VERSION}_x86_64.AppImage"; do
gh release download "v$VERSION" --pattern "$asset" --pattern "$asset.sig" --dir "$RUNNER_TEMP/candidate"
[[ -s "$RUNNER_TEMP/candidate/$asset" && -s "$RUNNER_TEMP/candidate/$asset.sig" ]]
done
# Previews built before the executable became `Buzz` stay re-promotable until pruned.
executable=$(tar -tzf "$RUNNER_TEMP/candidate/$archive" | grep -xE 'Buzz\.app/Contents/MacOS/(Buzz|buzz-foundation)')
tar -xOzf "$RUNNER_TEMP/candidate/$archive" "$executable" > "$RUNNER_TEMP/candidate/Buzz"
tar -xOzf "$RUNNER_TEMP/candidate/$archive" Buzz.app/Contents/MacOS/Buzz > "$RUNNER_TEMP/candidate/Buzz"
test -s "$RUNNER_TEMP/candidate/Buzz"
current=''
if gh release view preview-feed >/dev/null 2>&1; then
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ Get the newest build from the [releases page](https://github.com/block/buzz-app/

| Platform | Download | Notes |
| --- | --- | --- |
| macOS (Apple Silicon) | `Buzz_…_aarch64.dmg` | Signed and notarized. Intel Macs aren't supported. |
| macOS (Apple Silicon) | `Buzz_…_aarch64.dmg` | Signed and notarized. |
| macOS (Intel) | `Buzz_…_x64.dmg` | Signed and notarized. |
| Windows (x64) | `Buzz_…_x64_unsigned.exe` | Not code-signed, so Windows SmartScreen warns you before it runs. |
| Linux (x64) | `Buzz_…_amd64.deb` or `Buzz_…_x86_64.AppImage` | Built on Ubuntu 24.04. |

Expand Down Expand Up @@ -94,8 +95,8 @@ edit files there.
## Preview limitations

- Builds come straight from `main`; there's no stable release channel yet.
- macOS builds are for Apple Silicon only. The Windows installer is unsigned,
and Windows and Linux get less testing than macOS.
- The Windows installer is unsigned, and Intel macOS, Windows and Linux get
less testing than Apple Silicon macOS.
- Claude and Codex agents need macOS or Linux. Goose and Pi work everywhere.
- Buzz doesn't back up your key, so keep your own copy (see step 2).
- Bringing your identity over from Buzz 0.X doesn't bring your communities
Expand Down
18 changes: 10 additions & 8 deletions docs/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ The **Desktop previews** workflow builds all supported desktop platforms from
`main` and publishes one GitHub prerelease, tagged
`v<app-version>-preview.<run-number>.<attempt>` at the built commit:

- **Apple Silicon macOS:** signed/notarized DMG and a signed updater archive with
its `.sig`.
- **Apple Silicon and Intel macOS:** a signed/notarized DMG and a signed updater
archive with its `.sig` for each architecture.
- **Windows x64:** unsigned NSIS `.exe` installer and its updater `.sig`.
- **Linux x64:** Ubuntu 24.04 `.deb` and AppImage, each with its updater `.sig`.
- **Provenance:** `SOURCE_COMMIT` and one `SHA256SUMS` covering all nine asset files.
- **Provenance:** `SOURCE_COMMIT` and one `SHA256SUMS` covering all twelve asset files.

Publication waits for all three platform jobs, their existing payload checks and
Publication waits for every platform job, their existing payload checks and
the Windows/Linux updater signatures. A failed platform blocks publication and
automatic feed promotion; it does not publish a partial release. Downloads are
checked against each job's checksums before assembly. If a job fails, use
Expand All @@ -24,9 +24,11 @@ Runs are scheduled at **00:17, 06:17, 12:17, and 18:17 UTC**. To start one manua
gh workflow run release.yml --repo block/buzz-app --ref main
```

The macOS build uses `macos-latest` and the repository's pinned toolchain. The
workflow publishes an Apple-signed/notarized DMG and a separately Tauri-signed
updater `.app.tar.gz` and `.sig` for Apple Silicon. The updater archive is
The macOS builds run on Apple Silicon `macos-latest` with the repository's
pinned toolchain; the Intel build cross-compiles `x86_64-apple-darwin` with the
same Rust release from rustup, because Hermit's Rust ships only the host's
standard library. Each publishes an Apple-signed/notarized DMG and a separately
Tauri-signed updater `.app.tar.gz` and `.sig`. The updater archive is
rebuilt from the verified app in the signed DMG; built-in Tauri artifact
creation remains disabled because this DMG-only build does not emit an updater
archive.
Expand Down Expand Up @@ -97,7 +99,7 @@ enable the client flag alone: existing unbound signatures would fail. A signatur
mismatch fails before the rolling manifest is replaced.

The job builds the Tauri manifest with each artifact's signature from that
release and an immutable versioned asset URL: `darwin-aarch64`, `windows-x86_64`,
release and an immutable versioned asset URL: `darwin-aarch64`, `darwin-x86_64`, `windows-x86_64`,
`linux-x86_64-deb` and `linux-x86_64-appimage`. Linux has no generic entry, so
a deb install never receives an AppImage. The job refuses full-SemVer rollback and
changed same-version metadata, and replaces the rolling manifest last. The
Expand Down
1 change: 1 addition & 0 deletions scripts/preview-feed.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const release = `v${version}`;
// Linux entries are installer-specific: a deb install must never receive an AppImage.
const assets = {
"darwin-aarch64": `Buzz_${version}_aarch64.app.tar.gz`,
"darwin-x86_64": `Buzz_${version}_x64.app.tar.gz`,
"windows-x86_64": `Buzz_${version}_x64_unsigned.exe`,
"linux-x86_64-deb": `Buzz_${version}_amd64.deb`,
"linux-x86_64-appimage": `Buzz_${version}_x86_64.AppImage`,
Expand Down
1 change: 1 addition & 0 deletions scripts/prune-preview-releases.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ assert.match(manifest.version, versionPattern);
const protectedTag = `v${manifest.version}`;
const assets = {
"darwin-aarch64": "aarch64.app.tar.gz",
"darwin-x86_64": "x64.app.tar.gz",
"windows-x86_64": "x64_unsigned.exe",
"linux-x86_64-deb": "amd64.deb",
"linux-x86_64-appimage": "x86_64.AppImage",
Expand Down
12 changes: 10 additions & 2 deletions tests/integration/desktop-release.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,16 @@ const version = `${baseVersion}-preview.42.2`;
const sha = "a".repeat(40);
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
const assets = {
macos: [
"macos-aarch64": [
`Buzz_${version}_aarch64.dmg`,
`Buzz_${version}_aarch64.app.tar.gz`,
`Buzz_${version}_aarch64.app.tar.gz.sig`,
],
"macos-x64": [
`Buzz_${version}_x64.dmg`,
`Buzz_${version}_x64.app.tar.gz`,
`Buzz_${version}_x64.app.tar.gz.sig`,
],
windows: [`Buzz_${version}_x64_unsigned.exe`],
linux: [`Buzz_${version}_amd64.deb`, `Buzz_${version}_x86_64.AppImage`],
signatures: [
Expand Down Expand Up @@ -143,7 +148,7 @@ function fixture(t) {
};
}

test("actual assembly command collects all nine assets and emits one complete checksum file", (t) => {
test("actual assembly command collects all twelve assets and emits one complete checksum file", (t) => {
const { dir, run } = fixture(t);
const result = run();
assert.equal(result.status, 0, result.stderr);
Expand All @@ -169,6 +174,7 @@ for (const failure of [
"corrupt",
"mixed version",
"missing platform",
"missing Intel macOS",
"missing signature",
]) {
test(`actual assembly refuses ${failure} before publication`, (t) => {
Expand All @@ -182,6 +188,8 @@ for (const failure of [
writeFileSync(join(windows, "SHA256SUMS"), `${hash(file)} ./${older}\n`);
}
if (failure === "missing platform") rmSync(windows, { recursive: true });
if (failure === "missing Intel macOS")
rmSync(join(dir, "build-assets/macos-x64"), { recursive: true });
if (failure === "missing signature")
rmSync(join(dir, "build-assets/signatures", assets.signatures[2]));
assert.notEqual(run().status, 0);
Expand Down
32 changes: 23 additions & 9 deletions tests/integration/packaging.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -113,20 +113,34 @@ test("all-platform releases preserve candidate isolation and read-only installer
assert.deepEqual(
downloads.map((step) => step.with.path),
[
"build-assets/macos",
"build-assets/macos-aarch64",
"build-assets/macos-x64",
"build-assets/windows",
"build-assets/linux",
"build-assets/signatures",
],
);
for (const [index, job] of [
jobs.build,
jobs.windows,
jobs.linux,
jobs.updater_signatures,
].entries()) {
assert.equal(downloads[index].with.name, job.steps.at(-1).with.name);
}
assert.deepEqual(jobs.build.strategy.matrix.include, [
{ target: "aarch64-apple-darwin", arch: "aarch64", lipo: "arm64" },
{ target: "x86_64-apple-darwin", arch: "x64", lipo: "x86_64" },
]);
const build = jobs.build.steps.find(
(step) => step.name === "Build unsigned app and DMG",
).run;
assert.match(build, /target-libdir --target "\$TARGET"/);
assert.match(build, /pnpm tauri build .*--target "\$TARGET"/);
const macos = jobs.build.steps.at(-1).with.name;
assert.deepEqual(
downloads.map((step) => step.with.name),
[
...jobs.build.strategy.matrix.include.map(({ arch }) =>
macos.replace(/\$\{\{ matrix\.arch \}\}/, arch),
),
...[jobs.windows, jobs.linux, jobs.updater_signatures].map(
(job) => job.steps.at(-1).with.name,
),
],
);
const publication = jobs.publish.steps.at(-1).run;
assert.match(
publication,
Expand Down
Loading
Loading