You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Nothing runs actionlint — a structural workflow defect lands as a job-less red on a consuming repo #3228
CI validates workflows in several ways — check-workflow-timeouts.py caps every job, the shared-rule
check, the pin guards — but nothing runs actionlint, and no check catches a structural defect
in a workflow file.
What it cost, measured today
#3225 inserted an input between the previous input's required: line and its default: line. The
orphaned default attached to the new input, producing a duplicate default key, and the input
above silently lost its default. GitHub refuses the whole workflow.
The failure surfaced in a different repository: MeshWeaver.Plugins#1268 run 33777768374
concluded failure with zero jobs and zero contexts. A startup_failure renders as an ordinary
red and names nothing — no job to open, no log to read, no annotation on the commit. Attributing it
took comparing job counts across two heads in two repos (the prior head ran 29 jobs; this one ran 0)
and then linting the lane by hand.
actionlint names it in one line, at the source:
.github/workflows/node-repo-module-pack.yml:289:9: key "default" is duplicated in input of
workflow_call event. previously defined at line:288,col:9 [syntax-check]
🚨 And the verification that "passed" could not have failed.#3225 validated the lane with yaml.safe_load; PyYAML accepts duplicate mapping keys and silently keeps the last, so the parse
succeeded, the input's spec read back correctly, and an "unchanged required inputs" assertion passed.
Any workflow validation built on a permissive YAML load has this blind spot by construction.
Why it is worse for THIS repo than for most
These are workflow_call lanes five satellite repos pin. A structural break in one is invisible in
core — core does not call them — and lands as a job-less red on whichever satellite pins the new sha
first. The blast radius is every consumer, and the diagnosis happens furthest from the cause.
What to add
Run actionlint over .github/workflows/ in the existing workflow-validation job, failing on any
finding. Notes:
It is a single static binary; no runtime deps.
Start with -shellcheck= -pyflakes= (disable the embedded shell/python linters) so the change is
about workflow structure only and does not arrive with a large backlog of run: findings. Those
can be enabled later, deliberately.
Landed on main via #3269 (merged 2026-09-04T06:08:30Z). Closing manually — the merge-queue merge did not fire the Closes link, though closingIssuesReferences resolved to this issue correctly.
Verified on origin/main, .github/workflows/dotnet-test.yml:
actionlint runs over .github/workflows/, pinned to an explicit version (not latest);
-shellcheck= -pyflakes= as this issue asked, so it arrives about workflow STRUCTURE only, with the follow-up recorded rather than assumed;
it cannot pass on no evidence — the install step is || { echo "::error::could not download actionlint ${ver} from ${url} — this gate has no evidence without it, so it fails rather than skipping"; exit 1; }, with no continue-on-error: and no if: asking whether something is set. That was the part worth getting right: a skipped job and a passed job are the same colour on GitHub, which is what let Declare an image assembly SUPERSEDED so a type can move out of it into a module in one wave #3225's duplicate default key reach a satellite as a job-less red.
The gate's own comment names the gap it closes, so the next reader does not have to reconstruct it:
🚨 THE GAP actionlint CLOSES (MeshWeaver#3228): every check above reads a workflow with a permissive YAML load … actionlint names it in one line, at the source, on the PR that writes it
For the record, the PyYAML blind spot this issue identified bit again the same day, in a different repo: I validated an edit to Systemorph/Memex's helm-release.yml with yaml.safe_load_all, which passed, then ran actionlint separately because of this issue. Worth considering whether the satellite repos' node-repo-validate lane should carry the same step — Memex is not a node repo and runs none of it.
Activity
rbuergi commentedon Sep 4, 2026
Landed on
mainvia #3269 (merged 2026-09-04T06:08:30Z). Closing manually — the merge-queue merge did not fire theCloseslink, thoughclosingIssuesReferencesresolved to this issue correctly.Verified on
origin/main,.github/workflows/dotnet-test.yml:.github/workflows/, pinned to an explicit version (notlatest);-shellcheck= -pyflakes=as this issue asked, so it arrives about workflow STRUCTURE only, with the follow-up recorded rather than assumed;|| { echo "::error::could not download actionlint ${ver} from ${url} — this gate has no evidence without it, so it fails rather than skipping"; exit 1; }, with nocontinue-on-error:and noif:asking whether something is set. That was the part worth getting right: a skipped job and a passed job are the same colour on GitHub, which is what let Declare an image assembly SUPERSEDED so a type can move out of it into a module in one wave #3225's duplicatedefaultkey reach a satellite as a job-less red.The gate's own comment names the gap it closes, so the next reader does not have to reconstruct it:
For the record, the PyYAML blind spot this issue identified bit again the same day, in a different repo: I validated an edit to
Systemorph/Memex'shelm-release.ymlwithyaml.safe_load_all, which passed, then ran actionlint separately because of this issue. Worth considering whether the satellite repos'node-repo-validatelane should carry the same step — Memex is not a node repo and runs none of it.