Skip to content

Commit 8644157

Browse files
Goosterhofclaude
andcommitted
fix(lint:pkg): make Gate 6 ANSI-invariant so publint blocks fire in CI — queue #63
The lint:pkg wrapper captures publint stdout and matches PUBLINT_BLOCK_RE (/^(Suggestions|Warnings|Errors):$/m) to fail the gate on any advisory. In CI, publint colorizes its block headers (color-capable environment), emitting e.g. "\x1b[1m\x1b[34mSuggestions:\x1b[39m\x1b[22m". The leading SGR codes mean the line is not a bare "Suggestions:", so the regex never matched and the gate silently no-op'd — a CI false-NEGATIVE in which a genuine Warning/Error block would sail through undetected. Verified against raw CI logs: the gate had been a no-op in CI since publint 0.3.21 landed 2026-05-11; locally (plain-text, non-TTY) the regex matched and the gate fired correctly. Fix (belt-and-suspenders): spawn publint with NO_COLOR=1 AND strip residual ANSI SGR codes from captured stdout before the regex match, so the verdict is identical in every color environment (plain, TTY, FORCE_COLOR=1). ANSI_RE is built via String.fromCharCode(27) + RegExp() to avoid a control-char literal that oxlint's no-control-regex (Correctness) would reject. Header comment updated to record the verified CI-color root cause. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 9cfe18c commit 8644157

1 file changed

Lines changed: 38 additions & 7 deletions

File tree

scripts/lint-pkg.mjs

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,26 @@
44
// 1. publint + attw — treats publint suggestions/warnings/errors as fatal.
55
// publint 0.3.18 CLI does not expose a flag to fail on suggestions
66
// (--strict only promotes warnings → errors). This wrapper fills that gap:
7-
// it runs publint per workspace, captures stdout, and fails the gate if any
8-
// package emits a "Suggestions:", "Warnings:", or "Errors:" block.
9-
// attw --pack runs after publint per package and preserves its own exit code.
7+
// it runs publint per workspace, captures stdout, strips ANSI SGR codes,
8+
// and fails the gate if any package emits a "Suggestions:", "Warnings:", or
9+
// "Errors:" block. attw --pack runs after publint per package and preserves
10+
// its own exit code.
1011
// Motivated by enforcement queue #33 and the PR #35 regression: publint
1112
// suggestions about the "git+" URL prefix silently re-drifted across 10
1213
// packages because the gate tolerated them.
1314
//
15+
// ANSI invariance (enforcement queue #63): publint colors its block headers
16+
// when it detects a color-capable environment (TTY or FORCE_COLOR). In CI,
17+
// publint emitted ANSI-wrapped headers (e.g. "\x1b[1m\x1b[34mSuggestions:\x1b[39m\x1b[22m"),
18+
// so PUBLINT_BLOCK_RE — anchored on a bare "Suggestions:" line — never matched
19+
// and the gate silently no-op'd (false-NEGATIVE: a real Warning/Error block
20+
// would have sailed through CI undetected). Verified against raw CI logs:
21+
// the gate had been a no-op in CI since publint 0.3.21 landed 2026-05-11,
22+
// while locally (plain-text, non-TTY) the regex matched and the gate fired
23+
// correctly. Fix: spawn publint with NO_COLOR=1 AND strip residual ANSI from
24+
// captured stdout before the regex match — belt-and-suspenders so the verdict
25+
// is identical in every color environment (plain, TTY, FORCE_COLOR=1).
26+
//
1427
// 2. engines.node presence — closes enforcement queue #31 (drift-prevention
1528
// gate, deployed 2026-05-12). Every workspace package.json AND the root
1629
// package.json must declare a non-empty `engines.node` string. Value is NOT
@@ -27,6 +40,15 @@ import {join} from 'node:path';
2740
const PACKAGES_DIR = 'packages';
2841
const ROOT_MANIFEST = 'package.json';
2942
const PUBLINT_BLOCK_RE = /^(Suggestions|Warnings|Errors):$/m;
43+
// SGR / ANSI escape sequences (CSI ... final-byte). publint wraps its block
44+
// headers in these when color is enabled (CI default, FORCE_COLOR), which
45+
// otherwise defeats PUBLINT_BLOCK_RE's bare-line anchors. See header note,
46+
// enforcement queue #63.
47+
const ANSI_RE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*[a-zA-Z]`, 'g');
48+
49+
function stripAnsi(text) {
50+
return text.replace(ANSI_RE, '');
51+
}
3052

3153
function listPackageDirs() {
3254
return readdirSync(PACKAGES_DIR)
@@ -61,8 +83,14 @@ function checkEnginesNode(manifestPath, label) {
6183
return null;
6284
}
6385

64-
function runCaptured(cmd, args, cwd) {
65-
const result = spawnSync(cmd, args, {cwd, stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8', shell: false});
86+
function runCaptured(cmd, args, cwd, extraEnv) {
87+
const result = spawnSync(cmd, args, {
88+
cwd,
89+
stdio: ['ignore', 'pipe', 'pipe'],
90+
encoding: 'utf8',
91+
shell: false,
92+
env: extraEnv ? {...process.env, ...extraEnv} : process.env,
93+
});
6694
const stdout = result.stdout ?? '';
6795
const stderr = result.stderr ?? '';
6896
process.stdout.write(stdout);
@@ -95,8 +123,11 @@ function main() {
95123
process.stderr.write(` ${enginesFailure}\n`);
96124
}
97125

98-
const publint = runCaptured('npx', ['publint', 'run'], dir);
99-
const publintBlock = PUBLINT_BLOCK_RE.exec(publint.stdout);
126+
// NO_COLOR=1 keeps publint's output plain regardless of runner color
127+
// settings; stripAnsi defends against any residual SGR codes so the
128+
// PUBLINT_BLOCK_RE verdict is identical in every environment (queue #63).
129+
const publint = runCaptured('npx', ['publint', 'run'], dir, {NO_COLOR: '1'});
130+
const publintBlock = PUBLINT_BLOCK_RE.exec(stripAnsi(publint.stdout));
100131
if (publint.status !== 0) {
101132
failures.push(`${name}: publint exited ${publint.status}`);
102133
} else if (publintBlock) {

0 commit comments

Comments
 (0)