From 32db0f81d565c5f05a6c07a2eb10b880fbd596e5 Mon Sep 17 00:00:00 2001 From: le king fu Date: Wed, 30 Sep 2026 20:53:13 -0400 Subject: [PATCH] test(ci): catch continue-on-error in any YAML spelling (#35) The pin was anchored at the start of the line, so a step written as a flow mapping, `- { name: x, run: false, continue-on-error: true }`, passed it: 12/12 green with that step appended to ci.yml. A quoted key, `"continue-on-error": true`, escaped it the same way. The detector is now unanchored and accepts a closing quote before the colon. It still runs on `ciCode`, so full comment lines may quote the key; anything else that quotes it (a run: script, a step name, a trailing comment) now fails the pin, loudly, by design. The detector is checked on its own: through the same comment stripping as `ciCode`, it must catch a block key, a list item, a flow mapping and a quoted key, and ignore a full comment line. Proven by mutation: the flow-style and quoted-key steps appended to ci.yml each turn the pin red; the old anchored regex turns the flow-mapping and quoted-key rows red; dropping the quote class turns the quoted-key row red; disabling the comment stripping turns the comment row red. Co-Authored-By: Claude Opus 5.5 --- __tests__/ci.test.js | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/__tests__/ci.test.js b/__tests__/ci.test.js index fcde6b5..208402a 100644 --- a/__tests__/ci.test.js +++ b/__tests__/ci.test.js @@ -15,10 +15,13 @@ const dockerfile = fs.readFileSync(path.join(ROOT, "Dockerfile"), "utf-8"); // Comment lines stripped, so an explanation that quotes a key cannot satisfy // (or break) a pin. -const ciCode = ci - .split("\n") - .filter((line) => !/^\s*#/.test(line)) - .join("\n"); +function stripCommentLines(text) { + return text + .split("\n") + .filter((line) => !/^\s*#/.test(line)) + .join("\n"); +} +const ciCode = stripCommentLines(ci); // Lines of a top-level-indented block (` pull_request:`), up to the next key at // the same or a lower indentation. @@ -77,10 +80,32 @@ describe("CI workflow", () => { if (/^\s*uses:|\n\s*uses:/.test(step)) expect(step).not.toMatch(/working-directory:/); } }); + + // `continue-on-error: true` turns a red step green without a trace in the + // commit status. Any spelling of the key counts: a block key or a list item, + // but also a flow mapping and a quoted key, which a pin anchored at the start + // of the line let through (#35). Unanchored on purpose: the key quoted in a + // `run:` script, a step name or a trailing comment fails the pin too, loudly. + // Only full comment lines are exempt, being stripped from `ciCode`. + const CONTINUE_ON_ERROR = /continue-on-error["']?\s*:/; + test("never lets a failing step pass", () => { - // `continue-on-error: true` turns a red step green without a trace in the - // commit status. Anchored on the key position, so only a real key counts. - expect(ciCode).not.toMatch(/^\s*(- )?continue-on-error\s*:/m); + expect(ciCode).not.toMatch(CONTINUE_ON_ERROR); + }); + + // The detector itself, through the same comment stripping as `ciCode`. Anchor + // it at the start of the line again and the last two spellings turn red. + test.each([ + ["a block key", " - name: x\n run: npm test\n continue-on-error: true"], + ["a list item", " - continue-on-error: true\n run: npm test"], + ["a flow mapping", " - { name: x, run: false, continue-on-error: true }"], + ["a quoted key", ' - name: x\n "continue-on-error": true'], + ])("the continue-on-error pin catches %s", (_, step) => { + expect(stripCommentLines(step)).toMatch(CONTINUE_ON_ERROR); + }); + + test("the continue-on-error pin ignores a full comment line quoting the key", () => { + expect(stripCommentLines(" # never add continue-on-error: true here")).not.toMatch(CONTINUE_ON_ERROR); }); });