Skip to content

fix(lint-workflows): ignore actionlint's stale report of the code-quality scope - #52

Merged
rodrigoscna merged 1 commit into
mainfrom
claude/code-quality-lint-ignore-f3db8b
Sep 2, 2026
Merged

fix(lint-workflows): ignore actionlint's stale report of the code-quality scope#52
rodrigoscna merged 1 commit into
mainfrom
claude/code-quality-lint-ignore-f3db8b

Conversation

@rodrigoscna

@rodrigoscna rodrigoscna commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

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 -ignore for exactly that message from actions/lint-workflows itself.

Consumers previously carried their own .github/actionlint.yaml to 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-coverage requires the code-quality permission scope, and every workflow granting it fails actions/lint-workflows with 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: write and one granting a misspelt code-qualty:

.github/workflows/bad.yaml:7:3: unknown permission scope "code-qualty". all available permission scopes are "actions", "artifact-metadata", "attestations", "checks", "contents", "deployments", "discussions", "id-token", "issues", "models", "packages", "pages", "pull-requests", "repository-projects", "security-events", "statuses" [permissions]
.github/workflows/good.yaml:7:3: unknown permission scope "code-quality". all available permission scopes are "actions", "artifact-metadata", "attestations", "checks", "contents", "deployments", "discussions", "id-token", "issues", "models", "packages", "pages", "pull-requests", "repository-projects", "security-events", "statuses" [permissions]
exit=1

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:

=== good.yaml === exit=0

=== bad.yaml === exit=1
.github/workflows/bad.yaml:7:3: unknown permission scope "code-qualty". all available permission scopes are "actions", ... [permissions]

That second run is the whole claim: the false positive is gone and the real one survives.

Solution

actions/lint-workflows now hands actionlint one -ignore of its own, through raven-actions/actionlint's existing flags input. 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.

flags: '-ignore unknown\spermission\sscope\s"code-quality"'

-ignore takes 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 \s rather than the obvious quoted form

The wrapper splits its flags input itself, and does not run a shell. It tokenizes with INPUT_FLAGS.match(/(?:[^\s"]+|"[^"]*")+/g) and hands the tokens to @actions/exec, which spawns with an argument array — and on Linux _getSpawnArgs returns those arguments verbatim, quotes included.

So the two forms a reader would reach for first both fail silently:

Written as What actionlint receives
-ignore 'unknown permission scope "code-quality"' five arguments — -ignore, 'unknown, permission, scope, "code-quality"'
-ignore "unknown permission scope ..." one argument, still carrying its " — a regex that matches nothing

Writing the spaces as \s sidesteps 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.json gains a rule matching rhysd/actionlint that puts the retirement check in the body of the bump, with automerge: false so 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.md used the code-quality case 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 root README.md paragraph making the same claim is updated to match. The inputs table is unchanged.

Review Guide

actions/lint-workflows/action.yaml     ← passes flags: to the wrapper
   └── raven-actions/actionlint@v2.2.0  (external)  ← tokenizes flags, spawns the binary
          └── actionlint 1.7.12         (external)  ← applies -ignore as an RE2 regex
  • actions/lint-workflows/action.yaml:50 — the flag itself. The \s and 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

Decision Context
Match the message exactly rather than ignoring the permissions rule actionlint can disable a rule wholesale, which would have been shorter. It would also have stopped catching genuinely misspelt scopes, which is most of what that rule is for. The regex names code-quality so the rest of the rule keeps running.
Put the ignore in the action rather than leaving it to consumers The stale table is a property of the pin this action owns, so every consumer was writing the same .github/actionlint.yaml for 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 fix title 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, and npm ci left package-lock.json unchanged.

    $ mise exec node@24.20.0 -- node --version
    v24.20.0
    $ mise exec node@24.20.0 -- npm ci && mise exec node@24.20.0 -- npm run lint
    Checking formatting...
    All matched files use Prettier code style!
    $ git status --porcelain -- package-lock.json
    (no output)
    
  • actionlint passes over this repository's own workflows, so the YAML this PR edits is well-formed.

    $ actionlint -no-color; echo "exit=$?"
    exit=0
    
  • A workflow granting permissions: code-quality: write now passes. The flags value was read straight out of action.yaml, put through the wrapper's own tokenizer, then spawned as an argument array — the same path a runner takes.

    action.yaml flags: -ignore unknown\spermission\sscope\s"code-quality"
    argv to actionlint: ["-no-color","-ignore","unknown\\spermission\\sscope\\s\"code-quality\""]
    
    === good.yaml === exit=0
    
  • A workflow granting a misspelt permissions: code-qualty: write still fails, from the same run.

    === bad.yaml === exit=1
    .github/workflows/bad.yaml:7:3: unknown permission scope "code-qualty". ... [permissions]
    
  • The quoted forms this PR rejects really are broken, rather than merely suspected. Running the wrapper's own tokenizer over each:

    "-ignore 'unknown permission scope \"code-quality\"'"
      => ["-ignore","'unknown","permission","scope","\"code-quality\"'"]
    "-ignore \"unknown permission scope \\\"code-quality\\\"\""
      => ["-ignore","\"unknown permission scope \\\"code-quality\\\"\""]
    
  • The Lint workflows job on this pull request is green. It runs ./actions/lint-workflows over this repository, so a mis-tokenized flag would have failed here rather than in whichever repository next bumps its pin.

    $ gh pr checks 52
    Analyze (actions)                          pass  42s
    Check formatting                           pass  10s
    CodeQL                                     pass   3s
    Dry run release-please / Propose releases   pass   4s
    Lint workflows                             pass  11s
    

Cannot be verified before merge

  • Renovate's next rhysd/actionlint bump opens with the retirement note in its body and does not automerge — prBodyNotes and automerge are only observable when Renovate next runs against main, and no release has moved the pin yet.
  • A consumer granting code-quality lints 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's files nor its working-directory input, 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-actions in kanso-ui, which can then delete its own .github/actionlint.yaml. That repository is untouched here.

…lity scope

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@rodrigoscna rodrigoscna self-assigned this Sep 2, 2026
@rodrigoscna
rodrigoscna marked this pull request as ready for review September 2, 2026 18:11
@rodrigoscna
rodrigoscna merged commit 0e3a473 into main Sep 2, 2026
5 checks passed
@rodrigoscna
rodrigoscna deleted the claude/code-quality-lint-ignore-f3db8b branch September 2, 2026 18:11
@kanso-labs kanso-labs Bot mentioned this pull request Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant