From 454bf274f2834a6ad0721fc003b12071803381af Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 22 Sep 2026 18:53:23 +0100 Subject: [PATCH 1/4] fix(ci): give the lock-sync gate a workflow_dispatch trigger The gate is the one workflow in this repo with no manual trigger, so when it fails at startup there is no way to re-observe it: `gh run rerun` refuses a startup_failure, and the only other triggers are `pull_request` and `push` to main. That left verisimdb with six startup_failures and no way to take a clean seventh measurement. Adding `workflow_dispatch:` costs nothing (the gate still carries zero real `uses:` refs, so it cannot desync against actions.lock) and makes the gate re-observable on demand. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01X3hgXxWm6umMgZkjYyHnnm --- .github/workflows/lock-sync-gate.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/lock-sync-gate.yml b/.github/workflows/lock-sync-gate.yml index 936c873..db046cb 100644 --- a/.github/workflows/lock-sync-gate.yml +++ b/.github/workflows/lock-sync-gate.yml @@ -20,6 +20,7 @@ name: Lock Sync Gate # ruleset that requires this check. on: + workflow_dispatch: pull_request: push: branches: [main] From 3b667bedd1eb9168f0b8aba97618de213af68098 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 22 Sep 2026 18:58:38 +0100 Subject: [PATCH 2/4] fix(ci): give lock-sync-gate.yml an actions.lock entry verisimdb's actions.lock lists 24 of its 25 workflow files. The single omission is lock-sync-gate.yml, and it is the single workflow that cannot start: 7 of 7 runs are startup_failure, including one 22 minutes before and one 3.5 hours after the estate-wide startup incident window, plus a minimal zzz-probe.yml that died the same way on 2026-08-03. The gate carries zero real 'uses:' refs by design, so its entry is the empty list. This tests whether GitHub requires every workflow to be represented in actions.lock once the lockfile exists, rather than only those with refs. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01X3hgXxWm6umMgZkjYyHnnm --- .github/workflows/actions.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index e264c21..d5f8194 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -40,6 +40,7 @@ workflows: - 'peter-evans/repository-dispatch@v4.0.1' '.github/workflows/label-triage.yml': [] '.github/workflows/labels.yml': [] + '.github/workflows/lock-sync-gate.yml': [] '.github/workflows/mirror.yml': - 'hyperpolymath/standards@fcb8669169b4e9f5d9848608df880ae5fae812b4' '.github/workflows/pages.yml': From d196e14483324af7db42a099aac1a81b33588123 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 22 Sep 2026 19:01:03 +0100 Subject: [PATCH 3/4] chore: empty commit to confirm the gate cure reproduces The lock-entry commit flipped the gate from 7 consecutive startup_failure runs to success. That is one observation. This empty commit takes a second, with the cured actions.lock unchanged, to distinguish a cure from a coincidence. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01X3hgXxWm6umMgZkjYyHnnm From b8403efc9a8fa14460017e45c56d903e813382e0 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 22 Sep 2026 19:21:35 +0100 Subject: [PATCH 4/4] 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 Claude-Session: https://claude.ai/code/session_01X3hgXxWm6umMgZkjYyHnnm --- scripts/check-lock-sync.sh | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/scripts/check-lock-sync.sh b/scripts/check-lock-sync.sh index 0b286d4..598aa6a 100755 --- a/scripts/check-lock-sync.sh +++ b/scripts/check-lock-sync.sh @@ -219,6 +219,33 @@ END { if (!found) { printf "FAIL %s\n lockfile entry for a workflow file that does not exist\n", p; bad = 1 } } + # --- clause 4: COVERAGE. Every workflow FILE must have a key in the lockfile, + # including one with no uses: at all - the value is then an empty list. + # MEASURED 2026-09-22, single-variable flip on two independent repos: + # hyperpolymath/verisimdb's lock-sync-gate.yml was startup_failure 7 times + # running with ZERO uses: refs, and adding + # '.github/workflows/lock-sync-gate.yml': [] + # flipped it to success; reproduced on hyperpolymath/blocky-writer, 2 of 2. + # `gh actions-lock` already emits this empty-list form for other zero-uses: + # workflows (labels.yml), so it is the generator's own convention, not ours. + # Clauses 1-3 CANNOT catch this: they ask "is every uses: locked?", and a + # workflow with no uses: satisfies them vacuously while GitHub still refuses + # to start it. 13 repos passed clauses 1-3 with exactly this gap. + nunlisted = 0; unlisted = "" + for (i = 1; i < ARGC; i++) { + q = ARGV[i]; if (q == lockfile) continue + sub(/.*\//, "", q); q = ".github/workflows/" q + if (q in seen_path) continue + nunlisted++; unlisted = unlisted "\n " q + } + if (nunlisted > 0) { + printf "FAIL actions.lock: UNLISTED WORKFLOWS\n" + printf " %d workflow file(s) have no key in the lockfile. GitHub refuses such a\n", nunlisted + printf " run at startup (jobs=0) even when the workflow has no uses: at all.\n" + printf " The entry for a zero-uses: workflow is an empty list:%s\n", unlisted + bad = 1 + } + # --- clause 3: TRANSITIVE CLOSURE. Every ref named anywhere in the lockfile # must resolve to a top-level dependencies: record. A dangling edge makes # GitHub refuse the run at startup with jobs=0. --- @@ -256,12 +283,17 @@ END { print " 3. Nested `uses:` entries must be bare OWNER/REPO@REF. A subpath pin such as" print " github/codeql-action/upload-sarif@ is REJECTED by the schema; collapse it" print " to github/codeql-action@." + print " 4. For any UNLISTED WORKFLOWS above, add the path as a lockfile key. A workflow" + print " with no uses: takes an empty list: \x27.github/workflows/x.yml\x27: []" + print " `gh actions-lock` has been observed to OMIT such a workflow entirely; that" + print " omission is itself the defect, so re-running the tool may not add it." exit 1 } printf "actions.lock is in sync and transitively closed:\n" printf " * every uses: is locked under its own workflow path (job-level reusable refs included)\n" printf " * every lockfile entry is still referenced\n" printf " * every ref named in the lockfile resolves to a dependencies: record (0 dangling edges)\n" + printf " * every workflow file has a lockfile key (zero-uses: workflows included)\n" if (nunref > 0) printf " note: %d dependencies: record(s) are unreferenced - harmless, but prunable.\n", nunref }