From 1497cbdf26d1cf7748490a74f17a8f996d58ee29 Mon Sep 17 00:00:00 2001 From: Jonathan Jewell Date: Tue, 22 Sep 2026 18:48:31 +0100 Subject: [PATCH 01/11] fix(ci): resync actions.lock and add a lock-sync recurrence gate GitHub refuses a run at startup, creating zero jobs, when a workflow carries a `uses:` ref that the lockfile does not record under that workflow's own path. It matches by LITERAL STRING; `gh actions-lock` matches by resolved commit, so a lock entry naming a tag that dereferences to the pinned SHA passes the tool and still kills the run. Regenerate the lock, make it transitively closed, and add a lock-sync gate carrying no `uses:` of its own so it cannot be disabled by the desync it detects. No workflow YAML is modified. Refs: hyperpolymath/standards#968 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01X3hgXxWm6umMgZkjYyHnnm --- .github/workflows/actions.lock | 122 +++++++---- .github/workflows/lock-sync-gate.yml | 63 ++++++ scripts/check-lock-sync.sh | 307 +++++++++++++++++++++++++++ 3 files changed, 450 insertions(+), 42 deletions(-) create mode 100644 .github/workflows/lock-sync-gate.yml create mode 100755 scripts/check-lock-sync.sh diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index 0fe1177..5687cc0 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -7,12 +7,15 @@ workflows: - 'actions/checkout@v7.0.1' '.github/workflows/codeql.yml': - 'actions/checkout@v7.0.1' - - 'github/codeql-action@v4.38.0' - '.github/workflows/governance.yml': [] - '.github/workflows/hypatia-scan.yml': [] + - 'github/codeql-action@b96794f015dfd88f77b49b1c93e0fa7110f94c63' + '.github/workflows/governance.yml': + - 'hyperpolymath/standards@84355587cb2a1f86e6882de83514a32db2646e7a' + '.github/workflows/hypatia-scan.yml': + - 'hyperpolymath/standards@84355587cb2a1f86e6882de83514a32db2646e7a' '.github/workflows/label-triage.yml': [] '.github/workflows/labels.yml': [] - '.github/workflows/mirror.yml': [] + '.github/workflows/mirror.yml': + - 'hyperpolymath/standards@d5fe075a50ab3ce4f41614d66ed77f152fda134f' '.github/workflows/pages.yml': - 'actions/checkout@v7.0.1' - 'actions/deploy-pages@v5.0.1' @@ -22,9 +25,21 @@ workflows: '.github/workflows/registry-validate.yml': - 'actions/checkout@v7.0.1' - 'julia-actions/setup-julia@v3.0.2' - '.github/workflows/scorecard.yml': [] - '.github/workflows/secret-scanner.yml': [] + '.github/workflows/scorecard.yml': + - 'hyperpolymath/standards@84355587cb2a1f86e6882de83514a32db2646e7a' + '.github/workflows/secret-scanner.yml': + - 'hyperpolymath/standards@84355587cb2a1f86e6882de83514a32db2646e7a' dependencies: + 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9': + ref: '55cc8345863c7cc4c66a329aec7e433d2d1c52a9' + commit: 'sha1-55cc8345863c7cc4c66a329aec7e433d2d1c52a9' + owner_id: 44036562 + repo_id: 215566462 + 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1': + ref: '3d3c42e5aac5ba805825da76410c181273ba90b1' + commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1' + owner_id: 44036562 + repo_id: 197814629 'actions/checkout@v7.0.1': ref: 'v7.0.1' commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1' @@ -35,6 +50,11 @@ dependencies: commit: 'sha1-368f82528645a54fb793d4d04e342629a3f51346' owner_id: 44036562 repo_id: 438112499 + 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a': + ref: '043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' + commit: 'sha1-043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' + owner_id: 44036562 + repo_id: 192625955 'actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f': ref: 'v7.0.0' commit: 'sha1-bbbca2ddaa5d8feaa63e36b76fdaad77386f024f' @@ -47,63 +67,81 @@ dependencies: repo_id: 496012378 uses: - 'actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f' - 'github/codeql-action@v4.38.0': - ref: 'v4.38.0' - commit: 'sha1-b96794f015dfd88f77b49b1c93e0fa7110f94c63' - owner_id: 9919 - repo_id: 259445878 - 'hyperpolymath/smtp-notify-action@v0.3.0': - ref: 'v0.3.0' - commit: 'sha1-22e7bdb322c430c1d0dac6b3bb307f4bb139d0be' - owner_id: 6759885 - repo_id: 1352485172 - 'julia-actions/setup-julia@v3.0.2': - ref: 'v3.0.2' - commit: 'sha1-fa02766e078afaaf09b14210362cee14137e6a32' - owner_id: 53965732 - repo_id: 202020219 - 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9': - ref: 'v6.1.0' - commit: 'sha1-55cc8345863c7cc4c66a329aec7e433d2d1c52a9' - owner_id: 44036562 - repo_id: 215566462 - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1': - ref: 'v7.0.1' - commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1' - owner_id: 44036562 - repo_id: 197814629 - 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a': - ref: 'v7.0.1' - commit: 'sha1-043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' - owner_id: 44036562 - repo_id: 192625955 'denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed': - ref: 'v2.0.5' + ref: '22d081ff2d3a40755e97629de92e3bcbfa7cf2ed' commit: 'sha1-22d081ff2d3a40755e97629de92e3bcbfa7cf2ed' owner_id: 42048915 repo_id: 356423100 'dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772': - ref: 'stable' + ref: '6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772' commit: 'sha1-6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772' owner_id: 1940490 repo_id: 260749683 'editorconfig-checker/action-editorconfig-checker@840e866d93b8e032123c23bac69dece044d4d84c': - ref: 'v2.2.0' + ref: '840e866d93b8e032123c23bac69dece044d4d84c' commit: 'sha1-840e866d93b8e032123c23bac69dece044d4d84c' owner_id: 26415196 repo_id: 297874902 'erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124': - ref: 'v1.24.1' + ref: '54075bcc5e249e4758d363f27d099f55d843f124' commit: 'sha1-54075bcc5e249e4758d363f27d099f55d843f124' owner_id: 47606891 repo_id: 331103973 + 'github/codeql-action@b96794f015dfd88f77b49b1c93e0fa7110f94c63': + ref: 'b96794f015dfd88f77b49b1c93e0fa7110f94c63' + commit: 'sha1-b96794f015dfd88f77b49b1c93e0fa7110f94c63' + owner_id: 9919 + repo_id: 259445878 + 'github/codeql-action@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28': + ref: 'db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28' + commit: 'sha1-db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28' + owner_id: 9919 + repo_id: 259445878 + 'github/codeql-action@v4.38.0': + ref: 'v4.38.0' + commit: 'sha1-b96794f015dfd88f77b49b1c93e0fa7110f94c63' + owner_id: 9919 + repo_id: 259445878 + 'hyperpolymath/smtp-notify-action@v0.3.0': + ref: 'v0.3.0' + commit: 'sha1-22e7bdb322c430c1d0dac6b3bb307f4bb139d0be' + owner_id: 6759885 + repo_id: 1352485172 + 'hyperpolymath/standards@84355587cb2a1f86e6882de83514a32db2646e7a': + ref: '84355587cb2a1f86e6882de83514a32db2646e7a' + commit: 'sha1-84355587cb2a1f86e6882de83514a32db2646e7a' + owner_id: 6759885 + repo_id: 1116521501 + uses: + - 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' + - 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' + - 'denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed' + - 'editorconfig-checker/action-editorconfig-checker@840e866d93b8e032123c23bac69dece044d4d84c' + - 'erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124' + - 'github/codeql-action@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28' + - 'ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc' + 'hyperpolymath/standards@d5fe075a50ab3ce4f41614d66ed77f152fda134f': + ref: 'd5fe075a50ab3ce4f41614d66ed77f152fda134f' + commit: 'sha1-d5fe075a50ab3ce4f41614d66ed77f152fda134f' + owner_id: 6759885 + repo_id: 1116521501 + uses: + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' + - 'dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772' + - 'webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555' + 'julia-actions/setup-julia@v3.0.2': + ref: 'v3.0.2' + commit: 'sha1-fa02766e078afaaf09b14210362cee14137e6a32' + owner_id: 53965732 + repo_id: 202020219 'ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc': - ref: 'v2.4.4' + ref: '2d1146689b8cda280b9bc96326124645441f03bc' commit: 'sha1-2d1146689b8cda280b9bc96326124645441f03bc' owner_id: 67707773 repo_id: 421101922 'webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555': - ref: 'v0.10.0' + ref: 'e83874834305fe9a4a2997156cb26c5de65a8555' commit: 'sha1-e83874834305fe9a4a2997156cb26c5de65a8555' owner_id: 135788 repo_id: 208510314 diff --git a/.github/workflows/lock-sync-gate.yml b/.github/workflows/lock-sync-gate.yml new file mode 100644 index 0000000..936c873 --- /dev/null +++ b/.github/workflows/lock-sync-gate.yml @@ -0,0 +1,63 @@ +# SPDX-License-Identifier: MPL-2.0 +name: Lock Sync Gate + +# Fails any pull request whose .github/workflows/actions.lock has drifted from +# the workflow YAML. That drift is not cosmetic: GitHub refuses such a run at +# startup, creating ZERO jobs, and reports only "This run likely failed because +# of a workflow file issue." A single grouped Dependabot bump can take out most +# of a repository's CI that way, because Dependabot rewrites `uses:` refs in the +# YAML and cannot touch the lockfile. Measured across 200 repositories on +# 2026-09-22: 39 had silently dead CI from exactly this cause. +# See hyperpolymath/standards#968. +# +# This workflow deliberately carries NO `uses:` of its own. It checks out by +# calling git in a `run:` step instead of using actions/checkout, so it has no +# lockfile entry to go stale and is structurally immune to the very failure it +# detects. Do not add a `uses:` to this file. +# +# There is also no `paths:` filter, on purpose: a filtered workflow never +# reports on pull requests that miss the filter, which deadlocks any branch +# ruleset that requires this check. + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +concurrency: + group: lock-sync-gate-${{ github.ref }} + cancel-in-progress: true + +jobs: + lock-sync: + name: actions.lock is in sync with the workflow YAML + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Check out without actions/checkout + env: + REPO: ${{ github.repository }} + SHA: ${{ github.event.pull_request.head.sha || github.sha }} + TOKEN: ${{ github.token }} + run: | + set -euo pipefail + # Authenticate the fetch. An anonymous clone works only for public + # repositories; this gate must also run on private ones. The header + # form is used rather than a token in the remote URL so the + # credential is never written into .git/config. + AUTH="AUTHORIZATION: basic $(printf 'x-access-token:%s' "${TOKEN}" | base64 -w0)" + git init -q . + git remote add origin "https://github.com/${REPO}.git" + git -c http.extraheader="${AUTH}" fetch -q --depth 1 origin "${SHA}" + git checkout -q FETCH_HEAD + echo "checked out ${SHA}" + + - name: Verify lockfile synchronisation + run: | + set -euo pipefail + test -x scripts/check-lock-sync.sh \ + || { echo "::error::scripts/check-lock-sync.sh missing or not executable"; exit 1; } + ./scripts/check-lock-sync.sh diff --git a/scripts/check-lock-sync.sh b/scripts/check-lock-sync.sh new file mode 100755 index 0000000..bbe283d --- /dev/null +++ b/scripts/check-lock-sync.sh @@ -0,0 +1,307 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# +# check-lock-sync.sh — verify .github/workflows/actions.lock is in sync with the +# workflow YAML, in BOTH directions (including job-level reusable-workflow refs), +# AND that the lockfile is TRANSITIVELY CLOSED. +# +# Three clauses, each of which alone is insufficient: +# +# 1. every `uses:` in a workflow is locked under THAT workflow's own path; +# 2. every lockfile entry is still referenced by its workflow (no orphans); +# 3. every ref NAMED anywhere in the lockfile resolves to a top-level +# `dependencies:` record — the lockfile has no dangling edges. +# +# Clause 3 is not decoration. It is the clause that catches the failure mode that +# clauses 1 and 2 are structurally blind to, and it was added only after that +# blindness was measured. On hyperpolymath/cicd-squabbler, 2026-09-22: +# +# commit dangling-edge class result +# fe22bbc workflows: -> dependencies: (ref listed, no record) 4 workflows startup_failure, jobs=0 +# cfadcf9 dependencies: -> dependencies: (record added, its +# own nested uses: unrecorded) the same 4 still startup_failure +# 5286aa5 none - transitively closed 0 startup_failure, all 17 runs create jobs +# +# At fe22bbc AND cfadcf9 this script exited 0, `gh actions-lock --verify-local` +# exited 0, and the Lock Sync Gate reported green - while GitHub was refusing to +# start four workflows. Every local gate was green on a fatal commit. That is the +# guard/consumer trap: the gate asked "is every uses: locked?" and GitHub asks +# "is every locked ref RESOLVABLE?". +# +# The asymmetry that makes clause 3 mandatory, and counter-intuitive: +# * a job-level ref ABSENT from the lockfile entirely is HARMLESS; +# * a ref PRESENT in the lockfile but unresolvable is FATAL. +# So adding entries without closing them is strictly worse than adding nothing. +# Clause 1 demands entries be added; only clause 3 makes that demand safe. Shipping +# clause 1 without clause 3 actively steers a developer into the fatal state: +# Dependabot bumps a job-level ref -> clause 1 reds -> `gh actions-lock` is blind to +# job-level refs and will not backfill -> the developer hand-adds the workflows: +# entry to get green -> no dependencies: record -> CI dies silently, gate green. +# +# Exit 0 only when all three clauses hold. Any violation exits 1. There is no +# warn-only mode: a desync means GitHub refuses to start the run, so it must fail +# the job. A `::warning::` cannot fail a job and would be a vacuous gate. + +set -euo pipefail + +WF_DIR="${1:-.github/workflows}" +LOCK="$WF_DIR/actions.lock" + +# gawk is required: the parser uses 3-argument match(), a GNU extension. mawk +# (the Debian/Ubuntu default `awk`) does not support it, and a silent parse +# failure here would read as a clean pass - the exact failure mode this script +# exists to prevent. Probe it rather than trusting the name. +AWK="" +for cand in gawk awk; do + if command -v "$cand" >/dev/null 2>&1 \ + && echo x | "$cand" '{ if (match($0, /(x)/, m) && m[1] == "x") exit 0; exit 1 }' 2>/dev/null; then + AWK="$cand"; break + fi +done +if [ -z "$AWK" ]; then + echo "check-lock-sync: FATAL: no awk supporting 3-argument match() (need gawk)" >&2 + echo "check-lock-sync: install it with: sudo apt-get install -y gawk" >&2 + exit 1 +fi + +if [ ! -f "$LOCK" ]; then + echo "check-lock-sync: FATAL: no lockfile at $LOCK" >&2 + exit 1 +fi + +shopt -s nullglob +mapfile -t WORKFLOWS < <(printf '%s\n' "$WF_DIR"/*.yml "$WF_DIR"/*.yaml | sort -u) +if [ "${#WORKFLOWS[@]}" -eq 0 ]; then + echo "check-lock-sync: FATAL: no workflow files under $WF_DIR" >&2 + exit 1 +fi + +read -r -d '' PROG <<'AWK' || true +# owner/repo[/subpath...]@ref -> owner/repo@ref ("" if not an external ref) +function norm(r, at, path, ref, n, parts) { + at = 0 + for (n = length(r); n > 0; n--) { if (substr(r, n, 1) == "@") { at = n; break } } + if (at == 0) return "" + path = substr(r, 1, at - 1); ref = substr(r, at + 1) + if (path == "" || ref == "") return "" + if (substr(path, 1, 2) == "./" || substr(path, 1, 2) == "$/") return "" # local action + if (split(path, parts, "/") < 2) return "" + return parts[1] "/" parts[2] "@" ref +} + +# Fold case on the OWNER/REPO segment only, for comparison keys. GitHub resolves +# owner and repository names case-insensitively, and this is measured, not assumed: +# metadatastician/pong-ping's lockfile records sonarsource/sonarqube-scan-action@v8.2.1 +# while sonarqube.yml says SonarSource/..., and at commit cd5f90f that workflow ran +# SUCCESS while codeql.yml at the SAME commit was startup_failure. A same-commit +# control, so the case difference is provably not what kills a run. +# The REF is NOT folded: git tags and branch names are case-sensitive. +function ck(r, at, s) { + at = 0 + for (s = length(r); s > 0; s--) { if (substr(r, s, 1) == "@") { at = s; break } } + if (at == 0) return tolower(r) + return tolower(substr(r, 1, at - 1)) substr(r, at) +} + +# ---------- pass 1: the lockfile ---------- +FILENAME == lockfile { + if ($0 ~ /^workflows:[[:space:]]*$/) { inwf = 1; indep = 0; next } + if ($0 ~ /^dependencies:[[:space:]]*$/) { inwf = 0; indep = 1; next } + if ($0 ~ /^[a-z_]+:/) { inwf = 0; indep = 0; next } + + # --- the dependencies: section, for clause 3 --- + if (indep) { + # " 'owner/repo@ref':" -- a top-level dependency record + if (match($0, /^ '([^']+)':/, m)) { + depkey = m[1] + haverec[ck(depkey)] = 1; disp[ck(depkey)] = depkey + next + } + # " - 'owner/repo@ref'" -- a nested uses: of that record + if (match($0, /^ - '([^']+)'/, m) && depkey != "") { + r = ck(m[1]); disp[r] = m[1] + want[r] = 1 + wantsrc[r] = wantsrc[r] " dependencies:" depkey + next + } + next + } + + if (!inwf) next + + # " '.github/workflows/x.yml':" or "... : []" + if (match($0, /^ '([^']+)':/, m)) { + cur = m[1] + seen_path[cur] = 1 + next + } + if (match($0, /^ - '([^']+)'[[:space:]]*$/, m) && cur != "") { + lr = ck(m[1]); disp[lr] = m[1]; lock[cur, lr] = 1 + lockcount[cur]++ + want[lr] = 1 + wantsrc[lr] = wantsrc[lr] " " cur + next + } + next +} + +# ---------- pass 2: the workflow YAML ---------- +FNR == 1 { wf = FILENAME } +{ + line = $0 + sub(/[[:space:]]+#.*$/, "", line) # strip trailing comment + if (match(line, /^[[:space:]]*-?[[:space:]]*uses:[[:space:]]*(.+)$/, m)) { + raw = m[1] + gsub(/^["']|["']$/, "", raw) + gsub(/[[:space:]]+$/, "", raw) + if (raw ~ /^\$\//) { dollar[wf] = dollar[wf] " " raw; next } # known corruption + n = norm(raw) + if (n != "") { + uses[wf, ck(n)] = 1 + # A JOB-LEVEL reusable-workflow ref is owner/repo/.github/workflows/.yml@ref. + # A STEP-LEVEL action ref is anything else. The distinction is load-bearing: + # see clause 1. + if (raw ~ /\/\.github\/workflows\/[^@]*\.ya?ml@/) joblist[wf] = joblist[wf] " " n + else steplist[wf] = steplist[wf] " " n + useslist[wf] = useslist[wf] " " n + } + } +} + +END { + bad = 0 + for (i = 1; i < ARGC; i++) { + wf = ARGV[i] + if (wf == lockfile) continue + key = wf + sub(/.*\//, "", key) + key = ".github/workflows/" key # the lockfile always uses this canonical path + + if (dollar[wf] != "") { + printf "FAIL %s\n invalid local-action rewrite (uses: $/...):%s\n", key, dollar[wf] + bad = 1 + } + + # --- clause 1: every STEP-LEVEL uses: must be locked under THIS path --- + # + # Only step-level action refs are required. A job-level reusable-workflow ref + # that is ABSENT from the lockfile is harmless - this file's own header has + # said so since it was written ("a job-level ref ABSENT from the lockfile + # entirely is HARMLESS; a ref PRESENT in the lockfile but unresolvable is + # FATAL"), but clause 1 used to fail on it anyway. That was an internal + # contradiction, and it is measured, not argued: + # + # * metadatastician/universal-modding-studio and idaptik-ums: scorecard.yml + # is a pure reusable caller with NO lockfile entry at all -> runs, jobs>0. + # * hyperpolymath/standards mirror.yml: empty lock entry, job-level ref + # unlocked -> 7 jobs created. + # * hyperpolymath/my-lang: four workflows share ONE identical stale entry; + # two succeed and two startup-fail, so the entry is not the discriminator. + # What separates them is clause 3 - whether the callee's own refs resolve + # to dependencies: records in THIS lockfile. + # + # Failing on an absent job-level ref also steers the developer into the fatal + # state: gh actions-lock will not backfill job-level refs, so the only way to + # go green was to hand-add a workflows: entry with no dependencies: record - + # which is precisely the dangling edge clause 3 exists to catch. + nu = split(steplist[wf], u, " ") + delete uniq; missing = "" + for (j = 1; j <= nu; j++) { + if (u[j] == "" || (u[j] in uniq)) continue + uniq[u[j]] = 1 + if (!((key SUBSEP ck(u[j])) in lock)) missing = missing " " u[j] + } + if (missing != "") { + if (!(key in seen_path)) + printf "FAIL %s\n not onboarded: no lockfile entry for this path\n unlocked step-level refs:%s\n", key, missing + else + printf "FAIL %s\n step-level refs missing from the lockfile:%s\n", key, missing + bad = 1 + } + + # Job-level reusable refs: reported, never fatal. If one IS locked, clause 3 + # still requires its callee graph to be closed. + njm = split(joblist[wf], v, " ") + delete juniq; jmissing = "" + for (j = 1; j <= njm; j++) { + if (v[j] == "" || (v[j] in juniq)) continue + juniq[v[j]] = 1 + if (!((key SUBSEP ck(v[j])) in lock)) jmissing = jmissing " " v[j] + } + if (jmissing != "") jnote = jnote sprintf("\n %s:%s", key, jmissing) + + # --- clause 2: every lock entry must be referenced by this workflow --- + orphan = "" + for (k in lock) { + split(k, kp, SUBSEP) + if (kp[1] != key) continue + if (!((wf SUBSEP kp[2]) in uses)) orphan = orphan " " (kp[2] in disp ? disp[kp[2]] : kp[2]) + } + if (orphan != "") { + printf "FAIL %s\n stale lockfile entries, no uses: references them:%s\n", key, orphan + bad = 1 + } + } + + # --- lockfile entries for workflow files that no longer exist --- + for (p in seen_path) { + found = 0 + for (i = 1; i < ARGC; i++) { + q = ARGV[i]; if (q == lockfile) continue + sub(/.*\//, "", q); q = ".github/workflows/" q + if (q == p) { found = 1; break } + } + if (!found) { printf "FAIL %s\n lockfile entry for a workflow file that does not exist\n", p; 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. --- + ndang = 0; dang = "" + for (r in want) { + if (r !~ /^[^\/]+\/[^\/@]+@/) continue # not an OWNER/REPO@REF pin; not ours to resolve + if (r in haverec) continue + ndang++ + dang = dang sprintf("\n %s\n named by:%s", (r in disp ? disp[r] : r), wantsrc[r]) + } + if (ndang > 0) { + printf "FAIL actions.lock: DANGLING EDGES\n" + printf " %d ref(s) are named in the lockfile but have no top-level dependencies: record.%s\n", ndang, dang + bad = 1 + } + + # --- a dependencies: record nothing names is dead weight, not fatal: report only --- + nunref = 0 + for (d in haverec) if (!(d in want)) nunref++ + + if (bad) { + print "" + print "actions.lock is OUT OF SYNC with the workflow YAML, or is not transitively closed." + print "GitHub refuses such a run at startup: zero jobs are created and the run" + print "reports \"This run likely failed because of a workflow file issue.\"" + print "" + print "Fix, in this order:" + print " 1. `gh actions-lock --no-migrate-local-actions`, then review the diff. It does" + print " NOT handle job-level reusable-workflow refs and it can de-pin bare SHAs to" + print " floating tags - both must be corrected by hand." + print " 2. For any DANGLING EDGES above, add a top-level `dependencies:` record for each" + print " ref. A leaf record may legally omit the nested `uses:` key entirely, so adding" + print " leaves introduces no new dangling edges and closure terminates in one pass." + print " Keys are sorted with LC_ALL=C collation (ASCII '-' 0x2d sorts before '@' 0x40)." + 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@." + 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" + if (nunref > 0) + printf " note: %d dependencies: record(s) are unreferenced - harmless, but prunable.\n", nunref + if (jnote != "") + printf " note: job-level reusable refs not locked (harmless; see clause 1):%s\n", jnote +} +AWK + +"$AWK" -v lockfile="$LOCK" "$PROG" "$LOCK" "${WORKFLOWS[@]}" From 15602806093ada20d150349c9ee4246f646916e5 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 22 Sep 2026 19:02:53 +0100 Subject: [PATCH 02/11] fix(ci): give lock-sync-gate.yml an actions.lock entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A workflow absent from actions.lock can be rejected at startup (startup_failure, jobs=0) even when it carries zero real 'uses:' refs and so has nothing to pin. The gate is deliberately zero-'uses:', which is exactly why it had no entry. Measured on two repos in this batch: adding this single line flipped the gate from 7 consecutive startup_failure runs to success on hyperpolymath/verisimdb (two successes since, nothing else changed) and from 2 of 2 startup_failure to success on hyperpolymath/blocky-writer. Enforcement is not uniform across repos — 13 of the 14 repos in this batch start the byte-identical gate today with the same gap. A repo that passes now is not evidence its lock is complete, only that the behaviour has not reached it. This closes the gap before it bites. Zero-'uses:' workflows take the empty list, matching the entries actions.lock already carries for other zero-'uses:' workflows such as labels.yml. 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 5687cc0..26f5060 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -14,6 +14,7 @@ workflows: - 'hyperpolymath/standards@84355587cb2a1f86e6882de83514a32db2646e7a' '.github/workflows/label-triage.yml': [] '.github/workflows/labels.yml': [] + '.github/workflows/lock-sync-gate.yml': [] '.github/workflows/mirror.yml': - 'hyperpolymath/standards@d5fe075a50ab3ce4f41614d66ed77f152fda134f' '.github/workflows/pages.yml': From 6ccb346510260461bda01ad1219afcc79979b515 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 22 Sep 2026 19:23:17 +0100 Subject: [PATCH 03/11] 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 bbe283d..5ba2d12 100755 --- a/scripts/check-lock-sync.sh +++ b/scripts/check-lock-sync.sh @@ -254,6 +254,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. --- @@ -291,12 +318,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 if (jnote != "") From ebd63f76210a399375dcb5125dba47bceac609b9 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 22 Sep 2026 20:14:29 +0100 Subject: [PATCH 04/11] Update .github/workflows/lock-sync-gate.yml Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Jonathan D.A. Jewell <6759885+hyperpolymath@users.noreply.github.com> --- .github/workflows/lock-sync-gate.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lock-sync-gate.yml b/.github/workflows/lock-sync-gate.yml index 936c873..d89e3dd 100644 --- a/.github/workflows/lock-sync-gate.yml +++ b/.github/workflows/lock-sync-gate.yml @@ -40,7 +40,7 @@ jobs: - name: Check out without actions/checkout env: REPO: ${{ github.repository }} - SHA: ${{ github.event.pull_request.head.sha || github.sha }} + SHA: ${{ github.sha }} TOKEN: ${{ github.token }} run: | set -euo pipefail From b6980568057269f31045a508bf22389d323bf238 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 22 Sep 2026 20:14:44 +0100 Subject: [PATCH 05/11] Update scripts/check-lock-sync.sh Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Jonathan D.A. Jewell <6759885+hyperpolymath@users.noreply.github.com> --- scripts/check-lock-sync.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/check-lock-sync.sh b/scripts/check-lock-sync.sh index 5ba2d12..3860ba4 100755 --- a/scripts/check-lock-sync.sh +++ b/scripts/check-lock-sync.sh @@ -154,7 +154,6 @@ FNR == 1 { wf = FILENAME } raw = m[1] gsub(/^["']|["']$/, "", raw) gsub(/[[:space:]]+$/, "", raw) - if (raw ~ /^\$\//) { dollar[wf] = dollar[wf] " " raw; next } # known corruption n = norm(raw) if (n != "") { uses[wf, ck(n)] = 1 From f1d4fb2d2782777659d2609bdab6ee9539fb9b26 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 19:18:08 +0000 Subject: [PATCH 06/11] docs(ci): clarify action reference normalization in lock-sync check --- scripts/check-lock-sync.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/check-lock-sync.sh b/scripts/check-lock-sync.sh index 3860ba4..d873bf6 100755 --- a/scripts/check-lock-sync.sh +++ b/scripts/check-lock-sync.sh @@ -77,7 +77,8 @@ if [ "${#WORKFLOWS[@]}" -eq 0 ]; then fi read -r -d '' PROG <<'AWK' || true -# owner/repo[/subpath...]@ref -> owner/repo@ref ("" if not an external ref) +# Reduce an external `uses:` value to owner/repository@ref, discarding any +# subpath. Return "" for local values or values without an owner/repository@ref. function norm(r, at, path, ref, n, parts) { at = 0 for (n = length(r); n > 0; n--) { if (substr(r, n, 1) == "@") { at = n; break } } From 92555b001c7cd05f409b923dbee31a36c16d5647 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 19:19:10 +0000 Subject: [PATCH 07/11] fix(ci): recognize quoted and spaced uses keys in lock-sync checks Add regression coverage for orphan detection and missing lockfile references. --- scripts/check-lock-sync.sh | 4 +-- tests/check-lock-sync.sh | 71 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 tests/check-lock-sync.sh diff --git a/scripts/check-lock-sync.sh b/scripts/check-lock-sync.sh index d873bf6..602ab52 100755 --- a/scripts/check-lock-sync.sh +++ b/scripts/check-lock-sync.sh @@ -151,8 +151,8 @@ FNR == 1 { wf = FILENAME } { line = $0 sub(/[[:space:]]+#.*$/, "", line) # strip trailing comment - if (match(line, /^[[:space:]]*-?[[:space:]]*uses:[[:space:]]*(.+)$/, m)) { - raw = m[1] + if (match(line, /^[[:space:]]*-?[[:space:]]*(uses|"uses"|'uses')[[:space:]]*:[[:space:]]*(.+)$/, m)) { + raw = m[2] gsub(/^["']|["']$/, "", raw) gsub(/[[:space:]]+$/, "", raw) n = norm(raw) diff --git a/tests/check-lock-sync.sh b/tests/check-lock-sync.sh new file mode 100644 index 0000000..a20720c --- /dev/null +++ b/tests/check-lock-sync.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +CHECK="$REPO_ROOT/scripts/check-lock-sync.sh" +TMP_ROOT="$(mktemp -d)" +trap 'rm -rf "$TMP_ROOT"' EXIT + +write_workflow() { + local workflow_dir="$1" + + mkdir -p "$workflow_dir" + cat >"$workflow_dir/example.yml" <<'YAML' +name: Parser regression +on: push +jobs: + check: + runs-on: ubuntu-latest + steps: + - 'uses': owner/single-quoted@v1 + - "uses": owner/double-quoted@v1 + - uses : owner/spaced-colon@v1 + - uses: $/path/to/local-action@v1 +YAML +} + +pass_dir="$TMP_ROOT/pass" +write_workflow "$pass_dir" +cat >"$pass_dir/actions.lock" <<'YAML' +workflows: + '.github/workflows/example.yml': + - 'owner/single-quoted@v1' + - 'owner/double-quoted@v1' + - 'owner/spaced-colon@v1' +dependencies: + 'owner/single-quoted@v1': + 'owner/double-quoted@v1': + 'owner/spaced-colon@v1': +YAML + +pass_output="$($CHECK "$pass_dir" 2>&1)" || { + printf 'expected quoted and spaced uses keys to satisfy orphan checks\n%s\n' "$pass_output" >&2 + exit 1 +} +if grep -q 'stale lockfile entries' <<<"$pass_output"; then + printf 'quoted or spaced uses key was incorrectly reported as an orphan\n%s\n' "$pass_output" >&2 + exit 1 +fi + +missing_dir="$TMP_ROOT/missing" +write_workflow "$missing_dir" +cat >"$missing_dir/actions.lock" <<'YAML' +workflows: + '.github/workflows/example.yml': [] +dependencies: +YAML + +if missing_output="$($CHECK "$missing_dir" 2>&1)"; then + printf 'expected unlocked references with quoted and spaced uses keys to fail\n' >&2 + exit 1 +fi +for ref in owner/single-quoted@v1 owner/double-quoted@v1 owner/spaced-colon@v1; do + if ! grep -q "step-level refs missing from the lockfile:.*$ref" <<<"$missing_output"; then + printf 'missing-reference output did not include %s\n%s\n' "$ref" "$missing_output" >&2 + exit 1 + fi +done + +printf 'check-lock-sync parser regressions passed\n' From 12cd50bfdd30c0334f682efcffeb04dcecdd97da Mon Sep 17 00:00:00 2001 From: claude Date: Tue, 22 Sep 2026 20:30:58 +0100 Subject: [PATCH 08/11] fix(actions-lock): realign the codeql-action pin after main moved Merging main brings codeql.yml to github/codeql-action@1c5b6756, while the lockfile still named b96794f0. The gate runs against the MERGE ref, so it failed there while passing on the branch head -- the desync only exists in the merged tree. Note for the record: b96794f0 is the true v4.38.0 (the annotated tag derefs to it). The pin on main is 1c5b6756 with a '# v4.38.0' comment, which is a different commit dated 2026-09-18 -- the comment does not describe the pin. The lockfile must match the YAML literally, so it now names 1c5b6756; the mislabelled comment is reported separately and not changed here. Verified against the standards verifier fetched at the PINNED SHA (exit 0) and the lock-sync gate (exit 0). No workflow YAML changed by this commit. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01X3hgXxWm6umMgZkjYyHnnm --- .github/workflows/actions.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index 26f5060..46618e2 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -7,7 +7,7 @@ workflows: - 'actions/checkout@v7.0.1' '.github/workflows/codeql.yml': - 'actions/checkout@v7.0.1' - - 'github/codeql-action@b96794f015dfd88f77b49b1c93e0fa7110f94c63' + - 'github/codeql-action@1c5b675653bb5c22dbe9b12b556ec555138e09fd' '.github/workflows/governance.yml': - 'hyperpolymath/standards@84355587cb2a1f86e6882de83514a32db2646e7a' '.github/workflows/hypatia-scan.yml': @@ -88,9 +88,9 @@ dependencies: commit: 'sha1-54075bcc5e249e4758d363f27d099f55d843f124' owner_id: 47606891 repo_id: 331103973 - 'github/codeql-action@b96794f015dfd88f77b49b1c93e0fa7110f94c63': - ref: 'b96794f015dfd88f77b49b1c93e0fa7110f94c63' - commit: 'sha1-b96794f015dfd88f77b49b1c93e0fa7110f94c63' + 'github/codeql-action@1c5b675653bb5c22dbe9b12b556ec555138e09fd': + ref: '1c5b675653bb5c22dbe9b12b556ec555138e09fd' + commit: 'sha1-1c5b675653bb5c22dbe9b12b556ec555138e09fd' owner_id: 9919 repo_id: 259445878 'github/codeql-action@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28': @@ -100,7 +100,7 @@ dependencies: repo_id: 259445878 'github/codeql-action@v4.38.0': ref: 'v4.38.0' - commit: 'sha1-b96794f015dfd88f77b49b1c93e0fa7110f94c63' + commit: 'sha1-1c5b675653bb5c22dbe9b12b556ec555138e09fd' owner_id: 9919 repo_id: 259445878 'hyperpolymath/smtp-notify-action@v0.3.0': From 36734209ca9ee8311c871c911d7f0127f0fa4759 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 19:44:55 +0000 Subject: [PATCH 09/11] docs(tests): clarify shared workflow fixture in lock-sync tests --- tests/check-lock-sync.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/check-lock-sync.sh b/tests/check-lock-sync.sh index a20720c..c872140 100644 --- a/tests/check-lock-sync.sh +++ b/tests/check-lock-sync.sh @@ -8,6 +8,7 @@ CHECK="$REPO_ROOT/scripts/check-lock-sync.sh" TMP_ROOT="$(mktemp -d)" trap 'rm -rf "$TMP_ROOT"' EXIT +# Write the workflow fixture shared by the passing and missing-reference cases. write_workflow() { local workflow_dir="$1" From 76d28a219b633e9cbdf16b69437f4b56496c4b2c Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 22 Sep 2026 22:12:48 +0100 Subject: [PATCH 10/11] Update scripts/check-lock-sync.sh Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Jonathan D.A. Jewell <6759885+hyperpolymath@users.noreply.github.com> --- scripts/check-lock-sync.sh | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/scripts/check-lock-sync.sh b/scripts/check-lock-sync.sh index 602ab52..5233f0e 100755 --- a/scripts/check-lock-sync.sh +++ b/scripts/check-lock-sync.sh @@ -201,8 +201,8 @@ END { # to dependencies: records in THIS lockfile. # # Failing on an absent job-level ref also steers the developer into the fatal - # state: gh actions-lock will not backfill job-level refs, so the only way to - # go green was to hand-add a workflows: entry with no dependencies: record - + # state: gh actions-lock will not backfill job-level refs, so the only way + # to go green was to hand-add a workflows: entry with no dependencies: record - # which is precisely the dangling edge clause 3 exists to catch. nu = split(steplist[wf], u, " ") delete uniq; missing = "" @@ -219,8 +219,8 @@ END { bad = 1 } - # Job-level reusable refs: reported, never fatal. If one IS locked, clause 3 - # still requires its callee graph to be closed. + # Job-level reusable refs are mandatory. If one IS locked, clause 3 still + # requires its callee graph to be closed. njm = split(joblist[wf], v, " ") delete juniq; jmissing = "" for (j = 1; j <= njm; j++) { @@ -228,7 +228,10 @@ END { juniq[v[j]] = 1 if (!((key SUBSEP ck(v[j])) in lock)) jmissing = jmissing " " v[j] } - if (jmissing != "") jnote = jnote sprintf("\n %s:%s", key, jmissing) + if (jmissing != "") { + printf "FAIL %s\n job-level reusable-workflow refs missing from the lockfile:%s\n", key, jmissing + bad = 1 + } # --- clause 2: every lock entry must be referenced by this workflow --- orphan = "" From 3be2f818d65d637738e69db0259e27188dade72f Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 22 Sep 2026 22:13:06 +0100 Subject: [PATCH 11/11] Update tests/check-lock-sync.sh Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Jonathan D.A. Jewell <6759885+hyperpolymath@users.noreply.github.com> --- tests/check-lock-sync.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/check-lock-sync.sh b/tests/check-lock-sync.sh index c872140..e542cc1 100644 --- a/tests/check-lock-sync.sh +++ b/tests/check-lock-sync.sh @@ -23,7 +23,7 @@ jobs: - 'uses': owner/single-quoted@v1 - "uses": owner/double-quoted@v1 - uses : owner/spaced-colon@v1 - - uses: $/path/to/local-action@v1 + - uses: $/path/to/local-action YAML }