feat(release): make desktop releases immutable (#3568)

## Summary
- add a manual desktop release preparer that regenerates one
version-only candidate from current `origin/main`
- validate deterministic complete changelog accounting, candidate
authorship, allowed files, exact-head approval, required checks, and
two-parent merge topology before tagging the reviewed candidate
- move desktop tags/releases from `v*` to `desktop-v*` while preserving
relay, chart, push-chart, and mobile behavior
- stage all four platform outputs in Actions artifacts and grant GitHub
release write access only to one final all-platform-gated publisher
- publish the versioned release only after complete artifact assembly;
update stable `latest.json` last; never promote prereleases or published
rebuild outputs

## Safety properties
- desktop tags point to the reviewed candidate SHA, not the merge commit
- release builds remain tag-bound and reverify tag == checked-out HEAD
- one final writer fails closed on artifact basename collisions
- per-tag concurrency serializes publication without cancellation
- published reruns do not replace immutable versioned assets or promote
signatures from a rebuild
- candidate branches use an explicit remote OID lease when regenerated

## Validation
- `scripts/test-desktop-release-candidate.sh`
- `scripts/test-release-ref-contract.sh`
- `scripts/test-mobile-release-contract.sh`
- changed workflow YAML parsing (Ruby Psych)
- changed shell syntax (`bash -n`)
- `git diff --check`
- push hooks: branch-skew, Rust workspace tests (1,853 passed), desktop
Tauri tests (3 passed)

## Coordinated companion
- squareup/buzz-releases#79 updates the manually entered desktop
source-tag contract to stable-only `desktop-v*`
- merge the private contract companion before the first namespaced
desktop release

## Rollout blockers (no settings changed here)
Before the first candidate/release:
1. enable merge commits in repository settings
2. allow `merge` in ruleset `13596885`
3. require approval after the last push in ruleset `13596885`
4. include `refs/tags/desktop-v*` explicitly in release ruleset
`14378754`
5. prove the non-publishing candidate/merge/tag/artifact validation path
before any production release

Do not test the old workflow with a prerelease: it can still mutate the
production rolling updater release.

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
This commit is contained in:
Wes
2026-07-30 11:27:56 -07:00
committed by GitHub
co-authored by Carl
parent cca8839034
commit 1dfd89ea67
13 changed files with 763 additions and 198 deletions
@@ -4,7 +4,7 @@ name: Auto-tag on Release PR Merge
# 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 v<v> → release.yml (desktop app)
# 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
@@ -35,6 +35,11 @@ permissions:
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
@@ -57,7 +62,7 @@ jobs:
case "$BRANCH" in
version-bump/*)
VERSION="${BRANCH#version-bump/}"
TAG_PREFIX="v" ;;
TAG_PREFIX="desktop-v" ;;
relay-release/*)
VERSION="${BRANCH#relay-release/}"
TAG_PREFIX="relay-v" ;;
@@ -85,9 +90,34 @@ jobs:
{
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 }}
PR_PUSHER: ${{ github.event.pull_request.head.user.login }}
MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }}
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
@@ -102,21 +132,22 @@ jobs:
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" = "$GITHUB_SHA" ]; then
echo "Tag $TAG already exists at $GITHUB_SHA — skipping tag creation"
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 $GITHUB_SHA)"
echo "::error::Tag $TAG already exists at $EXISTING_SHA (expected $TARGET_SHA)"
exit 1
fi
fi
gh api --method POST "repos/$GITHUB_REPOSITORY/git/refs" \
-f ref="refs/tags/$TAG" \
-f sha="$GITHUB_SHA" \
-f sha="$TARGET_SHA" \
--silent
+2
View File
@@ -76,6 +76,8 @@ jobs:
- '.github/workflows/ci.yml'
- name: Release workflow source contract
run: scripts/test-release-ref-contract.sh
- name: Desktop release candidate contract
run: scripts/test-desktop-release-candidate.sh
- name: Mobile release contract
run: |
scripts/test-mobile-release-contract.sh
@@ -0,0 +1,38 @@
name: Prepare Desktop Release
on:
workflow_dispatch:
inputs:
version:
description: Semver to prepare (for example 0.5.1)
required: true
env:
RELEASE_AUTOMATION_NAME: Carl
RELEASE_AUTOMATION_EMAIL: c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz
jobs:
prepare:
if: github.repository == 'block/buzz'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Create short-lived release preparer token
id: preparer
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
permission-pull-requests: write
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
token: ${{ steps.preparer.outputs.token }}
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
- name: Prepare immutable candidate and open or update PR
env:
GH_TOKEN: ${{ steps.preparer.outputs.token }}
VERSION: ${{ inputs.version }}
run: scripts/prepare-desktop-release.sh "$VERSION"
+144 -174
View File
@@ -1,14 +1,13 @@
name: Release
concurrency:
group: desktop-release-${{ github.ref }}
cancel-in-progress: false
on:
push:
tags:
- 'v[0-9]*'
workflow_dispatch:
inputs:
version:
description: "Semver version matching the v-prefixed dispatch tag"
required: true
- 'desktop-v[0-9]*'
jobs:
# Shared setup: verify the immutable release tag, determine the version, and
@@ -19,23 +18,14 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
contents: read
outputs:
version: ${{ steps.version.outputs.version }}
source_sha: ${{ steps.source.outputs.source_sha }}
steps:
- name: Determine version
id: version
env:
EVENT_NAME: ${{ github.event_name }}
INPUT_VERSION: ${{ inputs.version }}
run: |
if [[ "$EVENT_NAME" == "push" ]]; then
VERSION="${GITHUB_REF_NAME#v}"
else
VERSION="$INPUT_VERSION"
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
run: echo "version=${GITHUB_REF_NAME#desktop-v}" >> "$GITHUB_OUTPUT"
- name: Validate version
env:
@@ -56,42 +46,9 @@ jobs:
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
scripts/verify-release-ref.sh v "$VERSION"
scripts/verify-release-ref.sh desktop-v "$VERSION"
echo "source_sha=$(git rev-parse 'HEAD^{commit}')" >> "$GITHUB_OUTPUT"
- name: Create versioned GitHub release
env:
VERSION: ${{ steps.version.outputs.version }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
RELEASE_SHA=$(git rev-parse HEAD)
NOTES=""
if [[ -f CHANGELOG.md ]]; then
NOTES=$(awk "/^## v${VERSION}\$/{found=1; next} found && /^## v/{exit} found && !/^\$/" CHANGELOG.md)
fi
if [[ -z "$NOTES" ]]; then
NOTES="Buzz Desktop v${VERSION}"
fi
PRERELEASE_FLAGS=()
if [[ "$VERSION" =~ -(test|alpha|beta|rc)([.-]|$) ]]; then
PRERELEASE_FLAGS=(--prerelease --latest=false)
fi
gh release create "v${VERSION}" \
--target "$RELEASE_SHA" \
--title "Buzz Desktop v${VERSION}" \
--notes "$NOTES" \
"${PRERELEASE_FLAGS[@]}"
- name: Create rolling auto-update release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create buzz-desktop-latest \
--prerelease \
--title "Buzz Desktop Auto-Update" \
--notes "Rolling release for the Tauri auto-updater. Do not download manually — use the versioned release instead." \
2>/dev/null || true
release:
name: Release
if: github.repository == 'block/buzz'
@@ -99,7 +56,7 @@ jobs:
needs: setup
timeout-minutes: 60
permissions:
contents: write
contents: read
id-token: write # required by block/apple-codesign-action for OIDC
outputs:
archive_name: ${{ steps.artifacts.outputs.archive_name }}
@@ -114,7 +71,7 @@ jobs:
persist-credentials: false
- name: Verify tag-bound release source
run: scripts/verify-release-ref.sh v "$VERSION"
run: scripts/verify-release-ref.sh desktop-v "$VERSION"
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
@@ -272,13 +229,19 @@ jobs:
fi
echo "dmg=$DMG" >> "$GITHUB_OUTPUT"
# Find the updater .tar.gz and .sig
# 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"
@@ -289,23 +252,15 @@ jobs:
env:
SIG_PATH: ${{ steps.artifacts.outputs.sig }}
- name: Upload arm64 DMG to versioned GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DMG_PATH: ${{ steps.artifacts.outputs.dmg }}
run: gh release upload "v${VERSION}" "$DMG_PATH" --clobber
- name: Upload updater archive to rolling release
if: github.ref == format('refs/tags/v{0}', needs.setup.outputs.version)
run: |
gh release upload buzz-desktop-latest \
"$ARCHIVE_PATH" \
"$SIG_PATH" \
--clobber
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ARCHIVE_PATH: ${{ steps.artifacts.outputs.archive }}
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)
@@ -314,7 +269,7 @@ jobs:
needs: setup
timeout-minutes: 60
permissions:
contents: write
contents: read
id-token: write # required by block/apple-codesign-action for OIDC
outputs:
archive_name: ${{ steps.artifacts.outputs.archive_name }}
@@ -330,7 +285,7 @@ jobs:
persist-credentials: false
- name: Verify tag-bound release source
run: scripts/verify-release-ref.sh v "$VERSION"
run: scripts/verify-release-ref.sh desktop-v "$VERSION"
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
@@ -443,6 +398,11 @@ jobs:
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"
@@ -453,23 +413,15 @@ jobs:
env:
SIG_PATH: ${{ steps.artifacts.outputs.sig }}
- name: Upload Intel DMG to versioned GitHub release
run: gh release upload "v${VERSION}" "$DMG_PATH" --clobber
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DMG_PATH: ${{ steps.unsigned.outputs.dmg }}
- name: Upload updater archive to rolling release
if: github.ref == format('refs/tags/v{0}', needs.setup.outputs.version)
run: |
gh release upload buzz-desktop-latest \
"$ARCHIVE_PATH" \
"$SIG_PATH" \
--clobber
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ARCHIVE_PATH: ${{ steps.artifacts.outputs.archive }}
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
@@ -480,7 +432,7 @@ jobs:
needs: setup
timeout-minutes: 60
permissions:
contents: write
contents: read
env:
# AppImage tools (linuxdeploy, appimagetool) are themselves AppImages.
# Containers lack FUSE, so we must use the extract-and-run fallback.
@@ -555,7 +507,7 @@ jobs:
- name: Verify tag-bound release source
env:
VERSION: ${{ needs.setup.outputs.version }}
run: scripts/verify-release-ref.sh v "$VERSION"
run: scripts/verify-release-ref.sh desktop-v "$VERSION"
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
@@ -689,29 +641,16 @@ jobs:
SIG_PATH: ${{ steps.linux-artifacts.outputs.sig }}
# NOTE: .deb is NOT auto-updatable (Tauri updater constraint — only AppImage supports it on Linux)
- name: Upload Linux artifacts to versioned GitHub release
env:
VERSION: ${{ needs.setup.outputs.version }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DEB_PATH: ${{ steps.linux-artifacts.outputs.deb }}
APPIMAGE_PATH: ${{ steps.linux-artifacts.outputs.appimage }}
run: |
gh release upload "v$VERSION" \
"$DEB_PATH" \
"$APPIMAGE_PATH" \
--clobber
- name: Upload updater archive to rolling release
if: github.ref == format('refs/tags/v{0}', needs.setup.outputs.version)
run: |
gh release upload buzz-desktop-latest \
"$ARCHIVE_PATH" \
"$SIG_PATH" \
--clobber
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ARCHIVE_PATH: ${{ steps.linux-artifacts.outputs.archive }}
SIG_PATH: ${{ steps.linux-artifacts.outputs.sig }}
- 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
@@ -719,7 +658,7 @@ jobs:
needs: setup
timeout-minutes: 60
permissions:
contents: write
contents: read
outputs:
archive_name: ${{ steps.artifacts.outputs.archive_name }}
sig: ${{ steps.read-sig.outputs.sig }}
@@ -735,7 +674,7 @@ jobs:
- name: Verify tag-bound release source
shell: bash
run: scripts/verify-release-ref.sh v "$VERSION"
run: scripts/verify-release-ref.sh desktop-v "$VERSION"
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
with:
@@ -745,7 +684,7 @@ jobs:
with:
node-version: 24.14.1
# Disable dependency caching: a writable cache in this release workflow
# (contents: write, feeds a signed installer) is a poisoning vector. pnpm
# (contents: read, feeds a signed installer) is a poisoning vector. pnpm
# install runs uncached below.
package-manager-cache: false
@@ -827,25 +766,14 @@ jobs:
env:
SIG_PATH: ${{ steps.artifacts.outputs.sig }}
- name: Upload Windows installer to versioned GitHub release
shell: bash
run: gh release upload "v${VERSION}" "$EXE_PATH" --clobber
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
EXE_PATH: ${{ steps.artifacts.outputs.exe }}
- name: Upload updater archive to rolling release
if: github.ref == format('refs/tags/v{0}', needs.setup.outputs.version)
shell: bash
run: |
gh release upload buzz-desktop-latest \
"$ARCHIVE_PATH" \
"$SIG_PATH" \
--clobber
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ARCHIVE_PATH: ${{ steps.artifacts.outputs.archive }}
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
@@ -853,7 +781,11 @@ jobs:
if: |
always() &&
needs.setup.result == 'success' &&
github.ref == format('refs/tags/v{0}', needs.setup.outputs.version)
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
@@ -870,7 +802,26 @@ jobs:
persist-credentials: false
- name: Verify tag-bound release source
run: scripts/verify-release-ref.sh v "$VERSION"
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:
@@ -899,7 +850,7 @@ jobs:
write_sig "$RESULT_LINUX" linux-x86_64 "$SIG_LINUX"
write_sig "$RESULT_WIN" windows-x86_64 "$SIG_WIN"
- name: Verify archive URLs are accessible
- name: Verify draft release has every updater archive
env:
RESULT_ARM64: ${{ needs.release.result }}
RESULT_X64: ${{ needs.release-macos-x64.result }}
@@ -911,39 +862,19 @@ jobs:
ARCHIVE_WIN: ${{ needs.release-windows.outputs.archive_name }}
run: |
set -euo pipefail
BASE="https://github.com/block/buzz/releases/download/buzz-desktop-latest"
ARCHIVES=()
add_archive() {
local result="$1" platform="$2" archive="$3"
if [[ "$result" == "success" ]]; then
[[ -n "$archive" ]] || { echo "::error::Missing archive name for successful platform: $platform"; exit 1; }
ARCHIVES+=("$archive")
fi
}
add_archive "$RESULT_ARM64" darwin-aarch64 "$ARCHIVE_ARM64"
add_archive "$RESULT_X64" darwin-x86_64 "$ARCHIVE_X64"
add_archive "$RESULT_LINUX" linux-x86_64 "$ARCHIVE_LINUX"
add_archive "$RESULT_WIN" windows-x86_64 "$ARCHIVE_WIN"
for name in "${ARCHIVES[@]}"; do
echo "Checking $BASE/$name ..."
success=false
for attempt in 1 2 3; do
if curl -fsI "$BASE/$name" > /dev/null 2>&1; then
success=true
break
fi
echo "Attempt $attempt failed for $name, retrying in 10s..."
sleep 10
done
if [ "$success" != "true" ]; then
echo "::error::Archive not accessible after 3 attempts: $BASE/$name"
exit 1
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
echo "All archive URLs verified."
- name: Generate unified latest.json
env:
@@ -957,7 +888,7 @@ jobs:
ARCHIVE_WIN: ${{ needs.release-windows.outputs.archive_name }}
run: |
set -euo pipefail
BASE="https://github.com/block/buzz/releases/download/buzz-desktop-latest"
BASE="https://github.com/block/buzz/releases/download/desktop-v${VERSION}"
TRIPLES=()
add_triple() {
@@ -977,6 +908,45 @@ jobs:
bash desktop/scripts/generate-oss-latest-json.sh "$VERSION" "${TRIPLES[@]}" > latest.json
cat latest.json
- name: Upload latest.json to rolling release
- name: Create or verify versioned draft
run: |
gh release upload buzz-desktop-latest latest.json --clobber
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: ${{ env.already_published != 'true' && !contains(needs.setup.outputs.version, '-') }}
run: gh release upload buzz-desktop-latest latest.json --clobber