Files
buzz/scripts/publish-mobile-release-candidate.sh
Tom Brow 30ca980b49 feat(release): support version-matched mobile stable branches (#8430)
Mobile candidates currently require the current `main` commit, so a
stable release branch cannot publish through the existing protected tag
workflow.

Add optional `--source-branch mobile-vMAJOR.MINOR` selection while
preserving `main` as the default. Both the command and App-backed
publisher reject a stable branch whose major/minor differs from the
requested version. Candidates retain exact-tip checks and immutable
annotated tags; the private build pipeline is unchanged.

Document the temporary stable-only distribution policy and explicit
per-fix backport decisions. No candidate, build, or upload is triggered
by this change. No duplicate implementation was found among open release
PRs.

Stable publication now checks active review and status-check protection
in both the operator and publisher. The process permits approved
backport PRs and stable-only corrections for regressions caused by
earlier backports. Administrator activation and verification of the
no-bypass ruleset remain outstanding; non-admin API responses hide the
bypass actor list.

### Validation

- Full local `just ci` passed during preparation; the final changes
affect shell release tooling, its workflow inputs, CI path detection,
and documentation.
- Publisher contract passed, including version-matched stable sources
and mismatched branch rejection.
- Operator contract covers default main, a divergent stable commit,
missing or mismatched sources, immutable sequencing, and publication
races. The final operator contract run passed.


- Updated publisher and operator contracts pass, including 17
weakened-policy cases and rejection of unavailable protection. The live,
currently unprotected stable branch is rejected.

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
2026-10-18 18:04:04 -07:00

86 lines
3.7 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
fail() {
echo "Error: $*" >&2
exit 1
}
# shellcheck source=scripts/release-rulesets.sh
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/release-rulesets.sh"
# shellcheck source=scripts/mobile-release-source.sh
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/mobile-release-source.sh"
[[ "$#" -eq 4 ]] || fail "usage: $0 X.Y.Z N COMMIT_SHA SOURCE_BRANCH"
version="$1"
candidate_number="$2"
target_sha="$3"
MOBILE_RELEASE_SOURCE_BRANCH="$4"
repo="${GITHUB_REPOSITORY:-}"
[[ "$repo" == "block/buzz" ]] || fail "candidate publishing is restricted to block/buzz"
[[ "$version" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]] || \
fail "'$version' is not a mobile release version (expected X.Y.Z)"
[[ "$candidate_number" =~ ^[1-9][0-9]*$ ]] || \
fail "'$candidate_number' is not a candidate number (expected N >= 1)"
[[ "$target_sha" =~ ^[0-9a-f]{40}$ ]] || fail "'$target_sha' is not a full commit SHA"
require_mobile_release_source "$version" "$MOBILE_RELEASE_SOURCE_BRANCH" || exit 1
command -v gh >/dev/null 2>&1 || fail "gh is required"
require_release_tag_ruleset || exit 1
require_mobile_release_branch_ruleset "$MOBILE_RELEASE_SOURCE_BRANCH" || exit 1
source_sha="$(gh api "repos/$repo/git/ref/heads/$MOBILE_RELEASE_SOURCE_BRANCH" --jq .object.sha)" || \
fail "could not resolve $repo $MOBILE_RELEASE_SOURCE_BRANCH"
[[ "$source_sha" == "$target_sha" ]] || \
fail "$repo $MOBILE_RELEASE_SOURCE_BRANCH moved from requested commit $target_sha to $source_sha"
commit_sha="$(gh api "repos/$repo/commits/$target_sha" --jq .sha)" || \
fail "$target_sha is not a commit in $repo"
[[ "$commit_sha" == "$target_sha" ]] || \
fail "GitHub resolved requested commit $target_sha as $commit_sha"
refs="$(
gh api --paginate "repos/$repo/git/matching-refs/tags/mobile-v${version}-rc." --jq '.[].ref'
)" || fail "could not list existing candidates for $version"
next=1
while IFS= read -r ref; do
[[ -n "$ref" ]] || continue
[[ "$ref" =~ ^refs/tags/mobile-v${version//./\.}-rc\.([1-9][0-9]*)$ ]] || continue
number="${BASH_REMATCH[1]}"
(( number >= next )) && next=$((number + 1))
done <<< "$refs"
[[ "$candidate_number" -eq "$next" ]] || \
fail "candidate sequence changed; expected rc.$candidate_number but next is rc.$next"
tag="mobile-v${version}-rc.${candidate_number}"
message="Buzz Mobile $version release candidate $candidate_number"
tag_object_sha="$(
gh api --method POST "repos/$repo/git/tags" \
-f tag="$tag" \
-f message="$message" \
-f object="$target_sha" \
-f type=commit \
--jq .sha
)" || fail "could not create annotated tag object for $tag"
[[ "$tag_object_sha" =~ ^[0-9a-f]{40}$ ]] || fail "GitHub returned an invalid tag object SHA"
gh api --method POST "repos/$repo/git/refs" \
-f ref="refs/tags/$tag" \
-f sha="$tag_object_sha" \
--silent || fail "could not publish $tag"
published_type="$(gh api "repos/$repo/git/ref/tags/$tag" --jq .object.type)" || \
fail "could not verify published tag $tag"
published_object="$(gh api "repos/$repo/git/ref/tags/$tag" --jq .object.sha)" || \
fail "could not verify published tag $tag"
[[ "$published_type" == "tag" && "$published_object" == "$tag_object_sha" ]] || \
fail "$tag does not reference the expected annotated tag object"
direct_type="$(gh api "repos/$repo/git/tags/$tag_object_sha" --jq .object.type)" || \
fail "could not verify annotated tag object $tag_object_sha"
direct_sha="$(gh api "repos/$repo/git/tags/$tag_object_sha" --jq .object.sha)" || \
fail "could not verify annotated tag object $tag_object_sha"
[[ "$direct_type" == "commit" && "$direct_sha" == "$target_sha" ]] || \
fail "$tag does not point directly to requested commit $target_sha"
printf 'Published %s at %s through buzz-release-bot.\n' "$tag" "$target_sha"