check-ci-graph: refuse a key or needs: it cannot read plainly (w0j4)

Quoted, anchored and multi-line needs: values, and quoted, spaced or
explicit ? keys, all skipped kb on a PR while the gate reported every
suite run. Rather than parse more YAML, every structural line inside
jobs: must take one plain form or the gate names it and fails. Also a
1be2 case at the one-column boundary, which the owner+1 rival passed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
This commit is contained in:
Edward Langley
2026-09-23 17:02:13 -07:00
parent 2aafff3b4c
commit 3eafd4dec7
3 changed files with 193 additions and 4 deletions
+121 -4
View File
@@ -76,6 +76,28 @@ _DISQUALIFY = ("if", "continue-on-error", "shell")
_JOB_DISQUALIFY = _DISQUALIFY + ("defaults",)
_NAME = r"[A-Za-z_][\w-]*"
_TOP_KEY = re.compile(rf"^{_NAME}:(\s|$)")
_NEEDS_VALUE = re.compile(rf"^(|{_NAME}|\[\s*{_NAME}(\s*,\s*{_NAME})*\s*\])$")
_NEEDS_ITEM = re.compile(rf"^ - {_NAME}$")
def _readable(line, indent, in_steps, in_needs):
"""Whether a structural line inside `jobs:` is one this gate can read. Anything
else is refused, not parsed: context/code/tooling.md (improvise-w0j4)."""
if indent == 2:
return bool(_JOB.match(line))
if indent == 4:
m = _JOB_KEY.match(line)
return bool(m) and (m.group(1) != "needs"
or bool(_NEEDS_VALUE.match(_inline_value(m.group(2)))))
if in_needs:
return bool(_NEEDS_ITEM.match(line))
if in_steps:
return indent in (6, 8) and bool(_STEP_KEY.match(line))
return indent > 4
def _inline_value(rest):
"""A key's inline value, without a YAML comment (`#` after whitespace)."""
return re.sub(r"\s+#.*$", "", rest).strip()
@@ -98,8 +120,9 @@ def ci_steps(text):
Nothing is read as shell. A key with an inline value owns every following
line indented deeper, which is how a block scalar, a folded value, a plain
continuation or a heredoc inside one is skipped whole (improvise-st8q)."""
steps, stray = [], []
steps, stray, unread = [], [], []
job, in_steps, job_flags, step = None, False, set(), None
in_jobs = False
workflow_flags = set()
owner, owner_step, quote = None, None, None
needs, if_jobs, in_needs = {}, set(), False
@@ -125,6 +148,13 @@ def ci_steps(text):
owner, owner_step = None, None
if not raw.strip() or raw.lstrip().startswith("#"):
continue
line = re.sub(r"\s+#.*$", "", raw)
if indent == 0:
in_jobs = line.startswith("jobs:")
if not _TOP_KEY.match(line):
unread.append(n)
elif in_jobs and indent <= 8 and not _readable(line, indent, in_steps, in_needs):
unread.append(n)
if raw.startswith("defaults:"):
workflow_flags.add("workflow defaults")
m = _JOB.match(raw)
@@ -181,7 +211,7 @@ def ci_steps(text):
for s in steps:
s["flags"] |= workflow_flags
s["flags"] |= {f"needs {j}, which has if" for j in _needed(s["job"], needs) & if_jobs}
return steps, stray
return steps, stray, unread
def _needed(job, needs):
@@ -214,11 +244,16 @@ def _unrun(s):
"ci_suites in scripts/checkers.sh")
def _unread(n):
return (f"ci.yml line {n} is a key or `needs:` value this gate cannot read as a "
"plain name, so it cannot trust the graph; spell it plainly")
def suite_verdict(suites, text):
"""The complaints: suites no counted step runs, and every step or line that
does not count, each named so the fix is obvious rather than silent."""
steps, stray = ci_steps(text)
out, ran, missing = [], set(), set()
steps, stray, unread = ci_steps(text)
out, ran, missing = [_unread(n) for n in unread], set(), set()
for s in steps:
m = _CI_CALL.match(s["run"] or "")
if not m:
@@ -269,6 +304,19 @@ def _stray(n):
"whole `run:` is `./scripts/checkers.sh --ci <job>`")
_GATE = " gate:\n if: false\n steps:\n - run: echo\n"
_KB_RUN = " - run: ./scripts/checkers.sh --ci kb\n"
_NEEDS_GATE = ["job `kb`'s `checkers.sh --ci` step is conditional (needs gate, which has if), "
"so it does not count"] + _NO_PW
def _unreadable(name, text, *markers, rest=()):
"""A case whose workflow spells a key or a `needs:` value the gate must refuse."""
lines = text.split("\n")
at = [next(i for i, l in enumerate(lines, 1) if m in l) for m in markers]
return (name, _REQ, text, [_unread(n) for n in at] + list(rest))
# (name, suites to require, workflow text, the complaints it must draw). Every
# case asks the REAL checkers.sh table, where `kb`'s row holds pr-watch.
SUITE_SELF_TEST = [
@@ -332,6 +380,75 @@ SUITE_SELF_TEST = [
["job `kb`'s `checkers.sh --ci` step is conditional (needs gate, which has if), "
"so it does not count"] + _NO_PW),
("the 1be2 continuation exactly one column past the key does not count (w0j4)", _REQ,
_wf(_BUILD, _job("kb", _KB_RUN + " || true\n")),
["job `kb`'s `checkers.sh --ci` step's run: continues onto another line, "
"so it does not count"] + _NO_PW),
("control: a padded flow list with a comment is still read (w0j4)", _REQ,
_wf(_BUILD, _GATE, _job("kb", _KB_RUN, head=" needs: [ gate ] # c\n")),
_NEEDS_GATE),
("control: a plain scalar needs: is still read (w0j4)", _REQ,
_wf(_BUILD, _GATE, _job("kb", _KB_RUN, head=" needs: gate\n")),
_NEEDS_GATE),
_unreadable("a quoted name in a flow needs: is refused (w0j4)",
_wf(_BUILD, _GATE, _job("kb", _KB_RUN, head=' needs: ["gate"]\n')),
'needs: ["gate"]'),
_unreadable("a quoted scalar needs: is refused (w0j4)",
_wf(_BUILD, _GATE, _job("kb", _KB_RUN, head=" needs: 'gate'\n")),
"needs: 'gate'"),
_unreadable("a quoted item in a block needs: is refused (w0j4)",
_wf(_BUILD, _GATE, _job("kb", _KB_RUN, head=' needs:\n - "gate"\n')),
'- "gate"'),
_unreadable("an anchored flow needs: is refused (w0j4)",
_wf(_BUILD, _GATE, _job("kb", _KB_RUN, head=" needs: &d [gate]\n")),
"needs: &d"),
_unreadable("an anchored block needs: is refused (w0j4)",
_wf(_BUILD, _GATE, _job("kb", _KB_RUN, head=" needs: &d\n - gate\n")),
"needs: &d"),
_unreadable("a flow needs: over two lines is refused (w0j4)",
_wf(_BUILD, _GATE, _job("kb", _KB_RUN, head=" needs: [build,\n gate]\n")),
"needs: [build,"),
_unreadable("a quoted job key is refused (w0j4)",
_wf(_BUILD, _job("kb", _KB_RUN, head=' "if": false\n')),
'"if"'),
_unreadable("a quoted step key is refused (w0j4)",
_wf(_BUILD, _job("kb", " - \"if\": false\n"
" run: ./scripts/checkers.sh --ci kb\n")),
'"if"'),
_unreadable("a single-quoted continue-on-error is refused (w0j4)",
_wf(_BUILD, _job("kb", _KB_RUN, head=" 'continue-on-error': true\n")),
"'continue-on-error'"),
_unreadable("a key with a space before its colon is refused (w0j4)",
_wf(_BUILD, _job("kb", _KB_RUN, head=" if : false\n")),
"if : false"),
_unreadable("a quoted job name is refused (w0j4)",
_wf(_BUILD, ' "gate":\n if: false\n steps:\n - run: echo\n',
_job("kb", _KB_RUN)),
'"gate"',
rest=["job `build`'s `checkers.sh --ci` step is conditional (job if), "
"so it does not count", _unrun("assemble-stamp")]),
_unreadable("a quoted top-level key is refused (w0j4)",
'"defaults":\n run:\n shell: bash\n' + _wf(_BUILD, _job("kb", _KB_RUN)),
'"defaults"'),
_unreadable("an explicit ? key is refused (w0j4)",
_wf(_BUILD, _job("kb", _KB_RUN, head=" ? if\n : false\n")),
"? if", ": false"),
("the same, two needs: away", _REQ,
_wf(_BUILD, " gate:\n if: false\n steps:\n - run: echo\n",
" mid:\n needs: gate\n steps:\n - run: echo\n",