check-ci-graph: a continued run: or a skipped need does not count

A plain scalar folds a deeper-indented next line into run:, so
`|| true` there made the counted call fail-open (improvise-1be2).
A job that needs a job under if: is skipped with it, directly or
transitively (improvise-bbwa). The unrun message now points at the
ci_suites row. tooling.md names the checkout-with-ref hole.

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 11:06:47 -07:00
parent d6706774a2
commit 1501def5c2
3 changed files with 113 additions and 10 deletions
+60 -4
View File
@@ -101,7 +101,8 @@ def ci_steps(text):
steps, stray = [], []
job, in_steps, job_flags, step = None, False, set(), None
workflow_flags = set()
owner, quote = None, None
owner, owner_step, quote = None, None, None
needs, if_jobs, in_needs = {}, set(), False
def close_step():
if step is not None:
@@ -118,8 +119,10 @@ def ci_steps(text):
if owner is not None and (not raw.strip() or indent > owner):
if "scripts/checkers.sh" in raw:
stray.append(n)
if owner_step is not None and raw.strip():
owner_step["multiline"] = True
continue
owner = None
owner, owner_step = None, None
if not raw.strip() or raw.lstrip().startswith("#"):
continue
if raw.startswith("defaults:"):
@@ -128,7 +131,10 @@ def ci_steps(text):
if m:
close_step()
step = None
job, in_steps, job_flags = m.group(1), False, set()
job, in_steps, job_flags, in_needs = m.group(1), False, set(), False
continue
if in_needs and re.match(r"^ - ", raw):
needs[job].add(_inline_value(raw.strip()[2:]))
continue
m = _JOB_KEY.match(raw)
if m and job is not None:
@@ -136,6 +142,11 @@ def ci_steps(text):
step = None
key, value = m.group(1), _inline_value(m.group(2))
in_steps = key == "steps" and value == ""
in_needs = key == "needs" and value == ""
if key == "needs":
needs[job] = {x.strip() for x in value.strip("[]").split(",") if x.strip()}
if key == "if":
if_jobs.add(job)
if key in _JOB_DISQUALIFY:
job_flags.add(key)
for s in steps:
@@ -165,12 +176,25 @@ def ci_steps(text):
quote = value[0]
else:
owner = len(k.group(1)) + (2 if raw.lstrip().startswith("- ") else 0)
owner_step = step if m and m.group(1) == "run" else None
close_step()
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
def _needed(job, needs):
"""Every job `job` needs, transitively: a skipped one skips it too (improvise-bbwa)."""
seen, todo = set(), list(needs.get(job, ()))
while todo:
j = todo.pop()
if j not in seen:
seen.add(j)
todo += needs.get(j, ())
return seen
def would_run(args):
"""What checkers.sh itself says these arguments run. Its argument rules and
its CI table are not restated here (improvise-maax)."""
@@ -186,7 +210,8 @@ def would_run(args):
def _unrun(s):
return f"`{s}` is a checkers.sh suite that no CI step runs; add it to ci.yml"
return (f"`{s}` is a checkers.sh suite that no CI step runs; add it to a row of "
"ci_suites in scripts/checkers.sh")
def suite_verdict(suites, text):
@@ -201,6 +226,9 @@ def suite_verdict(suites, text):
if m.group(1) != s["job"]:
out.append(f"job `{s['job']}` runs `checkers.sh --ci {m.group(1)}`; "
"a job runs only its own row of the CI table")
elif s.get("multiline"):
out.append(f"job `{s['job']}`'s `checkers.sh --ci` step's run: continues "
"onto another line, so it does not count")
elif s["flags"]:
out.append(f"job `{s['job']}`'s `checkers.sh --ci` step is conditional "
f"({', '.join(sorted(s['flags']))}), so it does not count")
@@ -284,6 +312,34 @@ SUITE_SELF_TEST = [
" steps:\n - run: echo\n"),
[_stray(8)] + _NO_PW),
("the call with a continuation line does not count (1be2)", _REQ,
_wf(_BUILD, _job("kb", " - run: ./scripts/checkers.sh --ci kb\n"
" || true\n")),
["job `kb`'s `checkers.sh --ci` step's run: continues onto another line, "
"so it does not count"] + _NO_PW),
("a job that needs a job under if: does not count (bbwa)", _REQ,
_wf(_BUILD, " gate:\n if: false\n steps:\n - run: echo\n",
_job("kb", " - run: ./scripts/checkers.sh --ci kb\n",
head=" needs: [build, gate]\n")),
["job `kb`'s `checkers.sh --ci` step is conditional (needs gate, which has if), "
"so it does not count"] + _NO_PW),
("the same, with needs: written as a list", _REQ,
_wf(_BUILD, " gate:\n if: false\n steps:\n - run: echo\n",
_job("kb", " - run: ./scripts/checkers.sh --ci kb\n",
head=" needs:\n - gate\n")),
["job `kb`'s `checkers.sh --ci` step is conditional (needs gate, which has if), "
"so it does not count"] + _NO_PW),
("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",
_job("kb", " - run: ./scripts/checkers.sh --ci kb\n",
head=" needs: [mid]\n")),
["job `kb`'s `checkers.sh --ci` step is conditional (needs gate, which has if), "
"so it does not count"] + _NO_PW),
("a step under if: does not count (tvte)", _REQ,
_wf(_BUILD, _job("kb", " - if: false\n run: ./scripts/checkers.sh --ci kb\n")),
["job `kb`'s `checkers.sh --ci` step is conditional (if), so it does not count"]