From 81fc602219bb8bb49ce6fc4ea0f60c13eee2caad Mon Sep 17 00:00:00 2001 From: KnockOutEZ Date: Sat, 29 Aug 2026 06:12:20 +0600 Subject: [PATCH 1/2] test(build): pin the prepare hook's off-value spellings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WIGOLO_SKIP_PREPARE is gated on bare truthiness, so =0/=false/=off all mean SKIP — the inverse of the operator's intent, and the opposite of the rule autoLaunchDisabled establishes one file over for the same class. Two arms: the off spellings must BUILD and must not print the skip line, and the values CI actually sets must still skip and still print it. --- tests/unit/prepare-build.test.ts | 44 ++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/unit/prepare-build.test.ts b/tests/unit/prepare-build.test.ts index 4a1f447d..b7098edb 100644 --- a/tests/unit/prepare-build.test.ts +++ b/tests/unit/prepare-build.test.ts @@ -154,6 +154,50 @@ describe('scripts/prepare-build.mjs — the git-dependency build hook', () => { expect(run.stdout).toMatch(/WIGOLO_SKIP_PREPARE/); }, 60_000); + /* + * The opt-out's OFF spellings, which bare truthiness read backwards. + * + * `if (process.env.WIGOLO_SKIP_PREPARE)` made `=0`, `=false` and `=off` all mean SKIP — the + * inverse of what the operator wrote — and this repo had already established the opposite rule + * one file over: `autoLaunchDisabled` (src/studio/auto-launch.ts) trims, lowercases and compares + * against `{'0','false','off'}` for exactly this class. Two flags shipped by one phase cannot + * disagree about what `0` means. + * + * The fail direction is quiet, which is why it needs arms rather than a reading: a local + * `npm ci` under a leaked `WIGOLO_SKIP_PREPARE=0` exits 0 with an unbuilt tree, and the missing + * `dist/` surfaces weeks later as module-not-found in whatever consumes it. + */ + const OFF_SPELLINGS = ['0', 'false', 'off', ' 0 ', 'FALSE', 'Off', ' False ', ' ', '']; + + it('BUILDS for every value that means off — `0`/`false`/`off`, in any casing, trimmed', () => { + plantToolchain('tsup'); + plantToolchain('typescript'); + plantBuild(); + for (const value of OFF_SPELLINGS) { + const run = runPrepare({ WIGOLO_SKIP_PREPARE: value }); + expect(run.status, JSON.stringify(value)).toBe(0); + expect(run.built, JSON.stringify(value)).toBe(true); + // Not merely "it built": a guard that announced the skip and then built anyway would leave + // the CI legs' own grep lying about what the install did. + expect(run.stdout, JSON.stringify(value)).not.toMatch(/WIGOLO_SKIP_PREPARE/); + } + }, 120_000); + + it('still SKIPS for every value CI actually sets, and prints the line those legs grep', () => { + // Anti-vacuity twin for the arm above: normalising the off values must not soften the opt-out + // itself. `=1` is the only spelling in the workflows and the Dockerfile today; the rest are + // here because "any other non-empty value skips" is the stated rule, not "1 skips". + plantToolchain('tsup'); + plantToolchain('typescript'); + plantBuild(); + for (const value of ['1', 'true', 'yes', 'TRUE', ' 1 ', 'no']) { + const run = runPrepare({ WIGOLO_SKIP_PREPARE: value }); + expect(run.status, JSON.stringify(value)).toBe(0); + expect(run.built, JSON.stringify(value)).toBe(false); + expect(run.stdout, JSON.stringify(value)).toMatch(/WIGOLO_SKIP_PREPARE/); + } + }, 120_000); + it('propagates a failing build rather than swallowing it', () => { // The opt-out must not become a blanket exit 0. A build that fails on the // git-dependency path has to fail the install, or the consumer gets a partial dist/. From 4ad94c789dcdac0b6ed1bc27ad6cebcfea36465a Mon Sep 17 00:00:00 2001 From: KnockOutEZ Date: Sat, 29 Aug 2026 06:12:25 +0600 Subject: [PATCH 2/2] fix(build): WIGOLO_SKIP_PREPARE honours 0/false/off instead of skipping on them The opt-out read any non-empty value as "skip", so a leaked WIGOLO_SKIP_PREPARE=0 made a local npm ci exit 0 with an unbuilt tree and surfaced weeks later as module-not-found. Normalize the way the phase's own rule does: trim, lowercase, and treat 0/false/off as off. Every value CI sets still skips, and still prints the line those legs grep. --- scripts/prepare-build.mjs | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/scripts/prepare-build.mjs b/scripts/prepare-build.mjs index 09b9490b..c849cf69 100644 --- a/scripts/prepare-build.mjs +++ b/scripts/prepare-build.mjs @@ -39,8 +39,37 @@ import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); -/** Opt-out for a caller that will build explicitly itself. Any non-empty value counts. */ -if (process.env.WIGOLO_SKIP_PREPARE) { +/** + * The spellings that mean OFF, matched trimmed and lowercased. + * + * ⚠ THIS WAS BARE TRUTHINESS, AND BARE TRUTHINESS READS THE FLAG BACKWARDS. `if (process.env.X)` + * makes `=0`, `=false` and `=off` all mean SKIP — the inverse of what the operator wrote — while + * this repo established the opposite rule one file over in the same phase: `autoLaunchDisabled` + * (`src/studio/auto-launch.ts`) trims, lowercases and compares against exactly these three values. + * Two flags shipped together cannot disagree about what `0` means. + * + * The fail direction is quiet rather than loud, which is why it is worth a set instead of a cast: + * a local `npm ci` under a leaked `WIGOLO_SKIP_PREPARE=0` exits 0 with an unbuilt tree, and the + * absent `dist/` surfaces much later as module-not-found in whatever consumes this package. + */ +const SKIP_OFF_VALUES = new Set(['0', 'false', 'off']); + +/** + * Opt-out for a caller that will build explicitly itself. + * + * Any non-empty value counts EXCEPT the off spellings above — so `=1`, the only value CI and the + * Dockerfile actually set, still skips. Trimmed before the comparison for the same reason + * `isLoopbackHost` (`src/studio/bind.ts`) trims: a value that arrived with the shell's whitespace + * still attached is the same stated intent. A value that is whitespace ONLY states no intent at + * all, so it falls back to the unset default, which is to build. + */ +function skipRequested(raw) { + if (raw === undefined) return false; + const value = raw.trim().toLowerCase(); + return value !== '' && !SKIP_OFF_VALUES.has(value); +} + +if (skipRequested(process.env.WIGOLO_SKIP_PREPARE)) { console.log('prepare: no build — WIGOLO_SKIP_PREPARE is set; the caller builds explicitly.'); process.exit(0); }