mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
## 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>
89 lines
4.7 KiB
Bash
Executable File
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}"
|