fix(lint-workflows): ignore actionlint's stale report of the code-quality scope - #52
Merged
Merged
Conversation
…lity scope Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes
unknown permission scope "code-quality"failing lint on workflows that are perfectly correct, caused by actionlint 1.7.12 carrying a permission-scope table that predates the scope GitHub has since added, by passing an-ignorefor exactly that message fromactions/lint-workflowsitself.Consumers previously carried their own
.github/actionlint.yamlto suppress it, one file at a time. They now get the correction by bumping their pin, and need nothing of their own.The suppression is scoped to the one message and retires when the pin moves.
Problem
A repository that uploads code coverage cannot lint its own workflows.
actions/upload-code-coveragerequires thecode-qualitypermission scope, and every workflow granting it failsactions/lint-workflowswith an error about a scope that is real and documented.actionlint carries its own table of valid scopes rather than reading one from GitHub, so the table is only ever as current as the release. This action pins 1.7.12 deliberately — an unpinned linter turns its next new rule into a red build on whichever pull request happens to be open — and that pin is what freezes the table.
Waiting for upstream is not a plan. rhysd/actionlint has had no commit since 2026-04-19 and no release since v1.7.12 on 2026-03-30. The request is #672, #658 and #674 add the scope and are mergeable but unmerged, and #719 asks whether the maintainer is still active.
Every consumer therefore worked around the same stale table separately, in its own
.github/actionlint.yaml, scoped to whichever file happened to grant the scope. The false positive is a fact about the pin, not about any one repository, so each of those files is the same correction written again.Evidence
actionlint 1.7.12, run over a workflow granting
permissions: code-quality: writeand one granting a misspeltcode-qualty:The correct workflow and the misspelt one are reported identically. With the flag this PR adds, and nothing else changed, the same two files give:
That second run is the whole claim: the false positive is gone and the real one survives.
Solution
actions/lint-workflowsnow hands actionlint one-ignoreof its own, throughraven-actions/actionlint's existingflagsinput. No new input on this action — the ignore is not a knob consumers should turn, it is a correction to a table they cannot see.-ignoretakes an RE2 regex matched against the message. This one spells the scope name out, so it silences that message and nothing wider — a misspelt scope still fails, which is what keeps the rule useful.Why
\srather than the obvious quoted formThe wrapper splits its
flagsinput itself, and does not run a shell. It tokenizes withINPUT_FLAGS.match(/(?:[^\s"]+|"[^"]*")+/g)and hands the tokens to@actions/exec, which spawns with an argument array — and on Linux_getSpawnArgsreturns those arguments verbatim, quotes included.So the two forms a reader would reach for first both fail silently:
-ignore 'unknown permission scope "code-quality"'-ignore,'unknown,permission,scope,"code-quality"'-ignore "unknown permission scope ...""— a regex that matches nothingWriting the spaces as
\ssidesteps the tokenizer instead of fighting it: the whole flag is one token with no whitespace in it. The"around the scope name stays, because actionlint's own message quotes the scope.The reminder rides in the bump pull request
.github/renovate.jsongains a rule matchingrhysd/actionlintthat puts the retirement check in the body of the bump, withautomerge: falseso that pull request waits to be read. The bump is the one moment anybody will think about removing the flag, and the pin moves rarely enough that reading it costs little.The rule sits after the existing automerge rule so it wins.
Documentation
actions/lint-workflows/README.mdused thecode-qualitycase as its worked example of silencing a rule, which is now the case the action handles itself. That example is replaced with a generic per-file one, and a new section says what the built-in ignore covers, why it lives here rather than in each consumer, and that it retires with the pin. The rootREADME.mdparagraph making the same claim is updated to match. The inputs table is unchanged.Review Guide
actions/lint-workflows/action.yaml:50— the flag itself. The\sand the embedded"are both load-bearing, and getting either wrong fails open rather than loudly: a regex matching nothing looks exactly like a linter that is still stale.Design Decisions
permissionsrulecode-qualityso the rest of the rule keeps running..github/actionlint.yamlfor the same reason. Fixing it once also means it is removed once, at the bump.Blast Radius
Not behind a feature flag, and not immediate either — consumers pin exact tags, so nothing changes anywhere until Renovate bumps a consumer to the release this cuts. A
fixtitle cuts that patch release.What to watch after a consumer picks it up: that its workflow linting still fails on real permission mistakes. The regex is narrow by construction, but a linter that has quietly stopped reporting is the failure mode worth checking for.
Rollback Plan
Revert this PR via GitHub's Revert button. Consumers already on the released tag are unaffected until they bump again; one that needs the old behaviour immediately can pin back to the previous tag.
Test plan
Verifiable by agent before merging
Prettier passes under the pinned Node from
.tool-versions, andnpm cileftpackage-lock.jsonunchanged.actionlintpasses over this repository's own workflows, so the YAML this PR edits is well-formed.A workflow granting
permissions: code-quality: writenow passes. Theflagsvalue was read straight out ofaction.yaml, put through the wrapper's own tokenizer, then spawned as an argument array — the same path a runner takes.A workflow granting a misspelt
permissions: code-qualty: writestill fails, from the same run.The quoted forms this PR rejects really are broken, rather than merely suspected. Running the wrapper's own tokenizer over each:
The
Lint workflowsjob on this pull request is green. It runs./actions/lint-workflowsover this repository, so a mis-tokenized flag would have failed here rather than in whichever repository next bumps its pin.Cannot be verified before merge
rhysd/actionlintbump opens with the retirement note in its body and does not automerge —prBodyNotesandautomergeare only observable when Renovate next runs againstmain, and no release has moved the pin yet.code-qualitylints clean on the released tag. This repository has no workflow granting the scope and should not gain one, and the action exposes neither the wrapper'sfilesnor itsworking-directoryinput, so there is nowhere in CI here to point it at a fixture — the local run above is the evidence for the behaviour itself.Follow-up, not in this PR
Once the release exists, Renovate bumps
kanso-labs/github-actionsin kanso-ui, which can then delete its own.github/actionlint.yaml. That repository is untouched here.