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:
+121
-4
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user