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:
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user