forgetest: the lint's stray-quote rule reads a step's first key, on its dash line, too

The rule that a key with one stray quote makes every step of the
workflow count as no step never saw a step's first key, the one on its
dash line. _STEP_RX reads no key with a stray quote (`- run": make`,
`- name": Build`), and _stray_key cannot match a line that starts with
`- `. So such a step dropped out with no "unread" set, and the
workflow's other steps still counted. YAML reads the key with its quote
(PyYAML gives {'name"': 'Build', 'run': 'make'}), or refuses the file
(`- "run: make`), and GitHub refuses a workflow with a step key outside
the step schema: a HOST_TESTED row passed the lint for a workflow that
runs nothing. The HOST_TESTED comment, the workflow_steps docstring and
the Test page state the rule for a step's key, and every step's first
key is on its dash line. No workflow of the tree has such a key.

workflow_steps now reads the first key of every list item as it reads a
step's other keys: the dash and the spaces after it become spaces, and
a key with one stray quote there makes every step of the workflow carry
"unread", which _on_every_run refuses. The rule also takes in a list
item outside the steps (a matrix include entry): that only lists more.
The docstring says so.

Proof: forgetest-ci "Unit tests" (`python -B -m unittest discover -s
tests -v` in forgetest): test_manifest.py runs 39 tests, OK, on Windows
with python 3.14 and on Ubuntu 24.04 with python 3.12.
test_a_step_counts_only_when_every_run_of_the_workflow_runs_it_and_its_failure_fails_the_run
gains a step `- run": make` before a step that runs the test: no step
counts. Negative control on a scratch copy: manifest.py as it was fails
that case (True is not false). A probe of workflow_runs: `- name":
Build` with `run: make` under it, and `- "run: make`, each before a
step that runs the test, count as no step, as a stray quote on a
step's second key already did. The 17 workflows of the tree's
repositories (211 steps) read to the same events and steps before and
after the change. The coverage lint on the tree manifest from the
recipe pins reports 0 uncovered paths, 0 host-tested row problems, 43
tests. pyflakes reports no undefined name.

Acceptance: none. forgetest is dev-only and in no fingerprint (the
source_sha of 43 of 43 tests is unchanged); the lint runs in CI, not in
a campaign, and tests/ are in no fingerprint.
This commit is contained in:
ScottW514
2026-10-17 06:59:13 -04:00
parent 609ae561bf
commit 3aecf800a5
2 changed files with 12 additions and 5 deletions
+9 -3
View File
@@ -511,9 +511,10 @@ def workflow_steps(text):
it; comments, whole lines and from ' #' to the end of a line, are not it; comments, whole lines and from ' #' to the end of a line, are not
read. Anything outside a step (a trigger's paths, a job's name) is not read. Anything outside a step (a trigger's paths, a job's name) is not
a step. A key with one stray quote (`if":`, `"run:`) at the top level, a step. A key with one stray quote (`if":`, `"run:`) at the top level,
at a job's name or a job's key, or at a step's key is no key the lint at a job's name or a job's key, at a step's key, or as the first key of
reads: every step of the workflow then carries "unread" and counts as any list item (a step's first key is on its dash line: `- run": make`)
no step.""" is no key the lint reads: every step of the workflow then carries
"unread" and counts as no step."""
lines = _lines(text) lines = _lines(text)
steps, i = [], 0 steps, i = [], 0
in_jobs, job_at, key_at, job = False, None, None, None in_jobs, job_at, key_at, job = False, None, None, None
@@ -534,6 +535,11 @@ def workflow_steps(text):
ind = _indent(lines[i]) ind = _indent(lines[i])
stray = stray or ind == 0 or (in_jobs and (job_at is None or ind == job_at or ( stray = stray or ind == 0 or (in_jobs and (job_at is None or ind == job_at or (
ind > job_at and (key_at is None or ind == key_at)))) ind > job_at and (key_at is None or ind == key_at))))
# A list item's first key is on its dash line, where _STEP_RX reads no
# key with a stray quote: the dash and the spaces after it become spaces.
item = re.match(r"^\s*-\s+", lines[i])
if item and _stray_key(" " * item.end() + lines[i][item.end():]):
stray = True
m = _STEP_RX.match(lines[i]) m = _STEP_RX.match(lines[i])
if not m: if not m:
i += 1 i += 1
+3 -2
View File
@@ -330,10 +330,11 @@ class CoverageReportTests(unittest.TestCase):
# however the step is spelled: more than one space after its dash, or a quoted key # however the step is spelled: more than one space after its dash, or a quoted key
self.assertFalse(self._runs("steps:\n - run: ./build/tray_test\n if: false\n", test)) self.assertFalse(self._runs("steps:\n - run: ./build/tray_test\n if: false\n", test))
self.assertFalse(self._runs(step % " \"if\": false\n", test)) self.assertFalse(self._runs(step % " \"if\": false\n", test))
# a key with one stray quote is no key the lint reads: at a step's key, a job's key or # a key with one stray quote is no key the lint reads: at a step's key (its first one on
# the top level, no step of the workflow counts # its dash line too), a job's key or the top level, no step of the workflow counts
self.assertFalse(self._runs(step % " if\": false\n", test)) self.assertFalse(self._runs(step % " if\": false\n", test))
self.assertFalse(self._runs("steps:\n - name: x\n run\": ./build/tray_test\n", test)) self.assertFalse(self._runs("steps:\n - name: x\n run\": ./build/tray_test\n", test))
self.assertFalse(self._runs("steps:\n - run\": make\n - run: ./build/tray_test\n", test))
self.assertFalse(self._runs("jobs:\n t:\n if\": false\n steps:\n - run: ./build/tray_test\n", self.assertFalse(self._runs("jobs:\n t:\n if\": false\n steps:\n - run: ./build/tray_test\n",
test)) test))
self.assertFalse(self._runs("jobs\":\n t:\n if: false\n steps:\n - run: ./build/tray_test\n", self.assertFalse(self._runs("jobs\":\n t:\n if: false\n steps:\n - run: ./build/tray_test\n",