Skip to content

ci(tools): parse the scripts inside single-quoted bash -c arguments - #893

Open
gHashTag wants to merge 1 commit into
mainfrom
ci/inner-bash-c-syntax
Open

gHashTag wants to merge 1 commit into
mainfrom
ci/inner-bash-c-syntax

Conversation

@gHashTag

@gHashTag gHashTag commented Oct 5, 2026

Copy link
Copy Markdown
Owner

Closes #892.

What changes

tools/check_run_syntax.py now does a second pass once a workflow run: script parses. Each single-quoted bash -c '...' / sh -c '...' argument in that script is read out and parsed with bash -n / sh -n.

The read-out follows the outer shell's rules:

  • '\'' is a quote;
  • a splice of an outer value, '"$PART"', '$X' or '${Y}', becomes one placeholder word;
  • bash -lc and bash -euo pipefail -c are recognised;
  • a quote that never closes, or a closing quote glued to anything else, is "cannot tell" (exit 2), not clean;
  • double-quoted bash -c "..." is not read, because the outer shell expands it first;
  • zsh -c is not read.

The script is checked only when the outer script parses. A broken outer script is already reported, and its quote pairing cannot be trusted.

The summary line now also counts the inner scripts:

workflows read: 122 of 122; bash -c '...' scripts read out of them: 24; steps and scripts that do not parse: 0

Measured

check result
main's 122 workflows 24 inner scripts, 8 with '"$VAR"' splices, all parse; exit 0
tools/test_check_run_syntax.py 75 of 75, including 12 new cases
negative control: main's gate run against the new cases the 7 new cases that expect a non-zero exit fail
positive control: the probe workflow at 8d0a7d9d4 (the apostrophe that broke run 37253683063) reported by both gates, since it breaks the outer quoting; no new finding there
stray fi inside the string outer bash -n exits 0, inner bash -n exits 2

The 12 new cases:

case expected exit
unclosed if inside bash -c 1
clean bash -c 0
'"$X"' splice 0
an error after a splice 1
bare '$X' / '${Y}' splice 0
sh -c '…' with <( 1
bash -lc 1
bash -euo pipefail -c 1
two scripts, second bad 1
glued 'echo x'y 2
zsh -c 0 (not read)
double-quoted 0 (not read)

Not measured

  • Scripts handed to bash -c through a variable or a heredoc. These are not read.
  • Workflow run time. The gate adds one bash -n subprocess per inner script, 24 on main. Its wall time in CI has not been measured.

🤖 Generated with Claude Code

check_run_syntax ran bash -n on each run: script, and its own docstring
said what that misses: a `docker run ... bash -c '...'` script is one
string to the outer shell, so an error inside it was not seen unless it
broke the outer quoting. (The apostrophe that broke run 37253683063 did
break the outer quoting, and main's gate already reports it; an unclosed
`if` or a stray `fi` inside the string it does not.)

Once the outer script parses, each single-quoted `bash -c '...'` and
`sh -c '...'` argument is now read out the way the outer shell reads it
-- '\'' is a quote, a splice of an outer value ('"$PART"', '$X') is one
word -- and parsed with bash -n / sh -n. A closing quote glued to
anything else, and a quote that never closes, are "cannot tell" (exit
2), not clean. Double-quoted bash -c "..." is not read: the outer shell
expands it first.

Measured on main's 122 workflows: 24 such scripts, 8 of them with
'"$VAR"' splices, all 24 parse. The self-test plants 12 new cases
(75 of 75 pass); main's gate run against the new cases fails the 7 that
expect a non-zero exit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown

X Brain Health Check

Score: 100.0/100
Status: 🟢 HEALTHY
Threshold: 80/100

X Brain is above merge threshold

@gHashTag

gHashTag commented Oct 5, 2026

Copy link
Copy Markdown
Owner Author

The three red checks on this PR come from main, not from this change.

check why it is red where it is red already
doc-refs #510's gft references: 7 new dangling references, 2 stale baseline entries Document references on main at 57de4a1f5, run 37225448197
orphan-artefacts the same #510 artefacts Orphaned artefacts on main at 57de4a1f5, run 37225448159
gates-can-fail (runs 37255200069, 37255237136) check_doc_refs and check_orphan_artefacts are "UNTESTABLE — already failing on a clean tree", so it tested 9 gates against a floor of 11. The 9 it did test all reject their defect. —

#872 turns doc-refs and orphan-artefacts green on its own branch. That gates-can-fail then reaches its floor again is an inference: it has not run on #872.

This PR changes only tools/check_run_syntax.py and its test. Its own gate, run-syntax, reads main's 122 workflows plus 24 inner scripts, and none fails to parse.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown

🧪 Brain Stress Test Results

Score: not measured (no stress test prints a Score: line)
Status: 🟢 PASS
Threshold: 270/300

X Stress test passed

@tri-claw

tri-claw Bot commented Oct 10, 2026

Copy link
Copy Markdown

Summary by t27-bees

The change adds a second parsing pass for single-quoted bash -c '...' / sh -c '...' arguments inside workflow run: steps. After the outer script passes bash -n, each inner script is extracted following the outer shell's quoting rules (escaped quotes '\'', splices like '"$VAR"' or '$X') and checked with bash -n / sh -n. Double-quoted arguments and zsh -c are not read.

Worth a look

  • tools/check_run_syntax.py:72-73 — The splice-handling loop appends only one OUTER_VALUE placeholder regardless of how many splices are found. A script like bash -c 'echo '$X' '$Y'' would become echo __outer__ instead of echo __outer__ __outer__, potentially hiding syntax errors or creating false ones.
  • tools/check_run_syntax.py:55 — INNER_OPEN uses \b and a complex flag pattern. It may not match bash -c '...' when flags like -euo pipefail are present because the alternation (?:-[a-zA-Z]*o[ \t]+[a-z]+|-[a-zA-Z]+) requires either -o <word> or a single-letter flag cluster, but -euo pipefail is a cluster followed by a separate word.
  • tools/check_run_syntax.py:58 — SPLICE for ${...} uses [^}]*, which stops at the first }. Nested braces like ${VAR:-${DEFAULT}} would be split incorrectly, though this is rare in practice.
  • tools/check_run_syntax.py:96-97 — When a glued quote is detected (e.g., 'echo x'y), CannotTell is raised and bubbles up to mark the entire workflow unreadable (exit 2). This matches the documented behavior but means one malformed inner script makes the whole file "cannot tell" rather than just flagging that script.
  • tools/check_run_syntax.py:109 — _INNER_SEEN is a global counter incremented per inner script found. If check() is called multiple times (e.g., in tests), the count accumulates across calls unless reset. main() resets it at line 284, but the module-level initialization at line 67 means imported state persists.

Read: 2 files, +111 -4, the whole diff. Head 909c2e5. Model: openai-compatible/nvidia/nemotron-3-ultra-550b-a55b. Commands: @t27-bees review | summary | help | pause | resume. Free for every repository.

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.

ci: check_run_syntax does not parse scripts inside single-quoted bash -c arguments

1 participant