Files
buzz/scripts/promote-oss-desktop-release.sh
43573d114b ci(release): gate OSS desktop auto-update promotion (#5398)
## Summary

Separate OSS desktop artifact publication from fleet-wide auto-update
promotion.

- retain the exact generated updater manifest as `updater-manifest.json`
on each immutable `desktop-vX.Y.Z` release
- stop the tag-triggered build from mutating
`buzz-desktop-latest/latest.json`
- add a `main`-only manual promotion workflow with one global
concurrency group
- validate stable semver, release/tag commit identity, draft/prerelease
state, exact platform set, signatures, version-bound asset URLs, asset
existence, monotonicity, idempotent retries, and a final stale-state
check before writing
- document the operator flow and pin the split with focused contract
tests

## Safety behavior

Publishing a versioned GitHub release no longer exposes it through the
in-app updater. Operators can install and test those exact
signed/notarized artifacts, then manually run **Promote OSS Desktop
Auto-Update** with the stable version.

Promotion rejects downgrades. A same-version retry succeeds only when
the rolling and candidate manifests are byte-identical. The workflow
re-reads the current rolling version immediately before its only write
and records the actor, source tag commit, previous version, manifest
digest, and run URL.

## Verification

Verified at commit `39caf1603be06bb476905225ec55f7bbbe86b237`:

```text
scripts/test-oss-desktop-promotion.sh
OSS desktop promotion contract passed

scripts/test-release-ref-contract.sh
release ref contract passed

git diff --check origin/main...HEAD
(clean)
```

The repository pre-push hook also passed `branch-skew` for the exact
pushed head; package suites were correctly skipped because this change
only touches release workflows, scripts, and documentation.

Originating conversation: Buzz channel `separate-publish-step-release`,
thread
`8857ce8bbe928e891165eddcf06c666cf6eae16181c3f02a6d8c396d8a536026`.

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-10 09:05:32 -07:00

89 lines
4.7 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
VERSION="${1:-}"
REPOSITORY="${GITHUB_REPOSITORY:-block/buzz}"
TAG="desktop-v${VERSION}"
CANDIDATE="updater-manifest.json"
ROLLING_TAG="buzz-desktop-latest"
EXPECTED_PLATFORMS='["darwin-aarch64","darwin-x86_64","linux-x86_64","windows-x86_64"]'
fail() { echo "::error::$*" >&2; exit 1; }
[[ "$REPOSITORY" == "block/buzz" ]] || fail "promotion is restricted to block/buzz"
[[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || fail "version must be stable semver X.Y.Z"
command -v gh >/dev/null || fail "gh is required"
command -v jq >/dev/null || fail "jq is required"
workdir="$(mktemp -d)"
trap 'rm -rf "$workdir"' EXIT
candidate="$workdir/$CANDIDATE"
current="$workdir/latest.json"
release_json="$(gh release view "$TAG" --repo "$REPOSITORY" --json isDraft,isPrerelease,targetCommitish,assets)"
[[ "$(jq -r .isDraft <<<"$release_json")" == false ]] || fail "$TAG is still a draft"
[[ "$(jq -r .isPrerelease <<<"$release_json")" == false ]] || fail "$TAG is a prerelease"
tag_sha="$(gh api "repos/$REPOSITORY/commits/$TAG" --jq .sha)"
target="$(jq -r .targetCommitish <<<"$release_json")"
target_sha="$(gh api "repos/$REPOSITORY/commits/$target" --jq .sha)"
[[ -n "$tag_sha" && "$target_sha" == "$tag_sha" ]] || fail "$TAG and its release target do not resolve to the same commit"
release_assets="$(jq -r '.assets[].name' <<<"$release_json")"
grep -Fxq "$CANDIDATE" <<<"$release_assets" || fail "$TAG has no $CANDIDATE asset"
gh release download "$TAG" --repo "$REPOSITORY" --pattern "$CANDIDATE" --dir "$workdir"
jq -e --arg version "$VERSION" --argjson expected "$EXPECTED_PLATFORMS" '
.version == $version and
(.platforms | keys == $expected) and
([.platforms[] | (.signature | type == "string" and length > 0)] | all) and
([.platforms[] | (.url | type == "string" and startswith("https://github.com/block/buzz/releases/download/desktop-v" + $version + "/"))] | all)
' "$candidate" >/dev/null || fail "$CANDIDATE failed version, platform, signature, or URL validation"
while IFS= read -r url; do
asset="${url##*/}"
[[ "$url" == "https://github.com/block/buzz/releases/download/$TAG/$asset" ]] || fail "$CANDIDATE contains non-canonical updater URL: $url"
grep -Fxq "$asset" <<<"$release_assets" || fail "$CANDIDATE references missing release asset: $asset"
done < <(jq -r '.platforms[].url' "$candidate")
gh release download "$ROLLING_TAG" --repo "$REPOSITORY" --pattern latest.json --dir "$workdir"
current_digest="$(sha256sum "$current" | awk '{print $1}')"
current_version="$(jq -er '.version | select(type == "string")' "$current")" || fail "current latest.json has no version"
[[ "$current_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || fail "current promoted version is not stable semver: $current_version"
highest="$(printf '%s\n%s\n' "$current_version" "$VERSION" | sort -V | tail -1)"
if [[ "$VERSION" == "$current_version" ]]; then
cmp -s "$candidate" "$current" || fail "$VERSION is already promoted with different manifest content"
echo "Version $VERSION is already promoted with identical manifest content."
exit 0
fi
[[ "$highest" == "$VERSION" ]] || fail "refusing downgrade from $current_version to $VERSION"
# Re-read immediately before the only write so a stale validation cannot silently
# overwrite a promotion performed outside this workflow.
rm -f "$current"
gh release download "$ROLLING_TAG" --repo "$REPOSITORY" --pattern latest.json --dir "$workdir"
[[ "$(sha256sum "$current" | awk '{print $1}')" == "$current_digest" ]] || fail "current promotion changed during validation; retry"
promotion="$workdir/latest.json"
cp "$candidate" "$promotion"
candidate_digest="$(sha256sum "$candidate" | awk '{print $1}')"
if ! gh release upload "$ROLLING_TAG" "$promotion" --repo "$REPOSITORY" --clobber; then
fail "promotion upload failed; latest.json may be temporarily unavailable, retry the promotion"
fi
rm -f "$promotion"
if ! gh release download "$ROLLING_TAG" --repo "$REPOSITORY" --pattern latest.json --dir "$workdir"; then
fail "promotion upload returned success but latest.json could not be verified; retry the promotion"
fi
[[ "$(sha256sum "$promotion" | awk '{print $1}')" == "$candidate_digest" ]] || fail "served latest.json does not match the promoted candidate; retry the promotion"
{
echo "### OSS desktop auto-update promoted"
echo "- Version: \`$VERSION\`"
echo "- Tag commit: \`$tag_sha\`"
echo "- Previous version: \`$current_version\`"
echo "- Manifest SHA-256: \`$(sha256sum "$candidate" | awk '{print $1}')\`"
echo "- Actor: \`${GITHUB_ACTOR:-unknown}\`"
if [[ -n "${GITHUB_SERVER_URL:-}" && -n "${GITHUB_RUN_ID:-}" ]]; then
echo "- Workflow: ${GITHUB_SERVER_URL}/${REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
fi
} >> "${GITHUB_STEP_SUMMARY:-/dev/null}"