Skip to content

ci: only lint real steps, and find the real end of an expression - #124

Merged
widgetii merged 1 commit into
masterfrom
linter-walk-and-expr-fixes
Aug 19, 2026
Merged

ci: only lint real steps, and find the real end of an expression#124
widgetii merged 1 commit into
masterfrom
linter-walk-and-expr-fixes

Conversation

@widgetii

Copy link
Copy Markdown
Member

Two bugs in the linter #122 added. Found by Qodo reviewing the port of it to OpenIPC/firmware#2290, and confirmed against this copy rather than taken on trust.

Both are latent here — nothing in this repo currently has a run key outside a step or a }} inside an expression string, and the block count is unchanged at 23. They're fixed because the failure mode in both cases is a red job with nothing wrong with the tree, which is how a check stops being believed.

1. Non-step run: keys were linted

walk() collected any mapping with a scalar run key, anywhere in the document. That's one key name away from linting things that aren't shell — an action input, an env var, or a matrix field called run holds arbitrary text. On this fixture:

jobs:
  j:
    env:
      run: not shell 'at all
    steps:
      - name: real
        run: echo hi
      - uses: some/action@v1
        with:
          run: also not shell 'at all

the current implementation collects ['<unnamed>', '<unnamed>', 'real'] — the env value and the action input both get parsed as bash, and both would fail the job.

A step now has to come out of a steps: sequence. I kept structural discovery rather than hardcoding jobs.*.steps[*] (which the bot suggested), because the steps: parent is the actual schema invariant and it keeps composite actions (runs: steps:) covered by the same rule.

2. Expression end was found naively

The substitution used \$\{\{.*?\}\}, which stops at the first }} even when it's inside a string literal:

x=${{ fromJSON('{"a": {"b": 1}}') }}

produces x=__GHA_EXPR__') }}, and bash -n then reports unexpected EOF while looking for matching \'`. A false failure on a perfectly valid block.

Replaced with a scanner that tracks GitHub's single-quoted string literals including the doubled '' escape, and returns the text untouched if an expression is never closed — a malformed workflow isn't this checker's business.

Verification

  • Self-tests for both; both fail against the current implementation
  • Block count unchanged at 23, so nothing real was dropped
  • Still flags the original ci: write releases from one paced job, not from 107 at once #121 bug at master.yml:354 (Collect assets) with the runner's own message
  • Docstring corrected — it described the behaviour this replaces — and the now-unused re import dropped
  • Keeps this copy identical to firmware's apart from the incident paragraph and the block floor

Selects 0 devices, thanks to the classification in #122.

🤖 Generated with Claude Code

Two bugs in the linter #122 added, found by Qodo reviewing the port of
it to OpenIPC/firmware and confirmed against this copy rather than taken
on trust.

walk() collected any mapping with a scalar `run` key, anywhere in the
document. That is one key name away from linting things that are not
shell: an action input, an env var or a matrix field called `run` holds
arbitrary text, and feeding it to bash -n fails a workflow that is fine.
On a fixture with one real step plus an `env: run:` and a `with: run:`,
this copy collects three blocks. Steps now have to come out of a
`steps:` sequence, which is the actual schema invariant and still not a
hardcoded jobs.*.steps[*] path, so composite actions (`runs: steps:`)
keep working.

The expression substitution used `\$\{\{.*?\}\}`, which stops at the
first `}}` even when it is inside a string literal. On

    x=${{ fromJSON('{"a": {"b": 1}}') }}

this copy produces `x=__GHA_EXPR__') }}` and bash -n then reports an
unbalanced quote -- a false failure on a valid block, in the direction
that trains people to ignore the job. Replaced with a scanner that
tracks GitHub's single-quoted strings, including the doubled '' escape,
and returns the text untouched if an expression is never closed.

Both are latent here: nothing in this repo has a `run` key outside a
step or a `}}` inside an expression string, and the block count is
unchanged at 23. They are fixed because the failure mode is a red job
with nothing wrong with the tree, which is how a check stops being
believed.

Self-tests for both. The docstring described the behaviour this
replaces, so it is corrected too, and the now-unused `re` import drops.
Keeps this copy identical to firmware's apart from the incident
paragraph and the block floor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Fix workflow shell lint step discovery and expression parsing

🐞 Bug fix 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Restricts shell lint discovery to run blocks nested beneath steps sequences.
• Scans expression boundaries safely when quoted strings contain closing braces.
• Adds regression self-tests while preserving source line-number reporting.
Diagram

graph TD
  A["Workflow YAML"] --> B["YAML AST"] --> C["Step Walker"] --> D["Run Blocks"]
  D --> E["Expression Scanner"] --> F["Shell Parser"]
Loading
High-Level Assessment

The current approach is appropriate: tracking whether mappings originate from a steps: sequence follows the relevant schema invariant without coupling discovery to jobs.*.steps[*], preserving composite-action support. A focused scanner is also preferable to the former non-greedy regex because expression terminators require quote awareness; adopting a full GitHub expression parser would add disproportionate complexity for boundary detection.

Files changed (1) +119 / -28

Bug fix (1) +119 / -28
lint-workflow-shell.pyConstrain step discovery and safely scan GitHub expressions +119/-28

Constrain step discovery and safely scan GitHub expressions

• Limits collected 'run' blocks to mappings originating from 'steps:' sequences, preventing action inputs, environment values, and matrix fields from being parsed as shell. Replaces regex expression substitution with a quote-aware scanner that handles embedded '}}', doubled quote escapes, multiline expressions, and unclosed expressions. Adds regression self-tests, updates behavioral documentation, and removes the unused 're' import.

.github/scripts/lint-workflow-shell.py

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider

Great, no issues found!

Qodo reviewed your code and found no material issues that require review

Grey Divider

Tip of the day
💡 Did you know, you can show, collapse, or hide each part of a finding: code, evidence, and all

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

@widgetii
widgetii merged commit fc476c8 into master Aug 19, 2026
6 checks passed
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