mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
fix(release): keep mobile candidates pinned as branches advance (#8454)
Mobile candidate publication currently fails if the source branch advances after selection, and the operator can report failure even after the correct immutable tag was created. It also prevents releasing an explicitly selected historical baseline. Add `--commit FULL_SHA` and independently verify in the operator and publisher that the selected commit belongs to the selected source branch's history. Compare immutable SHAs, keep the candidate pinned through publication, and verify the resulting annotated tag instead of requiring the branch to remain stationary. Stable-branch review/check protection and immutable-tag permissions remain enforced. Without `--commit`, selection still defaults to the current source tip. ### Validation - Updated operator and publisher contracts pass: historical main/stable selection, branch advancement during publication, unrelated or missing commits, ancestry lookup failure, and unchanged tag/sequence checks. - Removing ancestry enforcement causes the unrelated-history regression to fail. - Live read-only ancestry verification accepts the original 0.24 baseline. Local publication stops at the existing App-permission check; no tag or build was created. - Full local `just ci` passed. Signed-off-by: Tom Brow <tomb@block.xyz>
This commit is contained in:
@@ -42,3 +42,17 @@ require_mobile_release_branch_ruleset() {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
# Compare immutable SHAs, not a mutable branch name. The selected branch may
|
||||
# advance after this snapshot without changing the authorized candidate source.
|
||||
require_mobile_release_commit_ancestor() {
|
||||
local target_sha="$1" source_sha="$2" merge_base
|
||||
merge_base="$(gh api "repos/block/buzz/compare/$target_sha...$source_sha" --jq .merge_base_commit.sha)" || {
|
||||
echo "Could not verify candidate source ancestry" >&2
|
||||
return 1
|
||||
}
|
||||
[[ "$merge_base" == "$target_sha" ]] || {
|
||||
echo "Candidate commit $target_sha is not in selected source history at $source_sha" >&2
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@ set -euo pipefail
|
||||
usage() {
|
||||
cat >&2 <<'USAGE'
|
||||
usage:
|
||||
scripts/mobile-release.sh candidate X.Y.Z [--source-branch mobile-vX.Y]
|
||||
scripts/mobile-release.sh candidate X.Y.Z [--source-branch mobile-vX.Y] [--commit FULL_SHA]
|
||||
|
||||
candidate Publish the next immutable mobile-vX.Y.Z-rc.N candidate tag at the
|
||||
exact current remote source commit (main unless --source-branch is set).
|
||||
selected commit in source history (defaults to the current source tip).
|
||||
USAGE
|
||||
exit 2
|
||||
}
|
||||
@@ -94,16 +94,12 @@ remote_tag_commit_sha() {
|
||||
}
|
||||
|
||||
remote_source_commit_sha() {
|
||||
local ref="refs/heads/$MOBILE_RELEASE_SOURCE_BRANCH" line advertised_oid fetched_oid commit
|
||||
line="$(git ls-remote --refs origin "$ref")" || return 1
|
||||
[[ -n "$line" && "$line" != *$'\n'* ]] || return 1
|
||||
advertised_oid="${line%%$'\t'*}"
|
||||
git fetch -q --no-tags origin "$ref"
|
||||
fetched_oid="$(git rev-parse --verify FETCH_HEAD)"
|
||||
[[ "$fetched_oid" == "$advertised_oid" ]] || \
|
||||
fail "origin/$MOBILE_RELEASE_SOURCE_BRANCH moved while it was being resolved"
|
||||
local ref="refs/heads/$MOBILE_RELEASE_SOURCE_BRANCH" fetched_oid commit
|
||||
# Fetch a single snapshot. A later branch advance does not invalidate it.
|
||||
git fetch -q --no-tags origin "$ref" || return 1
|
||||
fetched_oid="$(git rev-parse --verify FETCH_HEAD)" || return 1
|
||||
commit="$(git rev-parse --verify 'FETCH_HEAD^{commit}')" || return 1
|
||||
[[ "$commit" == "$advertised_oid" ]] || \
|
||||
[[ "$commit" == "$fetched_oid" ]] || \
|
||||
fail "origin/$MOBILE_RELEASE_SOURCE_BRANCH did not resolve directly to a commit"
|
||||
printf '%s' "$commit"
|
||||
}
|
||||
@@ -111,13 +107,31 @@ remote_source_commit_sha() {
|
||||
command="${1:-}"
|
||||
case "$command" in
|
||||
candidate)
|
||||
[[ "$#" -eq 2 || "$#" -eq 4 ]] || usage
|
||||
[[ "$#" -ge 2 ]] || usage
|
||||
version="$2"
|
||||
shift 2
|
||||
MOBILE_RELEASE_SOURCE_BRANCH=main
|
||||
if [[ "$#" -eq 4 ]]; then
|
||||
[[ "$3" == --source-branch ]] || usage
|
||||
MOBILE_RELEASE_SOURCE_BRANCH="$4"
|
||||
fi
|
||||
requested_sha=""
|
||||
source_option_seen=false
|
||||
commit_option_seen=false
|
||||
while [[ "$#" -gt 0 ]]; do
|
||||
[[ "$#" -ge 2 ]] || usage
|
||||
case "$1" in
|
||||
--source-branch)
|
||||
[[ "$source_option_seen" == false ]] || usage
|
||||
source_option_seen=true
|
||||
MOBILE_RELEASE_SOURCE_BRANCH="$2"
|
||||
;;
|
||||
--commit)
|
||||
[[ "$commit_option_seen" == false ]] || usage
|
||||
commit_option_seen=true
|
||||
requested_sha="$2"
|
||||
[[ "$requested_sha" =~ ^[0-9a-f]{40}$ ]] || fail "--commit requires a full commit SHA"
|
||||
;;
|
||||
*) usage ;;
|
||||
esac
|
||||
shift 2
|
||||
done
|
||||
require_clean_semver "$version"
|
||||
require_mobile_release_source "$version" "$MOBILE_RELEASE_SOURCE_BRANCH" || exit 1
|
||||
require_clean_tree
|
||||
@@ -126,7 +140,9 @@ case "$command" in
|
||||
require_mobile_release_branch_ruleset "$MOBILE_RELEASE_SOURCE_BRANCH" || exit 1
|
||||
|
||||
local_head_sha="$(git rev-parse --verify 'HEAD^{commit}')" || fail "HEAD is not a commit"
|
||||
source_sha="$(remote_source_commit_sha)" || fail "origin/$MOBILE_RELEASE_SOURCE_BRANCH does not exist"
|
||||
source_tip="$(remote_source_commit_sha)" || fail "origin/$MOBILE_RELEASE_SOURCE_BRANCH does not exist"
|
||||
source_sha="${requested_sha:-$source_tip}"
|
||||
require_mobile_release_commit_ancestor "$source_sha" "$source_tip" || exit 1
|
||||
next=1
|
||||
if ! remote_tags="$(git ls-remote --refs --tags origin "refs/tags/mobile-v${version}-rc.*")"; then
|
||||
fail "could not list existing candidates for $version"
|
||||
@@ -161,17 +177,14 @@ case "$command" in
|
||||
gh run watch "$run_id" --repo block/buzz --exit-status --compact || \
|
||||
fail "App-backed publication failed: $run_url"
|
||||
|
||||
current_source_sha="$(remote_source_commit_sha)" || fail "origin/$MOBILE_RELEASE_SOURCE_BRANCH does not exist after publication"
|
||||
[[ "$current_source_sha" == "$source_sha" ]] || \
|
||||
fail "origin/$MOBILE_RELEASE_SOURCE_BRANCH moved from requested commit $source_sha to $current_source_sha during publication"
|
||||
published_sha="$(remote_tag_commit_sha "refs/tags/$tag")" || \
|
||||
fail "publication completed without exact annotated candidate tag $tag"
|
||||
[[ "$published_sha" == "$source_sha" ]] || \
|
||||
fail "$tag resolved to $published_sha instead of requested commit $source_sha"
|
||||
if [[ "$local_head_sha" != "$source_sha" ]]; then
|
||||
echo "Note: local HEAD is $local_head_sha; candidate source is current origin/$MOBILE_RELEASE_SOURCE_BRANCH $source_sha." >&2
|
||||
echo "Note: local HEAD is $local_head_sha; candidate source is pinned from origin/$MOBILE_RELEASE_SOURCE_BRANCH $source_sha." >&2
|
||||
fi
|
||||
printf 'Published %s at origin/%s commit %s through buzz-release-bot. Use this exact tag in Release Mobile.\n' \
|
||||
printf 'Published %s at pinned origin/%s commit %s through buzz-release-bot. Use this exact tag in Release Mobile.\n' \
|
||||
"$tag" "$MOBILE_RELEASE_SOURCE_BRANCH" "$source_sha"
|
||||
;;
|
||||
|
||||
|
||||
@@ -31,12 +31,12 @@ 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"
|
||||
[[ "$source_sha" =~ ^[0-9a-f]{40}$ ]] || fail "GitHub returned an invalid source commit 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"
|
||||
require_mobile_release_commit_ancestor "$target_sha" "$source_sha" || exit 1
|
||||
|
||||
refs="$(
|
||||
gh api --paginate "repos/$repo/git/matching-refs/tags/mobile-v${version}-rc." --jq '.[].ref'
|
||||
|
||||
@@ -35,6 +35,21 @@ case "${1:-}:${2:-}" in
|
||||
api:repos/block/buzz/git/ref/heads/main) printf '%s\n' "$GH_TARGET_SHA" ;;
|
||||
api:repos/block/buzz/git/ref/heads/mobile-v1.2) printf '%s\n' "$GH_STABLE_SHA" ;;
|
||||
api:repos/block/buzz/git/ref/heads/*) printf '%s\n' "$GH_MISMATCH_SHA" ;;
|
||||
api:repos/block/buzz/compare/*)
|
||||
[[ "${GH_COMPARE_FAIL:-}" != 1 ]] || exit 1
|
||||
pair="${2##*/}"
|
||||
base="${pair%%...*}"; tip="${pair##*...}"
|
||||
if [[ -n "${GH_EXPECT_COMPARE:-}" ]]; then
|
||||
[[ "$pair" == "$GH_EXPECT_COMPARE" ]] || exit 1
|
||||
fi
|
||||
if [[ -n "${GH_COMPARE_BASE_SHA:-}" ]]; then
|
||||
printf '%s\n' "$GH_COMPARE_BASE_SHA"
|
||||
elif [[ "$base" == "$tip" ]]; then
|
||||
printf '%s\n' "$base"
|
||||
else
|
||||
printf '%s\n' 9999999999999999999999999999999999999999
|
||||
fi
|
||||
;;
|
||||
api:repos/block/buzz/commits/*) printf '%s\n' "${2##*/}" ;;
|
||||
api:--paginate)
|
||||
[[ "$3" == "repos/block/buzz/git/matching-refs/tags/mobile-v1.2.3-rc." ]]
|
||||
@@ -116,8 +131,9 @@ if GH_TAG_EXCLUDES=refs/tags/mobile-v0.0.0 "$publisher" 1.2.3 1 "$GH_TARGET_SHA"
|
||||
exit 1
|
||||
fi
|
||||
if GH_TARGET_SHA=3333333333333333333333333333333333333333 \
|
||||
GH_ANNOTATED_TARGET_SHA=1111111111111111111111111111111111111111 \
|
||||
"$publisher" 1.2.3 1 1111111111111111111111111111111111111111 main >/dev/null 2>&1; then
|
||||
echo "publisher accepted a moved main branch" >&2
|
||||
echo "publisher accepted a commit outside main history" >&2
|
||||
exit 1
|
||||
fi
|
||||
if GH_EXISTING_REFS=$'refs/tags/mobile-v1.2.3-rc.1\n' \
|
||||
@@ -164,6 +180,25 @@ if GITHUB_REPOSITORY=attacker/buzz "$publisher" 1.2.3 1 "$GH_TARGET_SHA" main >/
|
||||
fi
|
||||
|
||||
GH_ANNOTATED_TARGET_SHA="$GH_STABLE_SHA" "$publisher" 1.2.3 1 "$GH_STABLE_SHA" mobile-v1.2
|
||||
# Old commits are accepted only when the immutable compare proves ancestry.
|
||||
old_sha=6666666666666666666666666666666666666666
|
||||
for source in main mobile-v1.2; do
|
||||
tip="$GH_TARGET_SHA"
|
||||
[[ "$source" != mobile-v1.2 ]] || tip="$GH_STABLE_SHA"
|
||||
GH_COMPARE_BASE_SHA="$old_sha" GH_EXPECT_COMPARE="$old_sha...$tip" GH_ANNOTATED_TARGET_SHA="$old_sha" \
|
||||
"$publisher" 1.2.3 1 "$old_sha" "$source"
|
||||
before_calls="$(wc -l < "$GH_CALLS")"
|
||||
for mode in unrelated unavailable; do
|
||||
if GH_COMPARE_FAIL="$([[ "$mode" == unavailable ]] && echo 1 || echo 0)" \
|
||||
GH_COMPARE_BASE_SHA="$GH_MISMATCH_SHA" GH_ANNOTATED_TARGET_SHA="$old_sha" \
|
||||
"$publisher" 1.2.3 1 "$old_sha" "$source" >/dev/null 2>&1; then
|
||||
echo "publisher accepted $mode ancestry for $source" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
[[ "$(wc -l < "$GH_CALLS")" == "$before_calls" ]]
|
||||
done
|
||||
|
||||
for source in mobile-v1.3 mobile-v2.2 mobile-v01.2 arbitrary ''; do
|
||||
if GH_ANNOTATED_TARGET_SHA="$GH_MISMATCH_SHA" "$publisher" 1.2.3 1 "$GH_MISMATCH_SHA" "$source" >/dev/null 2>&1; then
|
||||
echo "publisher accepted mismatched source '$source'" >&2
|
||||
|
||||
@@ -32,7 +32,13 @@ case "${1:-}:${2:-}" in
|
||||
*) exit 2 ;;
|
||||
esac
|
||||
;;
|
||||
api:repos/block/buzz/compare/*)
|
||||
[[ "${GH_COMPARE_FAIL:-}" != 1 ]] || exit 1
|
||||
pair="${2##*/}"
|
||||
git --git-dir="$GH_REMOTE" merge-base "${pair%%...*}" "${pair##*...}"
|
||||
;;
|
||||
workflow:run)
|
||||
printf '%s\n' attempted >> "$GH_WORKFLOW_ATTEMPTS"
|
||||
if [[ "${GH_WORKFLOW_DISPATCH_FAIL:-}" == "1" ]]; then
|
||||
printf '%s\n' "${GH_WORKFLOW_DISPATCH_ERROR:-dispatch failed}" >&2
|
||||
exit 1
|
||||
@@ -58,7 +64,7 @@ case "${1:-}:${2:-}" in
|
||||
shift
|
||||
done
|
||||
[[ -n "$version" && -n "$number" && -n "$sha" && -n "$source_branch" ]] || exit 1
|
||||
[[ "$(git --git-dir="$GH_REMOTE" rev-parse "refs/heads/$source_branch")" == "$sha" ]] || exit 1
|
||||
git --git-dir="$GH_REMOTE" merge-base --is-ancestor "$sha" "refs/heads/$source_branch" || exit 1
|
||||
printf '%s\t%s\t%s\n' "$version" "$number" "$sha" >> "$GH_WORKFLOW_CAPTURE"
|
||||
if [[ "${GH_WORKFLOW_NO_URL:-}" == "1" ]]; then
|
||||
exit 0
|
||||
@@ -93,6 +99,7 @@ chmod +x "$bin/gh"
|
||||
|
||||
export PATH="$bin:$PATH"
|
||||
export GH_WORKFLOW_CAPTURE="$tmp/workflow-dispatches"
|
||||
export GH_WORKFLOW_ATTEMPTS="$tmp/workflow-attempts"
|
||||
export GH_WORKTREE="$work"
|
||||
export GH_REMOTE="$remote"
|
||||
|
||||
@@ -150,7 +157,7 @@ if git -C "$operator" cat-file -e "$remote_main_sha^{commit}" 2>/dev/null; then
|
||||
fail "stale-clone fixture already contains the remote-only commit"
|
||||
fi
|
||||
run_release "$operator" candidate 1.2.3 > "$tmp/stale-output" 2> "$tmp/stale-error"
|
||||
grep -Fq "Note: local HEAD is $(git -C "$operator" rev-parse HEAD); candidate source is current origin/main $remote_main_sha." \
|
||||
grep -Fq "Note: local HEAD is $(git -C "$operator" rev-parse HEAD); candidate source is pinned from origin/main $remote_main_sha." \
|
||||
"$tmp/stale-error"
|
||||
cat "$tmp/stale-output"
|
||||
[[ "$(git --git-dir="$remote" rev-parse 'refs/tags/mobile-v1.2.3-rc.1^{commit}')" == \
|
||||
@@ -262,19 +269,40 @@ if GH_TAG_VERIFY_LIGHTWEIGHT=1 run_release "$operator" candidate 9.9.2 >/dev/nul
|
||||
fi
|
||||
[[ "$(git --git-dir="$remote" cat-file -t refs/tags/mobile-v9.9.2-rc.1)" == commit ]]
|
||||
|
||||
# The publisher and operator both reject a main-tip race. The immutable tag may
|
||||
# already exist at the prior tip, but the operator must not report it as a
|
||||
# current-main candidate.
|
||||
# Advancing main during publication must not invalidate the pinned tag.
|
||||
pre_race_main_sha="$(git --git-dir="$remote" rev-parse refs/heads/main)"
|
||||
if GH_MAIN_MOVE_DURING_WORKFLOW=1 run_release "$operator" candidate 9.9.3 > "$tmp/main-race-output" 2>&1; then
|
||||
fail "candidate succeeded after main moved during publication"
|
||||
fi
|
||||
GH_MAIN_MOVE_DURING_WORKFLOW=1 run_release "$operator" candidate 9.9.3 > "$tmp/main-race-output" 2>&1
|
||||
post_race_main_sha="$(git --git-dir="$remote" rev-parse refs/heads/main)"
|
||||
[[ "$pre_race_main_sha" != "$post_race_main_sha" ]]
|
||||
[[ "$(git --git-dir="$remote" rev-parse 'refs/tags/mobile-v9.9.3-rc.1^{commit}')" == \
|
||||
"$pre_race_main_sha" ]]
|
||||
grep -Fq "origin/main moved from requested commit $pre_race_main_sha to $post_race_main_sha during publication" \
|
||||
"$tmp/main-race-output"
|
||||
[[ "$(git --git-dir="$remote" rev-parse 'refs/tags/mobile-v9.9.3-rc.1^{commit}')" == "$pre_race_main_sha" ]]
|
||||
grep -Fq 'Published mobile-v9.9.3-rc.1' "$tmp/main-race-output"
|
||||
|
||||
# Explicit historical source remains pinned even when the source branch advances.
|
||||
run_release "$operator" candidate 7.7.0 --commit "$remote_main_sha"
|
||||
[[ "$(git --git-dir="$remote" rev-parse 'refs/tags/mobile-v7.7.0-rc.1^{commit}')" == "$remote_main_sha" ]]
|
||||
run_release "$operator" candidate 1.2.5 --commit "$remote_main_sha" --source-branch mobile-v1.2
|
||||
[[ "$(git --git-dir="$remote" rev-parse 'refs/tags/mobile-v1.2.5-rc.1^{commit}')" == "$remote_main_sha" ]]
|
||||
before_attempts="$(wc -l < "$GH_WORKFLOW_ATTEMPTS")"
|
||||
before_dispatches="$(wc -l < "$GH_WORKFLOW_CAPTURE")"
|
||||
if run_release "$operator" candidate 7.7.1 --commit "$stable_sha" >/dev/null 2>&1; then
|
||||
fail "accepted a commit outside main history"
|
||||
fi
|
||||
if run_release "$operator" candidate 1.2.6 --source-branch mobile-v1.2 --commit "$new_main_sha" >/dev/null 2>&1; then
|
||||
fail "accepted a main-only commit outside stable history"
|
||||
fi
|
||||
for invalid_sha in main HEAD deadbeef 9999999999999999999999999999999999999999 ''; do
|
||||
if run_release "$operator" candidate 7.7.1 --commit "$invalid_sha" >/dev/null 2>&1; then
|
||||
fail "accepted invalid or absent commit '$invalid_sha'"
|
||||
fi
|
||||
done
|
||||
if GH_COMPARE_FAIL=1 run_release "$operator" candidate 7.7.1 --commit "$remote_main_sha" >/dev/null 2>&1; then
|
||||
fail "accepted unavailable ancestry evidence"
|
||||
fi
|
||||
if run_release "$operator" candidate 7.7.1 --commit "$remote_main_sha" --commit "$new_main_sha" >/dev/null 2>&1; then
|
||||
fail "accepted duplicate commit options"
|
||||
fi
|
||||
[[ "$(wc -l < "$GH_WORKFLOW_ATTEMPTS")" == "$before_attempts" ]]
|
||||
[[ "$(wc -l < "$GH_WORKFLOW_CAPTURE")" == "$before_dispatches" ]]
|
||||
|
||||
# Publishing through a fork is rejected and unsupported gh versions fail before
|
||||
# dispatching any candidate publication.
|
||||
|
||||
Reference in New Issue
Block a user