From fe22bbc5cbebb35a1ad2cebe5238ed1e32272ab3 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 22 Sep 2026 12:30:40 +0100 Subject: [PATCH 1/9] fix(ci): resync actions.lock and add a lock-sync gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI in this repository has been silently dead: `.github/workflows/actions.lock` drifted from the workflow YAML, and GitHub refuses such a run at startup — zero jobs are created and the run reports only "This run likely failed because of a workflow file issue." Three changes: * `actions.lock` regenerated with `gh actions-lock --no-migrate-local-actions --no-narrow --no-interactive`, which repaired the step-level ref `step-security/harden-runner`. `--no-migrate-local-actions` is mandatory: fix mode otherwise rewrites `uses: ./…` to the invalid `uses: $/…`, which itself causes startup death. * Six job-level reusable-workflow refs added to `actions.lock` by hand. `gh actions-lock` v0.1.6 cannot see a job-level `uses:` (upstream #129), so it can neither report nor repair these. * `scripts/check-lock-sync.sh` + `.github/workflows/lock-sync-gate.yml` close the recurrence. Dependabot rewrites `uses:` refs and cannot touch the lockfile, so a grouped actions update re-breaks the repository on its next run. The gate fails such a pull request before it merges. It carries no `uses:` of its own — it checks out via git in a `run:` step — so it cannot be killed by the very desync it detects, and it has no `paths:` filter, so it cannot deadlock a ruleset that requires it. Verified before commit: all three mutants killed (a deleted step-level entry, a deleted job-level entry, an injected `$/` rewrite), positive control clean, `gh actions-lock --verify-local` clean, and only these three files modified. Refs: hyperpolymath/standards#968 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01X3hgXxWm6umMgZkjYyHnnm --- .github/workflows/actions.lock | 76 +++---------- .github/workflows/lock-sync-gate.yml | 63 +++++++++++ scripts/check-lock-sync.sh | 157 +++++++++++++++++++++++++++ 3 files changed, 236 insertions(+), 60 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 42be1e9..446868d 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -10,21 +10,24 @@ workflows: - 'actions/checkout@v7.0.1' '.github/workflows/dependabot-automerge.yml': - 'dependabot/fetch-metadata@v3.1.0' - - 'step-security/harden-runner@v2.9.1' + - 'step-security/harden-runner@v2.16.0' '.github/workflows/dogfood-gate.yml': - 'actions/checkout@v7.0.1' '.github/workflows/e2e.yml': - 'actions/checkout@v7.0.1' '.github/workflows/estate-rules.yml': - 'actions/checkout@v7.0.1' - '.github/workflows/governance.yml': [] + '.github/workflows/governance.yml': + - 'hyperpolymath/standards@8f2ee50841e216cd8c192eeb68953118190f105c' '.github/workflows/guix-policy.yml': - 'actions/checkout@v7.0.1' - '.github/workflows/hypatia-scan.yml': [] + '.github/workflows/hypatia-scan.yml': + - 'hyperpolymath/standards@da2c748aad55c1a1dcba00b60fe4a35017bc6540' '.github/workflows/instant-sync.yml': [] '.github/workflows/label-triage.yml': [] '.github/workflows/labels.yml': [] - '.github/workflows/mirror.yml': [] + '.github/workflows/mirror.yml': + - 'hyperpolymath/standards@8f2ee50841e216cd8c192eeb68953118190f105c' '.github/workflows/openssf-compliance.yml': - 'actions/checkout@v7.0.1' '.github/workflows/pages.yml': @@ -45,9 +48,12 @@ workflows: - 'actions/checkout@v7.0.1' '.github/workflows/runtime-policy.yml': - 'actions/checkout@v7.0.1' - '.github/workflows/rust-ci.yml': [] - '.github/workflows/scorecard.yml': [] - '.github/workflows/secret-scanner.yml': [] + '.github/workflows/rust-ci.yml': + - 'hyperpolymath/standards@8f2ee50841e216cd8c192eeb68953118190f105c' + '.github/workflows/scorecard.yml': + - 'hyperpolymath/standards@da2c748aad55c1a1dcba00b60fe4a35017bc6540' + '.github/workflows/secret-scanner.yml': + - 'hyperpolymath/standards@8f2ee50841e216cd8c192eeb68953118190f105c' '.github/workflows/security-policy.yml': - 'actions/checkout@v7.0.1' '.github/workflows/static-analysis-gate.yml': @@ -134,58 +140,8 @@ dependencies: commit: 'sha1-efb35369e0ad2afab669f228072c1b0d510eae64' owner_id: 2242 repo_id: 204253808 - 'step-security/harden-runner@v2.9.1': - ref: 'v2.9.1' - commit: 'sha1-5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde' + 'step-security/harden-runner@v2.16.0': + ref: 'v2.16.0' + commit: 'sha1-fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594' owner_id: 88700172 repo_id: 422287306 - 'Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6': - ref: 'v2' - commit: 'sha1-6323deb102c322ba6fcbdcafc7e3dddab59af2b6' - owner_id: 580492 - repo_id: 298565987 - '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 - 'dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772': - ref: 'stable' - commit: 'sha1-6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772' - owner_id: 1940490 - repo_id: 260749683 - 'editorconfig-checker/action-editorconfig-checker@51f63319f592f97930c73d9c46184d20bd206393': - ref: 'v3.0.0' - commit: 'sha1-51f63319f592f97930c73d9c46184d20bd206393' - owner_id: 26415196 - repo_id: 297874902 - 'erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124': - ref: 'v1.24.1' - commit: 'sha1-54075bcc5e249e4758d363f27d099f55d843f124' - owner_id: 47606891 - repo_id: 331103973 - 'goto-bus-stop/setup-zig@abea47f85e598557f500fa1fd2ab7464fcb39406': - ref: 'v2.2.1' - commit: 'sha1-abea47f85e598557f500fa1fd2ab7464fcb39406' - owner_id: 1006268 - repo_id: 212984112 - 'ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc': - ref: 'v2.4.4' - commit: 'sha1-2d1146689b8cda280b9bc96326124645441f03bc' - owner_id: 67707773 - repo_id: 421101922 - 'webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555': - ref: 'v0.10.0' - 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..6307081 --- /dev/null +++ b/scripts/check-lock-sync.sh @@ -0,0 +1,157 @@ +#!/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. +# +# Why this exists rather than `gh actions-lock --verify-local` alone: +# `gh actions-lock` v0.1.6 cannot see a job-level `uses:` (a reusable-workflow +# call). Measured on this repo, 2026-09-22: +# * mirror.yml's lock entry was stale by a whole SHA and died at startup with +# jobs=0, yet --verify-local reported "All 30 workflows have complete +# lockfile coverage" and fix mode did not repair it; +# * release.yml's job-level slsa-github-generator ref was absent from the +# lock, again unreported; and once added, --verify-local calls it `stale` +# with "no uses: in this workflow references it" — about a ref on line 144. +# GitHub's own startup enforcement DOES check those refs (that is what killed +# mirror.yml), so the tool is wrong in both directions and cannot be the +# authority. This script is. +# +# Exit 0 only when every workflow's `uses:` set equals its lockfile set exactly. +# Any mismatch exits 1. There is no warn-only mode: a desync means GitHub will +# refuse to start the run, so it must fail the job. + +set -euo pipefail + +WF_DIR="${1:-.github/workflows}" +LOCK="$WF_DIR/actions.lock" + +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 + +awk -v lockfile="$LOCK" ' +# 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 +} + +# ---------- pass 1: the lockfile ---------- +FILENAME == lockfile { + if ($0 ~ /^workflows:[[:space:]]*$/) { inwf = 1; next } + if ($0 ~ /^[a-z_]+:/) { inwf = 0; next } + if (!inwf) next + + # " '.github/workflows/x.yml':" or "... : []" + if (match($0, /^ '"'"'([^'"'"']+)'"'"':/, m)) { + cur = m[1] + seen_path[cur] = 1 + if ($0 ~ /\[\][[:space:]]*$/) cur_has_inline_empty = 1 + next + } + if (match($0, /^ - '"'"'([^'"'"']+)'"'"'[[:space:]]*$/, m) && cur != "") { + lock[cur, m[1]] = 1 + lockcount[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, n] = 1; 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 + } + + # --- direction 1: every uses: must be locked under THIS path --- + nu = split(useslist[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 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 refs:%s\n", key, missing + else + printf "FAIL %s\n refs missing from the lockfile:%s\n", key, missing + bad = 1 + } + + # --- direction 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] + } + 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 } + } + + if (bad) { + print "" + print "actions.lock is OUT OF SYNC with the workflow YAML." + 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 "Fix: run `gh actions-lock --no-migrate-local-actions`, then review the diff" + print "(it does not handle job-level reusable-workflow refs, and it can de-pin" + print "bare SHAs to floating tags - both must be corrected by hand)." + exit 1 + } + print "actions.lock is in sync: every uses: is locked under its own workflow path," + print "and every lockfile entry is referenced. Job-level reusable-workflow refs included." +} +' "$LOCK" "${WORKFLOWS[@]}" From cfadcf9c7e0c266089097e85fb4263a4649a0cc5 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 22 Sep 2026 12:35:08 +0100 Subject: [PATCH 2/9] fix(ci): add dependency records for the job-level reusable-workflow refs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Listing a ref under `workflows:` without a matching record under `dependencies:` produces an UNRESOLVABLE lockfile, and GitHub rejects the run at startup exactly as a desync does. Measured on this pull request: `hypatia-scan`, `rust-ci` and `secret-scanner` were green on `main` on 2026-09-21 and went `startup_failure` with jobs=0 on the previous commit here, which listed their job-level refs but supplied no records. The `dependencies:` section is the lockfile's resolution table, not commentary. `gh actions-lock` v0.1.6 writes neither half for a job-level `uses:` (upstream #129) and will not backfill a record for a key it did not write, so both halves are supplied by hand. The nested `uses:` list is the sorted union of the called reusable file's own pinned refs at that ref, normalised to OWNER/REPO@REF — the lockfile schema rejects a subpath such as `github/codeql-action/upload-sarif@…`. Verified: `scripts/check-lock-sync.sh` clean, `gh actions-lock --verify-local` clean (it was rc=2 on the un-normalised form), regeneration idempotent, and only `actions.lock` modified. Refs: hyperpolymath/standards#968 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01X3hgXxWm6umMgZkjYyHnnm --- .github/workflows/actions.lock | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index 446868d..e01b10a 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -135,6 +135,32 @@ dependencies: commit: 'sha1-22e7bdb322c430c1d0dac6b3bb307f4bb139d0be' owner_id: 6759885 repo_id: 1352485172 + 'hyperpolymath/standards@8f2ee50841e216cd8c192eeb68953118190f105c': + ref: '8f2ee50841e216cd8c192eeb68953118190f105c' + commit: 'sha1-8f2ee50841e216cd8c192eeb68953118190f105c' + owner_id: 6759885 + repo_id: 1116521501 + uses: + - 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' + - 'dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772' + - 'editorconfig-checker/action-editorconfig-checker@51f63319f592f97930c73d9c46184d20bd206393' + - 'erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124' + - 'goto-bus-stop/setup-zig@abea47f85e598557f500fa1fd2ab7464fcb39406' + - 'Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6' + - 'webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555' + 'hyperpolymath/standards@da2c748aad55c1a1dcba00b60fe4a35017bc6540': + ref: 'da2c748aad55c1a1dcba00b60fe4a35017bc6540' + commit: 'sha1-da2c748aad55c1a1dcba00b60fe4a35017bc6540' + owner_id: 6759885 + repo_id: 1116521501 + uses: + - 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' + - 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' + - 'erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124' + - 'github/codeql-action@cdf488f595d80d6e07e03d4674febd5ab45fa938' + - 'ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc' 'softprops/action-gh-release@v3.0.3': ref: 'v3.0.3' commit: 'sha1-efb35369e0ad2afab669f228072c1b0d510eae64' From 3981926bddd860d7f91405beb79a01817358e416 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 22 Sep 2026 12:35:39 +0100 Subject: [PATCH 3/9] chore(ci): accept the gh actions-lock managed-by header on the gate `gh actions-lock` stamps this line on every workflow it scans. Committing it keeps the working tree clean on subsequent runs. 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..4542ae3 100644 --- a/.github/workflows/lock-sync-gate.yml +++ b/.github/workflows/lock-sync-gate.yml @@ -1,3 +1,4 @@ +# This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 name: Lock Sync Gate From 5286aa589d72388ab43022425d9b265cf501b30f Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 22 Sep 2026 12:42:17 +0100 Subject: [PATCH 4/9] fix(actions.lock): close the dependency graph transitively Adds top-level `dependencies:` records for the 11 refs that appeared only inside another record's nested `uses:` list, leaving the lockfile with zero dangling edges. MEASURED on this PR: GitHub rejects a run at startup (jobs=0, "This run likely failed because of a workflow file issue") when the lockfile names a ref it cannot resolve to a record. Governance, Hypatia Security Scan, Rust CI and Secret Scanner all ran with jobs on `pull_request` on 2026-09-21 and went startup_failure on fe22bbc (workflows: entry, no record) and again on cfadcf9 (record present, but its nested uses: were themselves unrecorded). Both commits left a dangling edge; only its class differed. metadatastician/burble's working lockfile is the control: 11 nested refs, 49 records, 0 unrecorded. This brings cicd-squabbler to the same shape. Leaf records carry no nested `uses:`, so closure is reached in a single pass. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01X3hgXxWm6umMgZkjYyHnnm --- .github/workflows/actions.lock | 55 ++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index e01b10a..4fceb71 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -66,6 +66,11 @@ workflows: '.github/workflows/workflow-linter.yml': - 'actions/checkout@v7.0.1' dependencies: + 'Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6': + ref: '6323deb102c322ba6fcbdcafc7e3dddab59af2b6' + commit: 'sha1-6323deb102c322ba6fcbdcafc7e3dddab59af2b6' + owner_id: 580492 + repo_id: 298565987 'actions/attest-build-provenance@v4.2.2': ref: 'v4.2.2' commit: 'sha1-4d101475d8b20a2381f78447822ac1eab6504dd8' @@ -78,6 +83,16 @@ dependencies: commit: 'sha1-508db95dd578ae2727ebd6217d5ba78e4fbda05d' owner_id: 44036562 repo_id: 760701061 + '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' @@ -93,6 +108,11 @@ dependencies: commit: 'sha1-3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c' owner_id: 44036562 repo_id: 192626254 + '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' @@ -115,21 +135,46 @@ dependencies: commit: 'sha1-25dd0e34f4fe68f24cc83900b1fe3fe149efef98' owner_id: 27347476 repo_id: 371068214 + 'dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772': + ref: '6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772' + commit: 'sha1-6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772' + owner_id: 1940490 + repo_id: 260749683 + 'editorconfig-checker/action-editorconfig-checker@51f63319f592f97930c73d9c46184d20bd206393': + ref: '51f63319f592f97930c73d9c46184d20bd206393' + commit: 'sha1-51f63319f592f97930c73d9c46184d20bd206393' + owner_id: 26415196 + repo_id: 297874902 'editorconfig-checker/action-editorconfig-checker@v3.0.0': ref: 'v3.0.0' commit: 'sha1-51f63319f592f97930c73d9c46184d20bd206393' owner_id: 26415196 repo_id: 297874902 + 'erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124': + ref: '54075bcc5e249e4758d363f27d099f55d843f124' + commit: 'sha1-54075bcc5e249e4758d363f27d099f55d843f124' + owner_id: 47606891 + repo_id: 331103973 'erlef/setup-beam@v1.24.1': ref: 'v1.24.1' commit: 'sha1-54075bcc5e249e4758d363f27d099f55d843f124' owner_id: 47606891 repo_id: 331103973 + 'github/codeql-action@cdf488f595d80d6e07e03d4674febd5ab45fa938': + ref: 'cdf488f595d80d6e07e03d4674febd5ab45fa938' + commit: 'sha1-cdf488f595d80d6e07e03d4674febd5ab45fa938' + 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 + 'goto-bus-stop/setup-zig@abea47f85e598557f500fa1fd2ab7464fcb39406': + ref: 'abea47f85e598557f500fa1fd2ab7464fcb39406' + commit: 'sha1-abea47f85e598557f500fa1fd2ab7464fcb39406' + owner_id: 1006268 + repo_id: 212984112 'hyperpolymath/smtp-notify-action@v0.3.0': ref: 'v0.3.0' commit: 'sha1-22e7bdb322c430c1d0dac6b3bb307f4bb139d0be' @@ -161,6 +206,11 @@ dependencies: - 'erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124' - 'github/codeql-action@cdf488f595d80d6e07e03d4674febd5ab45fa938' - 'ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc' + 'ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc': + ref: '2d1146689b8cda280b9bc96326124645441f03bc' + commit: 'sha1-2d1146689b8cda280b9bc96326124645441f03bc' + owner_id: 67707773 + repo_id: 421101922 'softprops/action-gh-release@v3.0.3': ref: 'v3.0.3' commit: 'sha1-efb35369e0ad2afab669f228072c1b0d510eae64' @@ -171,3 +221,8 @@ dependencies: commit: 'sha1-fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594' owner_id: 88700172 repo_id: 422287306 + 'webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555': + ref: 'e83874834305fe9a4a2997156cb26c5de65a8555' + commit: 'sha1-e83874834305fe9a4a2997156cb26c5de65a8555' + owner_id: 135788 + repo_id: 208510314 From 8aa756e554a65dd4d948289545c4a77cc8044e59 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 22 Sep 2026 12:56:47 +0100 Subject: [PATCH 5/9] fix(ci): make the lock-sync gate detect dangling edges, the failure it was blind to The gate shipped in this PR was GREEN on two commits of this very branch that GitHub refused to start. Measured here, 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 check-lock-sync.sh exited 0, `gh actions-lock --verify-local` exited 0, and the Lock Sync Gate reported green while four workflows were dead. Every local gate was green on a fatal commit. That is the guard/consumer trap: the gate asked "is every uses: locked?" while GitHub asks "is every locked ref RESOLVABLE?". The asymmetry is counter-intuitive and is what makes the new clause mandatory: * 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 the new clause 3 makes that demand safe. Shipping clause 1 alone actively steers a developer into the fatal state: Dependabot bumps a job-level ref -> clause 1 reds -> `gh actions-lock` will not backfill a job-level key it did not write -> the developer hand-adds the workflows: entry to go green -> no dependencies: record -> CI dies silently with the gate still green. Clause 3: every ref named anywhere in the lockfile must resolve to a top-level dependencies: record. Closure terminates in one pass because a leaf record may legally omit the nested uses: key, so adding leaves introduces no new edges. Also require an awk with 3-argument match() (a GNU extension) and probe for it rather than trusting the name: mawk is the Debian/Ubuntu default `awk`, and a silent parse failure here would read as a clean pass - the exact failure mode this script exists to prevent. Regression-tested against the real trees, not synthetic mutants: red on fe22bbc (2 edges) and cfadcf9 (11 edges), green on 5286aa5. Mutant: deleting one leaf record is detected, with a before/after assertion that the deletion applied. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01X3hgXxWm6umMgZkjYyHnnm --- scripts/check-lock-sync.sh | 158 +++++++++++++++++++++++++++++-------- 1 file changed, 126 insertions(+), 32 deletions(-) diff --git a/scripts/check-lock-sync.sh b/scripts/check-lock-sync.sh index 6307081..7c4beb3 100755 --- a/scripts/check-lock-sync.sh +++ b/scripts/check-lock-sync.sh @@ -2,30 +2,68 @@ # 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. +# workflow YAML, in BOTH directions (including job-level reusable-workflow refs), +# AND that the lockfile is TRANSITIVELY CLOSED. # -# Why this exists rather than `gh actions-lock --verify-local` alone: -# `gh actions-lock` v0.1.6 cannot see a job-level `uses:` (a reusable-workflow -# call). Measured on this repo, 2026-09-22: -# * mirror.yml's lock entry was stale by a whole SHA and died at startup with -# jobs=0, yet --verify-local reported "All 30 workflows have complete -# lockfile coverage" and fix mode did not repair it; -# * release.yml's job-level slsa-github-generator ref was absent from the -# lock, again unreported; and once added, --verify-local calls it `stale` -# with "no uses: in this workflow references it" — about a ref on line 144. -# GitHub's own startup enforcement DOES check those refs (that is what killed -# mirror.yml), so the tool is wrong in both directions and cannot be the -# authority. This script is. +# Three clauses, each of which alone is insufficient: # -# Exit 0 only when every workflow's `uses:` set equals its lockfile set exactly. -# Any mismatch exits 1. There is no warn-only mode: a desync means GitHub will -# refuse to start the run, so it must fail the job. +# 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 @@ -38,7 +76,7 @@ if [ "${#WORKFLOWS[@]}" -eq 0 ]; then exit 1 fi -awk -v lockfile="$LOCK" ' +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 @@ -53,20 +91,41 @@ function norm(r, at, path, ref, n, parts) { # ---------- pass 1: the lockfile ---------- FILENAME == lockfile { - if ($0 ~ /^workflows:[[:space:]]*$/) { inwf = 1; next } - if ($0 ~ /^[a-z_]+:/) { inwf = 0; next } + 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[depkey] = 1 + next + } + # " - 'owner/repo@ref'" -- a nested uses: of that record + if (match($0, /^ - '([^']+)'/, m) && depkey != "") { + 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)) { + if (match($0, /^ '([^']+)':/, m)) { cur = m[1] seen_path[cur] = 1 - if ($0 ~ /\[\][[:space:]]*$/) cur_has_inline_empty = 1 next } - if (match($0, /^ - '"'"'([^'"'"']+)'"'"'[[:space:]]*$/, m) && cur != "") { + if (match($0, /^ - '([^']+)'[[:space:]]*$/, m) && cur != "") { lock[cur, m[1]] = 1 lockcount[cur]++ + want[m[1]] = 1 # clause 3: this must resolve too + wantsrc[m[1]] = wantsrc[m[1]] " " cur next } next @@ -79,7 +138,7 @@ FNR == 1 { wf = FILENAME } sub(/[[:space:]]+#.*$/, "", line) # strip trailing comment if (match(line, /^[[:space:]]*-?[[:space:]]*uses:[[:space:]]*(.+)$/, m)) { raw = m[1] - gsub(/^["'"'"']|["'"'"']$/, "", raw) + gsub(/^["']|["']$/, "", raw) gsub(/[[:space:]]+$/, "", raw) if (raw ~ /^\$\//) { dollar[wf] = dollar[wf] " " raw; next } # known corruption n = norm(raw) @@ -101,7 +160,7 @@ END { bad = 1 } - # --- direction 1: every uses: must be locked under THIS path --- + # --- clause 1: every uses: must be locked under THIS path --- nu = split(useslist[wf], u, " ") delete uniq; missing = "" for (j = 1; j <= nu; j++) { @@ -117,7 +176,7 @@ END { bad = 1 } - # --- direction 2: every lock entry must be referenced by this workflow --- + # --- clause 2: every lock entry must be referenced by this workflow --- orphan = "" for (k in lock) { split(k, kp, SUBSEP) @@ -141,17 +200,52 @@ END { 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, 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." + 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 "Fix: run `gh actions-lock --no-migrate-local-actions`, then review the diff" - print "(it does not handle job-level reusable-workflow refs, and it can de-pin" - print "bare SHAs to floating tags - both must be corrected by hand)." + 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 } - print "actions.lock is in sync: every uses: is locked under its own workflow path," - print "and every lockfile entry is referenced. Job-level reusable-workflow refs included." + 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 } -' "$LOCK" "${WORKFLOWS[@]}" +AWK + +"$AWK" -v lockfile="$LOCK" "$PROG" "$LOCK" "${WORKFLOWS[@]}" From 72d62502395103cf5c5082d79643dfd00cef16d3 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 12:00:57 +0000 Subject: [PATCH 6/9] docs(ci): clarify workflow reference normalization in the lock-sync check --- scripts/check-lock-sync.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/check-lock-sync.sh b/scripts/check-lock-sync.sh index 7c4beb3..7373078 100755 --- a/scripts/check-lock-sync.sh +++ b/scripts/check-lock-sync.sh @@ -77,7 +77,9 @@ if [ "${#WORKFLOWS[@]}" -eq 0 ]; then fi read -r -d '' PROG <<'AWK' || true -# owner/repo[/subpath...]@ref -> owner/repo@ref ("" if not an external ref) +# Normalise an external workflow `uses:` reference to owner/repository@ref, +# discarding any action subpath. Return an empty string for local actions or +# values without a repository path and 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 7dcf0bf7645f0d740136af6b27166fe6f97c62e8 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 22 Sep 2026 13:03:52 +0100 Subject: [PATCH 7/9] 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 4542ae3..b7da1b6 100644 --- a/.github/workflows/lock-sync-gate.yml +++ b/.github/workflows/lock-sync-gate.yml @@ -1,5 +1,5 @@ -# This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 +# This workflow is managed by gh actions-lock. name: Lock Sync Gate # Fails any pull request whose .github/workflows/actions.lock has drifted from From edf54e059046a4717bb52ad500d4282183d03831 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 22 Sep 2026 13:04:09 +0100 Subject: [PATCH 8/9] 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 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/check-lock-sync.sh b/scripts/check-lock-sync.sh index 7373078..c3a39a0 100755 --- a/scripts/check-lock-sync.sh +++ b/scripts/check-lock-sync.sh @@ -70,8 +70,8 @@ if [ ! -f "$LOCK" ]; then fi shopt -s nullglob -mapfile -t WORKFLOWS < <(printf '%s\n' "$WF_DIR"/*.yml "$WF_DIR"/*.yaml | sort -u) -if [ "${#WORKFLOWS[@]}" -eq 0 ]; then +WORKFLOWS=("$WF_DIR"/*.yml "$WF_DIR"/*.yaml) +if [[ "${#WORKFLOWS[@]}" -eq 0 ]]; then echo "check-lock-sync: FATAL: no workflow files under $WF_DIR" >&2 exit 1 fi From d4e4df8651f5d822690a206782caed11fde03f5a Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 13:15:10 +0100 Subject: [PATCH 9/9] Clarify external uses reference normalization comment (#102) Update the comment in `scripts/check-lock-sync.sh` to describe external `uses:` references generally, removing the workflow-specific wording. No runtime behavior changes. The committed change only clarifies an existing comment; it does not generate docstrings as the task title suggests. Validation was not run. [View coding task](https://app.coderabbit.ai/code/tasks/bd0471a7-353d-5fd3-8661-1ff6524236fa?source=coding_agent_github_pr_description) Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- scripts/check-lock-sync.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/check-lock-sync.sh b/scripts/check-lock-sync.sh index c3a39a0..977f893 100755 --- a/scripts/check-lock-sync.sh +++ b/scripts/check-lock-sync.sh @@ -77,7 +77,7 @@ if [[ "${#WORKFLOWS[@]}" -eq 0 ]]; then fi read -r -d '' PROG <<'AWK' || true -# Normalise an external workflow `uses:` reference to owner/repository@ref, +# Normalise an external `uses:` reference to owner/repository@ref, # discarding any action subpath. Return an empty string for local actions or # values without a repository path and ref. function norm(r, at, path, ref, n, parts) {