#!/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, C2's job was # cancelled in the concurrency group, 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"