Skip to content

Commit 574555e

Browse files
hyperpolymathclaude
andcommitted
check-lock-sync: add clause 4, lockfile COVERAGE
The gate could not defend the fix this PR ships. Clauses 1-3 ask "is every `uses:` locked under its own workflow path?" GitHub asks a DIFFERENT question: "is every workflow FILE represented in the lock?" A workflow with no `uses:` satisfies clauses 1-3 vacuously and GitHub still refuses to start it - which is exactly how lock-sync-gate.yml failed here 7 times running while the checker reported the lock in sync. Thirteen other repositories passed the gate with the same gap present, so a green gate was not evidence of a complete lock. Clause 4 diffs the set of files under .github/workflows/ against the set of lockfile keys, fails on any file with no key, names it, and quotes the empty-list form to add. Remediation step 4 warns that re-running `gh actions-lock` may not fix it, because omitting the file is the tool's own defect. Mutation-tested both ways: deleting the lock-sync-gate key fails the gate, and deleting the unrelated labels.yml key fails it too; the unmutated tree passes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X3hgXxWm6umMgZkjYyHnnm
1 parent 808d987 commit 574555e

1 file changed

Lines changed: 32 additions & 0 deletions

File tree

‎scripts/check-lock-sync.sh‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,33 @@ END {
254254
if (!found) { printf "FAIL %s\n lockfile entry for a workflow file that does not exist\n", p; bad = 1 }
255255
}
256256
257+
# --- clause 4: COVERAGE. Every workflow FILE must have a key in the lockfile,
258+
# including one with no uses: at all - the value is then an empty list.
259+
# MEASURED 2026-09-22, single-variable flip on two independent repos:
260+
# hyperpolymath/verisimdb's lock-sync-gate.yml was startup_failure 7 times
261+
# running with ZERO uses: refs, and adding
262+
# '.github/workflows/lock-sync-gate.yml': []
263+
# flipped it to success; reproduced on hyperpolymath/blocky-writer, 2 of 2.
264+
# `gh actions-lock` already emits this empty-list form for other zero-uses:
265+
# workflows (labels.yml), so it is the generator's own convention, not ours.
266+
# Clauses 1-3 CANNOT catch this: they ask "is every uses: locked?", and a
267+
# workflow with no uses: satisfies them vacuously while GitHub still refuses
268+
# to start it. 13 repos passed clauses 1-3 with exactly this gap.
269+
nunlisted = 0; unlisted = ""
270+
for (i = 1; i < ARGC; i++) {
271+
q = ARGV[i]; if (q == lockfile) continue
272+
sub(/.*\//, "", q); q = ".github/workflows/" q
273+
if (q in seen_path) continue
274+
nunlisted++; unlisted = unlisted "\n " q
275+
}
276+
if (nunlisted > 0) {
277+
printf "FAIL actions.lock: UNLISTED WORKFLOWS\n"
278+
printf " %d workflow file(s) have no key in the lockfile. GitHub refuses such a\n", nunlisted
279+
printf " run at startup (jobs=0) even when the workflow has no uses: at all.\n"
280+
printf " The entry for a zero-uses: workflow is an empty list:%s\n", unlisted
281+
bad = 1
282+
}
283+
257284
# --- clause 3: TRANSITIVE CLOSURE. Every ref named anywhere in the lockfile
258285
# must resolve to a top-level dependencies: record. A dangling edge makes
259286
# GitHub refuse the run at startup with jobs=0. ---
@@ -291,12 +318,17 @@ END {
291318
print " 3. Nested `uses:` entries must be bare OWNER/REPO@REF. A subpath pin such as"
292319
print " github/codeql-action/upload-sarif@<sha> is REJECTED by the schema; collapse it"
293320
print " to github/codeql-action@<sha>."
321+
print " 4. For any UNLISTED WORKFLOWS above, add the path as a lockfile key. A workflow"
322+
print " with no uses: takes an empty list: \x27.github/workflows/x.yml\x27: []"
323+
print " `gh actions-lock` has been observed to OMIT such a workflow entirely; that"
324+
print " omission is itself the defect, so re-running the tool may not add it."
294325
exit 1
295326
}
296327
printf "actions.lock is in sync and transitively closed:\n"
297328
printf " * every uses: is locked under its own workflow path (job-level reusable refs included)\n"
298329
printf " * every lockfile entry is still referenced\n"
299330
printf " * every ref named in the lockfile resolves to a dependencies: record (0 dangling edges)\n"
331+
printf " * every workflow file has a lockfile key (zero-uses: workflows included)\n"
300332
if (nunref > 0)
301333
printf " note: %d dependencies: record(s) are unreferenced - harmless, but prunable.\n", nunref
302334
if (jnote != "")

0 commit comments

Comments
 (0)