Marks the token grant as verified now that #28's own merge ran release-tag and moved v1 to that commit (confirmed against the CI status API and the v1 tag on origin), and rewords the scenario-9 stranding framing to "a run that deferred and left no newer run behind it" rather than a concurrency-group cancellation ci.yaml no longer allows. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UkSjXXtU6JYcN2vPntWhfb
181 lines
7.7 KiB
Bash
181 lines
7.7 KiB
Bash
#!/usr/bin/env bash
|
|
# Regression test for release-v1.sh against a scratch bare origin: v1 reaches
|
|
# main's tip once it has been gated, never lands on an ungated commit, and
|
|
# never moves backwards when two writers race (gitdan-actions#27).
|
|
#
|
|
# run_sweep mirrors release-sweep.yaml's step order -- check, gate only when
|
|
# needed, push the gated tip leased on the v1 the check read -- with the gate
|
|
# stood in for by a command, so a failing gate is a failing sweep.
|
|
set -euo pipefail
|
|
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
release="$script_dir/release-v1.sh"
|
|
|
|
scratch=$(mktemp -d)
|
|
trap 'rm -rf "$scratch"' EXIT
|
|
pass_count=0
|
|
fail() { echo "ASSERTION FAILED: $*" >&2; exit 1; }
|
|
ok() { pass_count=$((pass_count + 1)); echo "PASS: $*"; }
|
|
|
|
export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t
|
|
unset GITHUB_OUTPUT
|
|
|
|
# A fresh origin with main at one commit and v1 on it; dev pushes to main,
|
|
# ci and ci2 are the runners' clones.
|
|
fresh() {
|
|
rm -rf "$scratch/w"; mkdir -p "$scratch/w"
|
|
git init -q --bare "$scratch/w/origin.git"
|
|
git clone -q "$scratch/w/origin.git" "$scratch/w/dev" 2>/dev/null
|
|
commit_to_main >/dev/null
|
|
git -C "$scratch/w/dev" push -q origin HEAD:refs/tags/v1
|
|
git clone -q "$scratch/w/origin.git" "$scratch/w/ci"
|
|
git clone -q "$scratch/w/origin.git" "$scratch/w/ci2"
|
|
}
|
|
commit_to_main() {
|
|
git -C "$scratch/w/dev" commit -q --allow-empty -m "c$RANDOM"
|
|
git -C "$scratch/w/dev" push -q origin HEAD:refs/heads/main
|
|
git -C "$scratch/w/dev" rev-parse HEAD
|
|
}
|
|
origin_v1() { git -C "$scratch/w/origin.git" rev-parse -q --verify 'refs/tags/v1^{commit}' || true; }
|
|
origin_tip() { git -C "$scratch/w/origin.git" rev-parse refs/heads/main; }
|
|
in_ci() { (cd "$scratch/w/${CLONE:-ci}" && bash "$release" "$@"); }
|
|
field() { sed -n "s/^$1=//p"; }
|
|
|
|
run_sweep() {
|
|
local gate="$1" out tip v1
|
|
out=$(in_ci sweep-check)
|
|
[ "$(field needed <<<"$out")" = true ] || return 0
|
|
tip=$(field tip <<<"$out"); v1=$(field v1 <<<"$out")
|
|
"$gate" || return 1
|
|
in_ci push "$tip" "$v1"
|
|
}
|
|
|
|
echo "=== 1. sweep with v1 at the tip is a no-op ==="
|
|
fresh
|
|
before=$(origin_v1)
|
|
out=$(in_ci sweep-check)
|
|
[ "$(field needed <<<"$out")" = false ] || fail "a current v1 was reported as lagging"
|
|
run_sweep false || fail "a current v1 ran the gate"
|
|
[ "$(origin_v1)" = "$before" ] || fail "a no-op sweep moved v1"
|
|
ok "v1 == tip: needed=false, gate not run, v1 unchanged"
|
|
|
|
echo
|
|
echo "=== 2. sweep with v1 ahead of the tip is a no-op ==="
|
|
fresh
|
|
ahead=$(git -C "$scratch/w/dev" commit-tree -p HEAD -m ahead 'HEAD^{tree}')
|
|
git -C "$scratch/w/dev" push -q -f origin "$ahead:refs/tags/v1"
|
|
out=$(in_ci sweep-check)
|
|
[ "$(field needed <<<"$out")" = false ] || fail "a v1 descending from the tip was reported as lagging"
|
|
ok "v1 descends from tip: needed=false"
|
|
|
|
echo
|
|
echo "=== 3. sweep with v1 behind and a passing gate tags the tip ==="
|
|
fresh
|
|
tip=$(commit_to_main)
|
|
run_sweep true || fail "a passing sweep failed"
|
|
[ "$(origin_v1)" = "$tip" ] || fail "v1 is $(origin_v1), not the gated tip $tip"
|
|
ok "v1 behind, gate green: v1 -> tip"
|
|
|
|
echo
|
|
echo "=== 4. sweep with v1 behind and a failing gate goes red and tags nothing ==="
|
|
fresh
|
|
before=$(origin_v1)
|
|
commit_to_main >/dev/null
|
|
if run_sweep false; then fail "a sweep over a failing gate succeeded"; fi
|
|
[ "$(origin_v1)" = "$before" ] || fail "a failing gate still moved v1"
|
|
ok "v1 behind, gate red: sweep red, v1 unchanged"
|
|
|
|
echo
|
|
echo "=== 5. a lost lease to a newer writer is a clean skip, never a step back ==="
|
|
fresh
|
|
t1=$(commit_to_main)
|
|
out=$(in_ci sweep-check); v1_read=$(field v1 <<<"$out")
|
|
t2=$(commit_to_main)
|
|
CLONE=ci2 in_ci merge "$t2" >/dev/null
|
|
[ "$(origin_v1)" = "$t2" ] || fail "the merge job did not release its own tip"
|
|
in_ci push "$t1" "$v1_read" || fail "a lease lost to a newer v1 went red"
|
|
[ "$(origin_v1)" = "$t2" ] || fail "v1 went backwards from $t2 to $(origin_v1)"
|
|
ok "older writer lost the lease: exit 0, v1 stays at the newer $t2"
|
|
git -C "$scratch/w/ci" push -q -f origin "$t1:refs/tags/v1"
|
|
[ "$(origin_v1)" = "$t1" ] || fail "control: an unleased push did not step v1 back"
|
|
ok "control: the same push without the lease steps v1 back to $t1"
|
|
|
|
echo
|
|
echo "=== 6. a lease lost to an older writer retries and lands the newer commit ==="
|
|
fresh
|
|
t1=$(commit_to_main)
|
|
t2=$(commit_to_main)
|
|
out=$(in_ci sweep-check); v1_read=$(field v1 <<<"$out")
|
|
git -C "$scratch/w/dev" push -q -f origin "$t1:refs/tags/v1"
|
|
in_ci push "$t2" "$v1_read" || fail "a lease lost to an older v1 went red"
|
|
[ "$(origin_v1)" = "$t2" ] || fail "v1 is $(origin_v1), not $t2"
|
|
ok "v1 moved to an ancestor under us: retried, v1 -> $t2"
|
|
|
|
echo
|
|
echo "=== 7. a lease lost to an unrelated commit goes red ==="
|
|
fresh
|
|
tip=$(commit_to_main)
|
|
out=$(in_ci sweep-check); v1_read=$(field v1 <<<"$out")
|
|
stray=$(git -C "$scratch/w/dev" commit-tree -m stray 'HEAD^{tree}')
|
|
git -C "$scratch/w/dev" push -q -f origin "$stray:refs/tags/v1"
|
|
if in_ci push "$tip" "$v1_read" 2>/dev/null; then fail "a v1 moved sideways was accepted"; fi
|
|
[ "$(origin_v1)" = "$stray" ] || fail "the stray v1 was overwritten"
|
|
ok "v1 moved to a commit neither ahead nor behind: red, v1 untouched"
|
|
|
|
echo
|
|
echo "=== 8. a push rejected for another reason goes red ==="
|
|
fresh
|
|
tip=$(commit_to_main)
|
|
mkdir -p "$scratch/w/origin.git/hooks"
|
|
printf '#!/bin/sh\nexit 1\n' > "$scratch/w/origin.git/hooks/pre-receive"
|
|
chmod +x "$scratch/w/origin.git/hooks/pre-receive"
|
|
before=$(origin_v1)
|
|
if in_ci merge "$tip" 2>"$scratch/err"; then fail "a rejected push reported success"; fi
|
|
[ "$(origin_v1)" = "$before" ] || fail "v1 moved despite the rejection"
|
|
grep -q 'not a lost lease' "$scratch/err" || fail "the rejection was not diagnosed as one: $(cat "$scratch/err")"
|
|
ok "server rejection with v1 unmoved: red, not a lost lease"
|
|
|
|
echo
|
|
echo "=== 9. the merge job defers on a moved tip; the next sweep catches up ==="
|
|
# The stranded trace: C1's job runs after C2 merged and defers — a run that
|
|
# deferred and left no newer run behind it — and merges stop.
|
|
fresh
|
|
before=$(origin_v1)
|
|
c1=$(commit_to_main)
|
|
c2=$(commit_to_main)
|
|
in_ci merge "$c1" >/dev/null || fail "the deferring merge job went red"
|
|
[ "$(origin_v1)" = "$before" ] || fail "the merge job released a commit that was not the tip"
|
|
run_sweep true || fail "the catch-up sweep failed"
|
|
[ "$(origin_v1)" = "$c2" ] || fail "v1 is $(origin_v1), not the tip $c2"
|
|
ok "C1 deferred, C2 never ran: the sweep moved v1 to $c2"
|
|
|
|
echo
|
|
echo "=== 10. the merge job releases its own tip, and creates a missing v1 ==="
|
|
fresh
|
|
tip=$(commit_to_main)
|
|
in_ci merge "$tip" >/dev/null
|
|
[ "$(origin_v1)" = "$tip" ] || fail "the merge job did not release the tip"
|
|
git -C "$scratch/w/dev" push -q origin :refs/tags/v1
|
|
tip=$(commit_to_main)
|
|
in_ci merge "$tip" >/dev/null
|
|
[ "$(origin_v1)" = "$tip" ] || fail "the merge job did not create an absent v1"
|
|
[ "$(origin_tip)" = "$tip" ] || fail "main moved"
|
|
ok "tip == gated sha: released, including onto an absent v1"
|
|
|
|
echo
|
|
echo "=== 11. a v1 hand-placed on an unrelated commit is never silently overwritten ==="
|
|
# Unlike #7, nothing races here -- v1 already sits on the stray commit before
|
|
# the very first push attempt, so force-with-lease sees exactly the value it
|
|
# expects and would otherwise succeed outright.
|
|
fresh
|
|
stray=$(git -C "$scratch/w/dev" commit-tree -m stray 'HEAD^{tree}')
|
|
git -C "$scratch/w/dev" push -q -f origin "$stray:refs/tags/v1"
|
|
tip=$(commit_to_main)
|
|
if in_ci merge "$tip" 2>"$scratch/err"; then fail "an unrelated hand-placed v1 was overwritten"; fi
|
|
[ "$(origin_v1)" = "$stray" ] || fail "v1 moved off the hand-placed $stray"
|
|
grep -q "$stray" "$scratch/err" || fail "the error did not name the stray v1: $(cat "$scratch/err")"
|
|
grep -q "$tip" "$scratch/err" || fail "the error did not name the gated sha: $(cat "$scratch/err")"
|
|
ok "hand-placed v1, unrelated to tip: red on the first push, v1 untouched"
|
|
|
|
echo
|
|
echo "release-v1-selftest: all $pass_count assertions passed"
|