Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 106 additions & 5 deletions scripts/close-lock.sh
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,40 @@
# lock-selfcheck.sh and the pre-commit hook; its contract is the corpus in
# scripts/tests/fixtures/actions-lock-closure/.
#
# MOVED TAGS. A leaf record for a SHA-pinned action takes as its `ref:` a tag
# that points at that SHA now. A record whose `ref:` is the SHA itself is
# rejected by runner verification (standards#1271), so none is written. An
# action that moves its tags (dtolnay/rust-toolchain moves `v1`) leaves old pins
# with no tag at all. MEASURED on hyperpolymath/standards run 39286638218
# (2026-10-19 16:36 UTC): the runner said "Using locked action versions from the
# workflow's lockfile", downloaded dtolnay/rust-toolchain@e2a55d2… from a record
# with `ref: 'v1'`, and the step passed. By then `v1` was already 683b588
# (Dependabot moved standards to it at 14:45 UTC, e926751). So a record whose
# `ref:` names a tag that has since moved is accepted. When no current tag points
# at the SHA, and only when the tag listing itself succeeded, one of these is
# tried, in order:
# 1. the record for the same key in the lock as it stood BEFORE
# `gh actions-lock` rewrote it (`--prior FILE`; regen-dependabot-locks.sh
# passes it), used only when its commit is that SHA and its owner and repo
# ids are the ones the API returns now;
# 2. the `# <tag>` comment at a call site of that SHA, in this repository's
# workflows or in a called workflow read below.
# The candidate's `ref:` is written only when it is a tag in that repository
# NOW and its current commit descends from the SHA (compare status `ahead` or
# `identical`). That holds when a tag moved forward past the pin. A branch name
# (`# stable`), a deleted tag, a self-SHA ref and a commit the tag never reached
# are refused, and the key SKIPs with exit 3 as before.
#
# GH_BIN overrides the `gh` binary (the test suite uses an offline stub).
set -euo pipefail
CHECK=0
if [[ "${1:-}" == "--check" ]]; then CHECK=1; shift; fi
CHECK=0; PRIOR=""
while :; do
case "${1:-}" in
--check) CHECK=1; shift ;;
--prior) PRIOR="${2:?close-lock: --prior needs a FILE}"; shift 2 ;;
*) break ;;
esac
done
WF="${1:-.github/workflows}"
if [[ "$CHECK" -eq 1 && -f "$WF" ]]; then L="$WF"; else L="$WF/actions.lock"; fi
GH_BIN="${GH_BIN:-gh}"
Expand All @@ -62,6 +92,10 @@
[[ "$CHECK" -eq 1 ]] && exit 2
exit 1
fi
if [[ -n "$PRIOR" && ! -f "$PRIOR" ]]; then
echo "close-lock: no prior lock $PRIOR" >&2
exit 1
fi
SC="$(mktemp -d)"; trap 'rm -rf "$SC"' EXIT
TAB="$(printf '\t')"
# keys: list the dependency record keys already present in the lock.
Expand Down Expand Up @@ -97,6 +131,50 @@
keyfile() { printf '%s' "$1" | sha256sum | cut -c1-32; }
# fold: lowercase the owner/repo part of each owner/repo@ref on stdin.
fold() { awk '{ at = index($0, "@"); if (at) print tolower(substr($0, 1, at - 1)) substr($0, at); else print tolower($0) }'; }
# uses_tags FILE: "key<TAB>tag" for each external SHA-pinned `uses:` in FILE that
# carries a trailing `# <tag>` comment (the first word of the comment).
uses_tags() {
{ grep -E '^[[:space:]]*(-[[:space:]]+)?uses:[[:space:]]' "$1" \

Check warning on line 137 in scripts/close-lock.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Assign this positional parameter to a local variable.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaFZpUtXn9Dfrbupl2PC&open=AaFZpUtXn9Dfrbupl2PC&pullRequest=1323
| sed -E 's/^[[:space:]]*(-[[:space:]]+)?uses:[[:space:]]+//' | tr -d '"'"'"'' \
| awk -v t="$TAB" '{ if (!match($0, /[[:space:]]#/)) next
v = substr($0, 1, RSTART - 1); sub(/[[:space:]]+$/, "", v); sub(/,$/, "", v)
c = substr($0, RSTART + RLENGTH); sub(/^[[:space:]]+/, "", c); split(c, w, /[[:space:]]+/)
if (w[1] == "" || v ~ /^(\.\/|\$\/|docker:\/\/)/ || v ~ /[[:space:]]/) next
at = index(v, "@"); if (!at) next
r = substr(v, at + 1); if (length(r) != 40 || r !~ /^[0-9a-f]+$/) next
n = split(substr(v, 1, at - 1), a, "/"); if (n < 2 || a[1] == "" || a[2] == "") next
print a[1] "/" a[2] "@" r t w[1] }'; } || true
}
# prior_record KEY: "ref<TAB>commit<TAB>owner_id<TAB>repo_id" of KEY's record in
# the --prior lock (owner/repo compared case-insensitively), or nothing.
prior_record() {
[ -n "$PRIOR" ] || return 0

Check failure on line 151 in scripts/close-lock.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaFZpUtXn9Dfrbupl2PD&open=AaFZpUtXn9Dfrbupl2PD&pullRequest=1323
awk -v k="$(printf '%s\n' "$1" | fold)" -v t="$TAB" '

Check warning on line 152 in scripts/close-lock.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Assign this positional parameter to a local variable.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaFZpUtXn9Dfrbupl2PE&open=AaFZpUtXn9Dfrbupl2PE&pullRequest=1323
function fk(s, at) { at = index(s, "@"); return tolower(substr(s, 1, at - 1)) substr(s, at) }
/^dependencies:[[:space:]]*$/ { d = 1; next }
d && /^[a-z_]+:/ { d = 0 }
!d { next }
/^ '\''[^'\'']+'\'':$/ { if (on) exit; s = $0; sub(/^ '\''/, "", s); sub(/'\'':$/, "", s); on = (fk(s) == k); next }
on && /^ ref: '\''[^'\'']+'\''$/ { r = $0; sub(/^ ref: '\''/, "", r); sub(/'\''$/, "", r) }
on && /^ commit: '\''[^'\'']+'\''$/ { c = $0; sub(/^ commit: '\''/, "", c); sub(/'\''$/, "", c) }
on && /^ owner_id: [0-9]+$/ { o = $2 }
on && /^ repo_id: [0-9]+$/ { p = $2 }
END { if (r != "") print r t c t o t p }' "$PRIOR"
}
# moved_tag OWNER NAME SHA TAG: succeed when TAG is a tag of OWNER/NAME now (not
# a branch, not a SHA) and its current commit descends from SHA, i.e. the tag
# moved forward past the pin. Otherwise print why on stdout and fail.
moved_tag() {
local tsha st
if ! printf '%s' "$4" | grep -qxE '[A-Za-z0-9._+@/-]+' || printf '%s' "$4" | grep -qxE '[0-9a-f]{40}'; then

Check warning on line 169 in scripts/close-lock.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Assign this positional parameter to a local variable.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaFZpUtXn9Dfrbupl2PG&open=AaFZpUtXn9Dfrbupl2PG&pullRequest=1323

Check warning on line 169 in scripts/close-lock.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Assign this positional parameter to a local variable.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaFZpUtXn9Dfrbupl2PF&open=AaFZpUtXn9Dfrbupl2PF&pullRequest=1323
echo "not a symbolic tag name"; return 1
fi
tsha=$("$GH_BIN" api "repos/$1/$2/commits/tags/$4" --jq .sha 2>/dev/null) || tsha=""
if ! printf '%s' "$tsha" | grep -qxE '[0-9a-f]{40}'; then echo "no tag of that name"; return 1; fi
st=$("$GH_BIN" api "repos/$1/$2/compare/$3...$tsha" --jq .status 2>/dev/null) || st=""
case "$st" in ahead|identical) return 0 ;; esac

