From 3aecf800a5288611711d1e6e8954088d1299d975 Mon Sep 17 00:00:00 2001 From: ScottW514 Date: Sat, 17 Oct 2026 06:59:13 -0400 Subject: [PATCH] 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. --- forgetest/forgetest/manifest.py | 12 +++++++++--- forgetest/tests/test_manifest.py | 5 +++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/forgetest/forgetest/manifest.py b/forgetest/forgetest/manifest.py index ff17b21..57e517a 100644 --- a/forgetest/forgetest/manifest.py +++ b/forgetest/forgetest/manifest.py @@ -511,9 +511,10 @@ def workflow_steps(text): 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 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 - reads: every step of the workflow then carries "unread" and counts as - no step.""" + at a job's name or a job's key, at a step's key, or as the first key of + any list item (a step's first key is on its dash line: `- run": make`) + is no key the lint reads: every step of the workflow then carries + "unread" and counts as no step.""" lines = _lines(text) steps, i = [], 0 in_jobs, job_at, key_at, job = False, None, None, None @@ -534,6 +535,11 @@ def workflow_steps(text): ind = _indent(lines[i]) 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)))) + # 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]) if not m: i += 1 diff --git a/forgetest/tests/test_manifest.py b/forgetest/tests/test_manifest.py index 832b85c..ba7df1a 100644 --- a/forgetest/tests/test_manifest.py +++ b/forgetest/tests/test_manifest.py @@ -330,10 +330,11 @@ class CoverageReportTests(unittest.TestCase): # 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(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 - # the top level, no step of the workflow counts + # a key with one stray quote is no key the lint reads: at a step's key (its first one on + # 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("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", test)) self.assertFalse(self._runs("jobs\":\n t:\n if: false\n steps:\n - run: ./build/tray_test\n",