mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
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>
267 lines
12 KiB
Bash
Executable File
267 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
publisher="$repo_root/scripts/publish-mobile-release-candidate.sh"
|
|
tmp="$(mktemp -d)"
|
|
trap 'rm -rf "$tmp"' EXIT
|
|
|
|
bin="$tmp/bin"
|
|
mkdir -p "$bin"
|
|
cat > "$bin/gh" <<'GH'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
record() {
|
|
printf '%s\n' "$*" >> "$GH_CALLS"
|
|
}
|
|
|
|
case "${1:-}:${2:-}" in
|
|
api:repos/block/buzz/rulesets/19321162)
|
|
[[ "${GH_BRANCH_POLICY_FAIL:-}" != 1 ]] || exit 1
|
|
policy='{"target":"branch","enforcement":"active","current_user_can_bypass":"never","conditions":{"ref_name":{"include":["refs/heads/mobile-v*"],"exclude":[]}},"bypass_actors":[],"rules":[{"type":"deletion"},{"type":"non_fast_forward"},{"type":"pull_request","parameters":{"required_approving_review_count":1,"dismiss_stale_reviews_on_push":true,"require_last_push_approval":true,"required_review_thread_resolution":true}},{"type":"required_status_checks","parameters":{"strict_required_status_checks_policy":true,"required_status_checks":[{"context":"Mobile"},{"context":"DCO Check"}]}}]}'
|
|
printf '%s\n' "${GH_BRANCH_POLICY:-$policy}" | jq -r "$4"
|
|
;;
|
|
api:repos/block/buzz/rulesets/14378754)
|
|
case "$*" in
|
|
*'.enforcement'*) printf '%s\n' "${GH_TAG_RULESET_STATE:-active}" ;;
|
|
*'.current_user_can_bypass'*) printf '%s\n' "${GH_CURRENT_USER_CAN_BYPASS-always}" ;;
|
|
*'[.rules[].type]'*) printf '%s\n' "${GH_TAG_RULE_TYPES:-creation,deletion,non_fast_forward,update}" ;;
|
|
*'.conditions.ref_name.include[]'*) printf '%s\n' "${GH_TAG_INCLUDES:-refs/tags/mobile-v*}" ;;
|
|
*'.conditions.ref_name.exclude[]'*) printf '%s\n' "${GH_TAG_EXCLUDES:-}" ;;
|
|
*) exit 2 ;;
|
|
esac
|
|
;;
|
|
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." ]]
|
|
printf '%s' "${GH_EXISTING_REFS:-}"
|
|
;;
|
|
api:--method)
|
|
endpoint="$4"
|
|
case "$endpoint" in
|
|
repos/block/buzz/git/tags)
|
|
record "$*"
|
|
printf '%s\n' "$GH_TAG_OBJECT_SHA"
|
|
;;
|
|
repos/block/buzz/git/refs)
|
|
record "$*"
|
|
;;
|
|
*) exit 2 ;;
|
|
esac
|
|
;;
|
|
api:repos/block/buzz/git/ref/tags/mobile-v1.2.3-rc.*)
|
|
if [[ "$*" == *'.object.type'* ]]; then
|
|
printf '%s\n' "${GH_PUBLISHED_REF_TYPE:-tag}"
|
|
else
|
|
printf '%s\n' "${GH_PUBLISHED_REF_SHA:-$GH_TAG_OBJECT_SHA}"
|
|
fi
|
|
;;
|
|
api:repos/block/buzz/git/tags/*)
|
|
if [[ "$*" == *'.object.type'* ]]; then
|
|
printf '%s\n' "${GH_ANNOTATED_TARGET_TYPE:-commit}"
|
|
else
|
|
printf '%s\n' "${GH_ANNOTATED_TARGET_SHA:-$GH_TARGET_SHA}"
|
|
fi
|
|
;;
|
|
*)
|
|
echo "unexpected gh call: $*" >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
GH
|
|
chmod +x "$bin/gh"
|
|
|
|
export PATH="$bin:$PATH"
|
|
export GH_CALLS="$tmp/calls"
|
|
export GITHUB_REPOSITORY=block/buzz
|
|
export GH_TARGET_SHA=1111111111111111111111111111111111111111
|
|
export GH_STABLE_SHA=4444444444444444444444444444444444444444
|
|
export GH_MISMATCH_SHA=5555555555555555555555555555555555555555
|
|
export GH_TAG_OBJECT_SHA=2222222222222222222222222222222222222222
|
|
|
|
"$publisher" 1.2.3 1 "$GH_TARGET_SHA" main
|
|
grep -Fq -- '-f tag=mobile-v1.2.3-rc.1' "$GH_CALLS"
|
|
grep -Fq -- '-f message=Buzz Mobile 1.2.3 release candidate 1' "$GH_CALLS"
|
|
grep -Fq -- "-f object=$GH_TARGET_SHA" "$GH_CALLS"
|
|
grep -Fq -- '-f type=commit' "$GH_CALLS"
|
|
grep -Fq -- '-f ref=refs/tags/mobile-v1.2.3-rc.1' "$GH_CALLS"
|
|
grep -Fq -- "-f sha=$GH_TAG_OBJECT_SHA" "$GH_CALLS"
|
|
|
|
if GH_CURRENT_USER_CAN_BYPASS=never "$publisher" 1.2.3 1 "$GH_TARGET_SHA" main >/dev/null 2>&1; then
|
|
echo "publisher accepted an App token without an always bypass" >&2
|
|
exit 1
|
|
fi
|
|
if GH_CURRENT_USER_CAN_BYPASS='' "$publisher" 1.2.3 1 "$GH_TARGET_SHA" main >/dev/null 2>&1; then
|
|
echo "publisher accepted a ruleset response without an effective bypass" >&2
|
|
exit 1
|
|
fi
|
|
if GH_TAG_RULESET_STATE=disabled "$publisher" 1.2.3 1 "$GH_TARGET_SHA" main >/dev/null 2>&1; then
|
|
echo "publisher accepted disabled tag protection" >&2
|
|
exit 1
|
|
fi
|
|
if GH_TAG_RULE_TYPES=creation "$publisher" 1.2.3 1 "$GH_TARGET_SHA" main >/dev/null 2>&1; then
|
|
echo "publisher accepted incomplete tag protection" >&2
|
|
exit 1
|
|
fi
|
|
if GH_TAG_INCLUDES=refs/tags/v\* "$publisher" 1.2.3 1 "$GH_TARGET_SHA" main >/dev/null 2>&1; then
|
|
echo "publisher accepted a tag ruleset that excludes mobile candidates" >&2
|
|
exit 1
|
|
fi
|
|
if GH_TAG_EXCLUDES=refs/tags/mobile-v0.0.0 "$publisher" 1.2.3 1 "$GH_TARGET_SHA" main >/dev/null 2>&1; then
|
|
echo "publisher accepted tag ruleset exclusions" >&2
|
|
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 commit outside main history" >&2
|
|
exit 1
|
|
fi
|
|
if GH_EXISTING_REFS=$'refs/tags/mobile-v1.2.3-rc.1\n' \
|
|
"$publisher" 1.2.3 1 "$GH_TARGET_SHA" main >/dev/null 2>&1; then
|
|
echo "publisher accepted a stale candidate number" >&2
|
|
exit 1
|
|
fi
|
|
if GH_PUBLISHED_REF_TYPE=commit "$publisher" 1.2.3 1 "$GH_TARGET_SHA" main >/dev/null 2>&1; then
|
|
echo "publisher accepted a lightweight published tag" >&2
|
|
exit 1
|
|
fi
|
|
if GH_PUBLISHED_REF_SHA=3333333333333333333333333333333333333333 \
|
|
"$publisher" 1.2.3 1 "$GH_TARGET_SHA" main >/dev/null 2>&1; then
|
|
echo "publisher accepted the wrong annotated tag object" >&2
|
|
exit 1
|
|
fi
|
|
if GH_ANNOTATED_TARGET_TYPE=tag "$publisher" 1.2.3 1 "$GH_TARGET_SHA" main >/dev/null 2>&1; then
|
|
echo "publisher accepted a nested annotated tag" >&2
|
|
exit 1
|
|
fi
|
|
if GH_ANNOTATED_TARGET_SHA=3333333333333333333333333333333333333333 \
|
|
"$publisher" 1.2.3 1 "$GH_TARGET_SHA" main >/dev/null 2>&1; then
|
|
echo "publisher accepted an annotated tag on the wrong commit" >&2
|
|
exit 1
|
|
fi
|
|
if "$publisher" 01.2.3 1 "$GH_TARGET_SHA" main >/dev/null 2>&1; then
|
|
echo "publisher accepted a marketing version with a leading zero" >&2
|
|
exit 1
|
|
fi
|
|
if "$publisher" 1.2.3 01 "$GH_TARGET_SHA" main >/dev/null 2>&1; then
|
|
echo "publisher accepted a candidate number with a leading zero" >&2
|
|
exit 1
|
|
fi
|
|
if GH_EXISTING_REFS=$'refs/tags/mobile-v1.2.3-rc.1\nrefs/tags/mobile-v1.2.3-rc.7\nrefs/tags/mobile-v1.2.3-rc.08\nrefs/tags/mobile-v1.2.4-rc.99\n' \
|
|
"$publisher" 1.2.3 8 "$GH_TARGET_SHA" main >/dev/null; then
|
|
:
|
|
else
|
|
echo "publisher did not sequence from the highest exact candidate" >&2
|
|
exit 1
|
|
fi
|
|
if GITHUB_REPOSITORY=attacker/buzz "$publisher" 1.2.3 1 "$GH_TARGET_SHA" main >/dev/null 2>&1; then
|
|
echo "publisher accepted the wrong repository" >&2
|
|
exit 1
|
|
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
|
|
exit 1
|
|
fi
|
|
done
|
|
# Mutate actual policy JSON so these tests exercise the production jq predicate.
|
|
python3 - "$publisher" <<'PYPOLICY'
|
|
import copy, json, os, subprocess, sys
|
|
policy = {'target': 'branch', 'enforcement': 'active', 'current_user_can_bypass': 'never', 'conditions': {'ref_name': {'include': ['refs/heads/mobile-v*'], 'exclude': []}}, 'bypass_actors': [], 'rules': [{'type': 'deletion'}, {'type': 'non_fast_forward'}, {'type': 'pull_request', 'parameters': {'required_approving_review_count': 1, 'dismiss_stale_reviews_on_push': True, 'require_last_push_approval': True, 'required_review_thread_resolution': True}}, {'type': 'required_status_checks', 'parameters': {'strict_required_status_checks_policy': True, 'required_status_checks': [{'context': 'Mobile'}, {'context': 'DCO Check'}]}}]}
|
|
mutations = {
|
|
"disabled": lambda p: p.update(enforcement="disabled"),
|
|
"wrong target": lambda p: p.update(target="tag"),
|
|
"wrong scope": lambda p: p["conditions"]["ref_name"].update(include=["refs/heads/mobile-release/*"]),
|
|
"excluded branch": lambda p: p["conditions"]["ref_name"].update(exclude=["refs/heads/mobile-v1.2"]),
|
|
"caller bypass": lambda p: p.update(current_user_can_bypass="always"),
|
|
"other bypass": lambda p: p.update(bypass_actors=[{"actor_id":1}]),
|
|
"no deletion guard": lambda p: p["rules"].pop(0),
|
|
"no force guard": lambda p: p["rules"].pop(1),
|
|
"no review": lambda p: p["rules"].pop(2),
|
|
"no checks": lambda p: p["rules"].pop(3),
|
|
"no approvals": lambda p: p["rules"][2]["parameters"].update(required_approving_review_count=0),
|
|
"stale approvals": lambda p: p["rules"][2]["parameters"].update(dismiss_stale_reviews_on_push=False),
|
|
"self approval": lambda p: p["rules"][2]["parameters"].update(require_last_push_approval=False),
|
|
"open threads": lambda p: p["rules"][2]["parameters"].update(required_review_thread_resolution=False),
|
|
"non-strict checks": lambda p: p["rules"][3]["parameters"].update(strict_required_status_checks_policy=False),
|
|
"missing mobile": lambda p: p["rules"][3]["parameters"]["required_status_checks"].pop(0),
|
|
"missing DCO": lambda p: p["rules"][3]["parameters"]["required_status_checks"].pop(1),
|
|
}
|
|
for name, mutate in mutations.items():
|
|
value = copy.deepcopy(policy)
|
|
mutate(value)
|
|
env = dict(os.environ, GH_BRANCH_POLICY=json.dumps(value), GH_ANNOTATED_TARGET_SHA=os.environ["GH_STABLE_SHA"])
|
|
before = open(env["GH_CALLS"]).read()
|
|
result = subprocess.run([sys.argv[1], "1.2.3", "1", env["GH_STABLE_SHA"], "mobile-v1.2"], env=env, capture_output=True, text=True)
|
|
assert result.returncode != 0 and "must enforce review" in result.stderr, (name, result.stderr)
|
|
assert open(env["GH_CALLS"]).read() == before, name
|
|
PYPOLICY
|
|
if GH_BRANCH_POLICY_FAIL=1 "$publisher" 1.2.3 1 "$GH_STABLE_SHA" mobile-v1.2 >/dev/null 2>&1; then
|
|
echo "publisher accepted unavailable branch policy" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Exercise the checked-in workflow command with its actual source input binding.
|
|
python3 - "$repo_root/.github/workflows/mobile-release-candidate.yml" "$tmp" <<'PYWORKFLOW'
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
workflow = Path(sys.argv[1]).read_text()
|
|
step = workflow.split(" - name: Publish annotated candidate tag\n", 1)[1]
|
|
assert re.search(r"^ SOURCE_BRANCH: \$\{\{ inputs\.source_branch \}\}$", step, re.M), "source input is not bound to SOURCE_BRANCH"
|
|
command = re.search(r"^ run: (.+)$", step, re.M).group(1)
|
|
root = Path(sys.argv[2]) / "workflow"
|
|
(root / "scripts").mkdir(parents=True)
|
|
stub = root / "scripts/publish-mobile-release-candidate.sh"
|
|
stub.write_text('#!/usr/bin/env bash\nprintf "%s\\n" "$@"\n')
|
|
stub.chmod(0o755)
|
|
env = dict(os.environ, MOBILE_VERSION="1.2.3", CANDIDATE_NUMBER="1", TARGET_SHA="4" * 40, SOURCE_BRANCH="mobile-v1.2")
|
|
actual = subprocess.check_output(["bash", "-eu", "-c", command], cwd=root, env=env, text=True).splitlines()
|
|
assert actual == ["1.2.3", "1", "4" * 40, "mobile-v1.2"], actual
|
|
PYWORKFLOW
|
|
|
|
echo "mobile release candidate publisher contract passed"
|