Check failure on line 175 in scripts/close-lock.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a default case (*) to handle unexpected values.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaFZpUtXn9Dfrbupl2PH&open=AaFZpUtXn9Dfrbupl2PH&pullRequest=1323
echo "tag is at ${tsha:0:12}, which does not descend from the pin (compare: ${st:-failed})"; return 1
}

if [[ "$CHECK" -eq 1 ]]; then
keys | fold | LC_ALL=C sort -u > "$SC/have"
Expand All @@ -119,9 +197,10 @@
fi

# ── reusable workflows: read each called file once, transitively ─────────────
mkdir -p "$SC/u"; : > "$SC/read"; : > "$SC/bad"; : > "$SC/more"
mkdir -p "$SC/u"; : > "$SC/read"; : > "$SC/bad"; : > "$SC/more"; : > "$SC/ctags"
for f in "$WF"/*.yml "$WF"/*.yaml; do
[ -e "$f" ] || continue
uses_tags "$f" >> "$SC/ctags"
uses_values "$f" | reusable_calls
done | LC_ALL=C sort -u > "$SC/calls"
while :; do
Expand All @@ -135,6 +214,7 @@
&& grep -qE '^[[:space:]]*\{?[[:space:]]*"?jobs"?[[:space:]]*:' "$SC/body"; then
uses_values "$SC/body" | external_refs >> "$SC/u/$(keyfile "$k")"
uses_values "$SC/body" | reusable_calls >> "$SC/more"
uses_tags "$SC/body" >> "$SC/ctags"
else
echo " SKIP $k (cannot read $p at $ref)"; echo "$k" >> "$SC/bad"
fi
Expand Down Expand Up @@ -216,16 +296,37 @@
echo " SKIP $k (cannot resolve commit)"; skipped=$((skipped + 1)); continue
fi
rec_ref="$ref"
if printf '%s' "$ref" | grep -qxE '[0-9a-f]{40}' && ! grep -qxF "$k$TAB" "$SC/owed"; then

Check warning on line 299 in scripts/close-lock.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of using the literal '[0-9a-f]{40}' 5 times.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaFZpUtXn9Dfrbupl2PQ&open=AaFZpUtXn9Dfrbupl2PQ&pullRequest=1323
# Consume all pages before choosing a tag. An early pipeline exit can
# SIGPIPE gh under pipefail and discard an otherwise valid result.
tag_ref=""
tag_ref=""; listed=0; why=""
if "$GH_BIN" api "repos/$owner/$name/tags?per_page=100" --paginate \
--jq '.[] | select(.commit.sha == "'"$sha"'") | .name' > "$SC/tags" 2>/dev/null; then
listed=1
tag_ref=$(awk '/^[A-Za-z0-9._+@\/-]+$/ && !(length($0) == 40 && $0 ~ /^[0-9a-f]+$/) { if (!first) first = $0; if (!preferred && $0 ~ /^v?[0-9]+\.[0-9]+\.[0-9]+/) preferred = $0 } END { if (preferred) print preferred; else if (first) print first }' "$SC/tags")
fi
# No current tag points at the SHA: try a moved tag (see MOVED TAGS).
if [ -z "$tag_ref" ] && [ "$listed" -eq 1 ]; then

Check failure on line 309 in scripts/close-lock.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaFZpUtXn9Dfrbupl2PI&open=AaFZpUtXn9Dfrbupl2PI&pullRequest=1323

Check failure on line 309 in scripts/close-lock.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaFZpUtXn9Dfrbupl2PJ&open=AaFZpUtXn9Dfrbupl2PJ&pullRequest=1323
IFS="$TAB" read -r p_ref p_commit p_owner p_repo <<<"$(prior_record "$k")" || true
if [ -n "${p_ref:-}" ]; then

Check failure on line 311 in scripts/close-lock.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaFZpUtXn9Dfrbupl2PK&open=AaFZpUtXn9Dfrbupl2PK&pullRequest=1323
if [ "$p_commit" != "sha1-$sha" ]; then why="$why; prior record: commit $p_commit"

Check failure on line 312 in scripts/close-lock.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaFZpUtXn9Dfrbupl2PL&open=AaFZpUtXn9Dfrbupl2PL&pullRequest=1323
elif [ "$p_owner $p_repo" != "$ids" ]; then why="$why; prior record: ids $p_owner $p_repo, API $ids"

Check failure on line 313 in scripts/close-lock.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaFZpUtXn9Dfrbupl2PM&open=AaFZpUtXn9Dfrbupl2PM&pullRequest=1323
elif r=$(moved_tag "$owner" "$name" "$sha" "$p_ref"); then tag_ref="$p_ref"; src="the prior lock record"
else why="$why; prior ref '$p_ref': $r"; fi
fi
if [ -z "$tag_ref" ]; then
while IFS= read -r c; do
[ -n "$c" ] || continue

Check failure on line 319 in scripts/close-lock.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaFZpUtXn9Dfrbupl2PN&open=AaFZpUtXn9Dfrbupl2PN&pullRequest=1323
if r=$(moved_tag "$owner" "$name" "$sha" "$c"); then tag_ref="$c"; src="a call-site comment"; break; fi
why="$why; call-site '# $c': $r"
done < <(awk -F'\t' -v k="$(printf '%s\n' "$k" | fold)" 'BEGIN { OFS = "\t" }
{ at = index($1, "@"); if (tolower(substr($1, 1, at - 1)) substr($1, at) == k) print ($2 ~ /^v?[0-9]+\.[0-9]+\.[0-9]+/ ? 0 : 1), $2 }' \
"$SC/ctags" | LC_ALL=C sort -u | cut -f2)
fi
[ -n "$tag_ref" ] && echo " ~ $k ref '$tag_ref' from $src: no current tag points at the pin; the tag moved forward from it"

Check failure on line 326 in scripts/close-lock.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaFZpUtXn9Dfrbupl2PO&open=AaFZpUtXn9Dfrbupl2PO&pullRequest=1323
fi
if [ -z "$tag_ref" ]; then

Check failure on line 328 in scripts/close-lock.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaFZpUtXn9Dfrbupl2PP&open=AaFZpUtXn9Dfrbupl2PP&pullRequest=1323
echo " SKIP $k (cannot resolve a symbolic tag for leaf SHA)"
echo " SKIP $k (cannot resolve a symbolic tag for leaf SHA${why:+$why})"
skipped=$((skipped + 1))
continue
fi
Expand Down
12 changes: 10 additions & 2 deletions scripts/regen-dependabot-locks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
# gh actions-lock → relock-sha-keys → complete-job-refs → close-lock → prune-stale
# then RESTORES every file except actions.lock, because `gh actions-lock`
# rewrite mode de-pins inline SHAs and can invent `uses: $/...` local refs.
# close-lock gets the lock as it stood before `gh actions-lock` (--prior). That
# lets a leaf whose tag has since moved keep its old `ref:` (close-lock.sh,
# MOVED TAGS).
# The result is committed only when:
# * the ONLY changed path is .github/workflows/actions.lock,
# * the lock names no `$/` ref and close-lock resolved every edge, and
Expand Down Expand Up @@ -73,10 +76,14 @@ LOCK_PATH=.github/workflows/actions.lock
# --verify-local, fails a lock that is missing their refs. Before that, they
# were refused as `composite-unsupported`.
regen_lock() {
local dir="$1" changed
local dir="$1" changed prior
prior="$(mktemp)"
(
cd "$dir" || exit 1
[ -f "$LOCK_PATH" ] || { echo no-lock; exit 0; }
# Snapshot the lock before the tool rewrites it: close-lock may reuse a
# record from it for a leaf whose tag moved (close-lock.sh, MOVED TAGS).
cp "$LOCK_PATH" "$prior" || { echo tool-failed; exit 0; }
# `gh actions-lock` exits non-zero whenever a workflow names a mutable
# ref it will not auto-pin (e.g. hyperpolymath/cicd-suite/...@main, which
# the template's dogfood-gate and main-estate-audit carry), even though it
Expand All @@ -95,7 +102,7 @@ regen_lock() {
git clean -fdq
bash "$REGEN_TOOLS/relock-sha-keys.sh" >&2 || { echo tool-failed; exit 0; }
bash "$REGEN_TOOLS/complete-job-refs.sh" >&2 || { echo tool-failed; exit 0; }
GH_BIN="$GH_BIN" bash "$REGEN_TOOLS/close-lock.sh" >&2
GH_BIN="$GH_BIN" bash "$REGEN_TOOLS/close-lock.sh" --prior "$prior" >&2
case $? in 0) ;; 3) echo unresolvable; exit 0 ;; *) echo tool-failed; exit 0 ;; esac
bash "$REGEN_TOOLS/prune-stale.sh" >&2 || { echo tool-failed; exit 0; }
if grep -Fq '$/' "$LOCK_PATH"; then echo corrupt; exit 0; fi
Expand All @@ -110,6 +117,7 @@ regen_lock() {
if ! bash "$REGEN_TOOLS/check-lock-sync.sh" >&2; then echo unverified; exit 0; fi
if [ -z "$changed" ]; then echo current; else echo changed; fi
)
rm -f "$prior"
}

# Commit messages: a Dependabot/applier PR repair, and a default-branch repair.
Expand Down
102 changes: 101 additions & 1 deletion scripts/tests/close-lock-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ bad() { echo "FAIL: $1"; fail=$((fail + 1)); }

# Stub: repos/<o>/<n> → "7 9"; commits/v1 → $C; anything under o/missing → an
# HTTP error body on stdout, exit 1; anything under o/garbled → a truncated body
# on stdout, exit 0 (the {"messa shape AGENTS.md §5 records). A contents/ call
# on stdout, exit 0 (the {"messa shape AGENTS.md §5 records). Moved tags: the
# tag v1 of any o/ repo is at $C, $C descends from $A (compare `ahead`), $B is
# on another line (`diverged`), and no other tag exists. A contents/ call
# serves the file planted under $WORK/contents (its path with / ? = as _), or a
# 404 body with exit 1 when none is planted.
mkdir -p "$WORK/contents"
Expand All @@ -48,6 +50,10 @@ case "\$2" in
repos/o/buildmeta/tags*) echo 'v1.2.3+build.4' ;;
repos/o/releaseat/tags*) echo 'release@2026' ;;
repos/o/leaf/tags*) printf 'v4\nv4.38.0\n' ;;
repos/o/*/commits/tags/v1) echo $C ;;
repos/o/*/commits/tags/*) echo '{"message":"No commit found"}'; exit 1 ;;
repos/o/*/compare/$A...$C) echo ahead ;;
repos/o/*/compare/*) echo diverged ;;
repos/o/*/commits/v1) echo $C ;;
repos/o/*/commits/*) echo '{"message":"No commit found"}'; exit 1 ;;
repos/o/*) echo '7 9' ;;
Expand Down Expand Up @@ -256,6 +262,100 @@ out=$(run "$d"); st=$?
[ "$st" = 3 ] && ! grep -qxF " 'o/garbled@$A':" "$d/actions.lock" \
&& ok "malformed called file (exit 0, no jobs:) → SKIP, exit 3" || bad "malformed called file → SKIP, exit 3 — st=$st $out"

# runp <workflows dir> <prior lock>: run with --prior.
runp() { (cd "$1/../.." && GH_BIN="$WORK/gh" bash "$TARGET" --prior "$2") 2>&1; }
# mkprior <name>: a pre-regeneration lock whose dependencies: come from stdin.
mkprior() { { echo 'dependencies:'; cat; } > "$WORK/$1.prior"; printf '%s' "$WORK/$1.prior"; }
# refused <label> <dir> <out> <st>: exit 3, the dangling key got no record.
refused() {
[ "$4" = 3 ] && grep -qF "SKIP o/" <<<"$3" && ! grep -qE "^ 'o/(untagged|tagerror)@" "$2/actions.lock" \
&& ok "$1" || bad "$1 — st=$4 $3"
}
# prior_rec <key> <ref> <commit-sha> <owner_id>: one prior record.
prior_rec() { printf " '%s':\n ref: '%s'\n commit: 'sha1-%s'\n owner_id: %s\n repo_id: 9\n" "$1" "$2" "$3" "$4"; }

echo "=== moved tag: the pre-regeneration record supplies ref: ==="
d=$(mklock mv1 "o/untagged@$A" </dev/null); p=$(prior_rec "o/untagged@$A" v1 "$A" 7 | mkprior mv1)
out=$(runp "$d" "$p"); st=$?
[ "$st" = 0 ] && [ "$(record "$d/actions.lock" "o/untagged@$A")" = " 'o/untagged@$A':
ref: 'v1'
commit: 'sha1-$A'
owner_id: 7
repo_id: 9" ] && grep -qF "~ o/untagged@$A ref 'v1' from the prior lock record" <<<"$out" \
&& ok "prior record with a forward-moved tag → leaf record ref 'v1', reported" || bad "prior record → ref 'v1' — st=$st $out"
d=$(mklock mv2 "o/untagged@$A" </dev/null); p=$(prior_rec "O/Untagged@$A" v1 "$A" 7 | mkprior mv2)
out=$(runp "$d" "$p"); st=$?
[ "$st" = 0 ] && record "$d/actions.lock" "o/untagged@$A" | grep -qxF " ref: 'v1'" \
&& ok "prior record key matches owner/repo case-insensitively" || bad "prior record case fold — st=$st $out"

echo "=== moved tag: planted positives — a prior record that does not qualify writes nothing ==="
d=$(mklock mvr1 "o/untagged@$A" </dev/null); p=$(prior_rec "o/untagged@$A" v1 "$B" 7 | mkprior mvr1)
out=$(runp "$d" "$p"); st=$?; refused "prior record for another commit → refused" "$d" "$out" "$st"
d=$(mklock mvr2 "o/untagged@$A" </dev/null); p=$(prior_rec "o/untagged@$A" v1 "$A" 8 | mkprior mvr2)
out=$(runp "$d" "$p"); st=$?; refused "prior record whose owner_id differs from the API → refused" "$d" "$out" "$st"
d=$(mklock mvr3 "o/untagged@$A" </dev/null); p=$(prior_rec "o/untagged@$A" "$A" "$A" 7 | mkprior mvr3)
out=$(runp "$d" "$p"); st=$?; refused "prior self-SHA ref → refused (standards#1271)" "$d" "$out" "$st"
grep -qF "prior ref '$A': not a symbolic tag name" <<<"$out" && ok "…refused by name, before any lookup" || bad "self-SHA prior reason — $out"
d=$(mklock mvr4 "o/untagged@$A" </dev/null); p=$(prior_rec "o/untagged@$A" gone "$A" 7 | mkprior mvr4)
out=$(runp "$d" "$p"); st=$?; refused "prior ref naming no current tag → refused" "$d" "$out" "$st"
d=$(mklock mvr5 "o/untagged@$B" </dev/null); p=$(prior_rec "o/untagged@$B" v1 "$B" 7 | mkprior mvr5)
out=$(runp "$d" "$p"); st=$?
[ "$st" = 3 ] && grep -qF 'does not descend from the pin (compare: diverged)' <<<"$out" && ! grep -qF " 'o/untagged@$B':" "$d/actions.lock" \
&& ok "prior tag whose commit does not descend from the pin → refused" || bad "diverged prior tag — st=$st $out"
d=$(mklock mvr6 "o/tagerror@$A" </dev/null); p=$(prior_rec "o/tagerror@$A" v1 "$A" 7 | mkprior mvr6)
cp "$d/actions.lock" "$WORK/mvr6.before"; out=$(runp "$d" "$p"); st=$?
[ "$st" = 3 ] && cmp -s "$WORK/mvr6.before" "$d/actions.lock" \
&& ok "failed tag listing → no fallback, exit 3, lock unchanged" || bad "failed tag listing must not fall back — st=$st $out"
d=$(mklock mvr7 "o/untagged@$A" </dev/null)
out=$(runp "$d" "$WORK/no-such.prior"); st=$?
[ "$st" = 1 ] && ok "--prior naming a missing file → exit 1" || bad "--prior missing file — st=$st $out"

echo "=== moved tag: a call-site '# <tag>' comment supplies ref: ==="
plant o/r .github/workflows/cm.yml "$A" <<EOF
jobs:
a:
runs-on: ubuntu-latest
steps:
- uses: o/untagged@$A # v1
EOF
d=$(mklock mc1 "o/r@$A" </dev/null); mkwf mc1 "o/r/.github/workflows/cm.yml@$A"
out=$(run "$d"); st=$?
[ "$st" = 0 ] && record "$d/actions.lock" "o/r@$A" | grep -qxF " - 'o/untagged@$A'" \
&& record "$d/actions.lock" "o/untagged@$A" | grep -qxF " ref: 'v1'" \
&& grep -qF "~ o/untagged@$A ref 'v1' from a call-site comment" <<<"$out" \
&& ok "comment in a called workflow → leaf record ref 'v1'" || bad "called-workflow comment — st=$st $out"
d=$(mklock mc2 "o/untagged@$A" </dev/null)
printf '{\n jobs: {\n a: {\n steps: [\n {\n uses: "o/untagged@%s", # v1\n },\n ],\n },\n },\n}\n' "$A" > "$d/ci.yml"
out=$(run "$d"); st=$?
[ "$st" = 0 ] && record "$d/actions.lock" "o/untagged@$A" | grep -qxF " ref: 'v1'" \
&& ok "KYAML comment in this repository's workflow → leaf record ref 'v1'" || bad "local KYAML comment — st=$st $out"
d=$(mklock mc3 "o/untagged@$A" </dev/null); p=$(prior_rec "o/untagged@$A" gone "$A" 7 | mkprior mc3)
printf 'jobs:\n a:\n steps:\n - uses: o/untagged@%s # v1\n' "$A" > "$d/ci.yml"
out=$(runp "$d" "$p"); st=$?
[ "$st" = 0 ] && record "$d/actions.lock" "o/untagged@$A" | grep -qxF " ref: 'v1'" && grep -qF 'from a call-site comment' <<<"$out" \
&& ok "a refused prior record falls through to the call-site comment" || bad "prior → comment fallthrough — st=$st $out"

echo "=== moved tag: planted positives — a call-site comment that does not qualify writes nothing ==="
d=$(mklock mcr1 "o/untagged@$A" </dev/null)
printf 'jobs:\n a:\n steps:\n - uses: o/untagged@%s # stable\n' "$A" > "$d/ci.yml"
out=$(run "$d"); st=$?
refused "comment naming a branch, not a tag (# stable) → refused" "$d" "$out" "$st"
grep -qF "call-site '# stable': no tag of that name" <<<"$out" && ok "…and the refusal says why" || bad "refusal reason — $out"
d=$(mklock mcr2 "o/untagged@$B" </dev/null)
printf 'jobs:\n a:\n steps:\n - uses: o/untagged@%s # v1\n' "$B" > "$d/ci.yml"
out=$(run "$d"); st=$?
[ "$st" = 3 ] && ! grep -qF " 'o/untagged@$B':" "$d/actions.lock" \
&& ok "comment tag whose commit does not descend from the pin → refused" || bad "diverged comment tag — st=$st $out"
d=$(mklock mcr3 "o/untagged@$A" </dev/null)
printf 'jobs:\n a:\n steps:\n - uses: o/untagged@%s # %s\n' "$A" "$A" > "$d/ci.yml"
out=$(run "$d"); st=$?
refused "comment naming the SHA itself → refused (standards#1271)" "$d" "$out" "$st"
grep -qF "call-site '# $A': not a symbolic tag name" <<<"$out" && ok "…refused by name, before any lookup" || bad "self-SHA comment reason — $out"
d=$(mklock mcr4 "o/untagged@$A" </dev/null)
printf 'jobs:\n a:\n steps:\n - uses: o/other@%s # v1\n' "$A" > "$d/ci.yml"
out=$(run "$d"); st=$?
refused "comment on another action's pin is not borrowed → refused" "$d" "$out" "$st"

echo "=== structural ==="
mkdir -p "$WORK/t7/.github/workflows"
(cd "$WORK/t7" && GH_BIN="$WORK/gh" bash "$TARGET") >/dev/null 2>&1; st=$?
Expand Down
Loading
Loading