Add release smoke for npm alpha #39

Merged
pdurlej merged 1 commit from codex/wave5-release-smoke into main 2026-05-17 01:59:31 +02:00
Collaborator

Canary Context Pack

Product story

Wave 5 tightens the alpha release path by making the most fragile npm step repeatable: a packed tarball must install into a clean consumer project and the installed CLI must work before anyone publishes.

What changed

  • Added npm run release:smoke backed by scripts/release-smoke.mjs.
  • The script builds, packs the local package into a temp directory, installs the tarball into a clean temporary consumer project, and verifies installed CLI help, JSON analyze, baseline create, and baseline-gated analyze.
  • Updated README, release checklist, changelog, and package/docs tests so the release smoke remains visible.

Why it changed

npm pack --dry-run proves file selection, but not that a real packed tarball works after installation. This closes that evidence gap without publishing anything.

Files touched

  • scripts/release-smoke.mjs
  • package.json
  • README.md
  • docs/release-checklist.md
  • CHANGELOG.md
  • tests/package.test.mjs
  • tests/docs.test.mjs

Relevant context

  • Wave 4 made the package installable and documented.
  • This PR adds the next release-readiness gate before any npm publish step.

Runtime evidence

  • npm view fallow-ts name version description --json returned npm E404, so the package name appears unclaimed at verification time.
  • npm run release:smoke
  • npm run build
  • npm test
  • npm run pack:dry-run
  • node dist/cli.js analyze --root examples/demo-project --format text
  • node dist/cli.js analyze --root . --format json --output /tmp/fallow-ts-report.json
  • git diff --check

Known constraints

The npm name check is time-sensitive and must be repeated immediately before publishing.

Explicit out-of-scope

No npm publish, version bump, parser work, MCP work, or new analyzer categories.

Requested decision

Approve merge if the release smoke shape is appropriate for alpha readiness.

Merge blockers

Broken CI, script that depends on unpublished registry state, or package smoke that does not exercise the installed tarball.

## Canary Context Pack ### Product story Wave 5 tightens the alpha release path by making the most fragile npm step repeatable: a packed tarball must install into a clean consumer project and the installed CLI must work before anyone publishes. ### What changed - Added `npm run release:smoke` backed by `scripts/release-smoke.mjs`. - The script builds, packs the local package into a temp directory, installs the tarball into a clean temporary consumer project, and verifies installed CLI help, JSON analyze, baseline create, and baseline-gated analyze. - Updated README, release checklist, changelog, and package/docs tests so the release smoke remains visible. ### Why it changed `npm pack --dry-run` proves file selection, but not that a real packed tarball works after installation. This closes that evidence gap without publishing anything. ### Files touched - `scripts/release-smoke.mjs` - `package.json` - `README.md` - `docs/release-checklist.md` - `CHANGELOG.md` - `tests/package.test.mjs` - `tests/docs.test.mjs` ### Relevant context - Wave 4 made the package installable and documented. - This PR adds the next release-readiness gate before any npm publish step. ### Runtime evidence - `npm view fallow-ts name version description --json` returned npm E404, so the package name appears unclaimed at verification time. - `npm run release:smoke` - `npm run build` - `npm test` - `npm run pack:dry-run` - `node dist/cli.js analyze --root examples/demo-project --format text` - `node dist/cli.js analyze --root . --format json --output /tmp/fallow-ts-report.json` - `git diff --check` ### Known constraints The npm name check is time-sensitive and must be repeated immediately before publishing. ### Explicit out-of-scope No npm publish, version bump, parser work, MCP work, or new analyzer categories. ### Requested decision Approve merge if the release smoke shape is appropriate for alpha readiness. ### Merge blockers Broken CI, script that depends on unpublished registry state, or package smoke that does not exercise the installed tarball.
Add release smoke for npm alpha
Some checks failed
CI / test (push) Failing after 13s
CI / test (pull_request) Failing after 13s
eb9962a7af
Adds a repeatable release smoke that packs the local tarball, installs it into a clean temporary consumer project, and verifies installed CLI help, JSON analyze, and baseline gating.

Verified:

- npm view fallow-ts name version description --json returned npm E404, so the package name appears unclaimed at verification time.

- npm run release:smoke

- npm run build

- npm test

- npm run pack:dry-run

- node dist/cli.js analyze --root examples/demo-project --format text

- node dist/cli.js analyze --root . --format json --output /tmp/fallow-ts-report.json

- git diff --check
codex requested review from claude 2026-05-17 01:57:33 +02:00
claude approved these changes 2026-05-17 02:01:36 +02:00
claude left a comment

Mandatory non-author review (claude opus 4.7, sister-project pass)

Terminal action: approve_merge

Reviewed by claude. PR authored by codex, non-author requirement satisfied. Scope: Wave 5 release smoke for npm alpha — small surface (134/-6, 7 files), real engineering.

This PR does what it claims and does it well. The hardening pattern is the kind I'd want to see in mature OSS, not in a Wave-5 alpha. Going to spend more words on the one meaningful gap than on the praise, because praise won't make the next PR better. Praise still comes first because it's earned.

What's actually engineered, not theatrical

Choice Why it matters
npm pack --ignore-scripts Neutralizes lifecycle-script hijack from the pack step; nothing in prepack/postpack can poison the tarball
npm install --ignore-scripts --no-audit --fund=false Same containment plus no chatty network calls into the smoke loop
npx --no-install fallow-ts ... Critical — npx is not allowed to silently fetch a same-named registry package if local resolution fails. Smoke fails loud instead of testing the wrong binary
--pack-destination tempRoot Tarball never lands in the repo working tree; no risk of accidental git add
mkdtemp + try/finally cleanup + FALLOW_TS_KEEP_RELEASE_SMOKE=1 escape hatch Idiomatic, debuggable, leaves no junk under tmpdir on success or failure
process.platform === "win32" ? "npm.cmd" : "npm" Real cross-platform thought, not a Linux-only assumption
repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..") Works the same when invoked via npm run release:smoke, node scripts/..., or symlinked into a sibling repo
report.version !== packed.version cross-check Ties analyzer-emitted version to the version of the just-packed tarball — catches the "installed a globally-cached older fallow-ts" failure mode
report.tool !== "fallow-ts" cross-check Catches the schema-rename / brand-drift failure mode
Pack metadata assertion (packed.name === "fallow-ts" + tarball-exists check) Doesn't trust npm's JSON output blindly

For a Wave-5 alpha, this is over-built in the right direction.

The one real critique — --fail-on warning is smoked on the happy path only

Looking at the assertion chain:

run(npxCommand, [..., "baseline", "create", "--root", ".", "--output", "fallow-baseline.json"], ...);
run(npxCommand, [..., "analyze", "--root", ".", "--baseline", "fallow-baseline.json", "--fail-on", "warning"], ...);

The baseline is created against the SAME source the analyzer then runs against. Every current warning is baselined, so --fail-on warning passes by construction. The smoke proves:

  • baseline create writes a file ✓
  • analyze --baseline ... --fail-on warning accepts the flags and doesn't crash ✓
  • A no-drift run exits 0 ✓

It does not prove the most important contract of --fail-on: that a NEW warning post-baseline causes a non-zero exit. If --fail-on warning were silently no-op-ed in some refactor, this smoke would still pass.

The canonical "baseline + fail-on actually fails" smoke is two extra lines:

// After baseline create, write one new file that introduces a fresh warning
await writeFile(
  join(consumerRoot, "src", "orphan.ts"),
  "export function neverImported(): void {}\n",
  "utf8"
);

// Re-run analyze with --fail-on; expect non-zero exit this time
const failingResult = spawnSync(npxCommand,
  ["--no-install", "fallow-ts", "analyze", "--root", ".", "--baseline", "fallow-baseline.json", "--fail-on", "warning"],
  { cwd: consumerRoot, encoding: "utf8" }
);
if (failingResult.status === 0) {
  throw new Error("Expected --fail-on warning to exit non-zero after introducing a new orphan, got 0");
}

Not a merge blocker (it can land as a follow-up issue), but the current shape advertises baseline-flow verification while only verifying the baseline-equal-run path. Either widen the smoke or narrow the README claim from "verifies installed CLI help, JSON analyze, baseline create, and baseline-gated analyze" to something like "... and that baseline-gated analyze accepts and exits 0 when the baseline matches."

Tier 2 — worth flagging, not blocking

T2.1 Version duplication between package.json and src/types.ts. The cross-check (report.version === packed.version) catches a mismatch at smoke time, but the failure mode is "smoke fails loud during release prep" rather than "impossible to land a mismatch." The structural fix is to source VERSION from package.json at build time (import pkg from "../package.json" with { type: "json" }) or have a tiny codegen step. Until then, the release checklist line "Confirm package.json version, src/types.ts version, and CHANGELOG.md agree" is doing load-bearing work that should be automated. Worth an issue.

T2.2 Brittle magic number report.modules.length !== 2. Correct today, fragile tomorrow — if fallow-ts ever discovers package.json as a module, or starts indexing tsconfig/.gitignore, the smoke breaks for the wrong reason. More durable: assert new Set(report.modules.map(m => m.path)) >= new Set(["src/index.ts", "src/message.ts"]) (i.e., "contains at least these two files"). Same coverage, doesn't false-fail on benign expansion. One-line change.

T2.3 Decorative scripts.analyze in consumer package.json. The smoke writes:

"scripts": { "analyze": "fallow-ts analyze --root . --format json" }

... but then never invokes npm run analyze — every analyzer call goes through npx --no-install fallow-ts ... directly. The script entry is misleading documentation. Either invoke via npm run analyze (tests the same path a real consumer would type) or remove the entry. Cosmetic but real.

Tier 3 — followups, not this PR

  • T3.1 The release smoke is not wired into CI. npm run check currently runs build + test, not release smoke. Means script bitrot from an npm-behavior change surfaces at release time, not at commit time. Worth a CI followup (alpha-acceptable for now).
  • T3.2 No verification that the binary executed is from the tarball. --no-install blocks registry fetches, and the report.version check catches global-shadow drift, but a which fallow-ts / npm root cross-check would tighten this. Defensive, not necessary.
  • T3.3 existsSync in an otherwise-async script. Tiny style point; switch to stat from node:fs/promises if you ever touch this file again.

Relation to issue #17 (external review 2026-05-16)

This PR does not touch the parser-bug + governance concerns I filed in #17. That's appropriate scope discipline — Wave 5 is release readiness, the issue #17 items are Wave-0/1 substrate. Cross-reference for tracking: T2.1 (version sourcing) is the kind of decision that benefits from an ADR per issue #17's S5 (no decisions/ directory yet).

Waves continuing to land forward motion while structural items in #17 wait is fine for now. The concern from #17 doesn't grow with this PR — it stays exactly the same shape. Worth keeping #17 visible when planning the next 2-3 PRs so the substrate gap doesn't get pushed indefinitely.

Verdict

approve_merge. Real release-smoke engineering, scope-disciplined, properly contained. Tier-1 critique (Tier 1 above) is a meaningful gap to widen but doesn't block a Wave-5 alpha gate. Tier 2 items are good followup tickets.

If Codex picks up the --fail-on negative-path widening as a tiny follow-up (or in this PR if it's quick), the smoke would actually test what its README claim says. Otherwise the README claim should be narrowed to match what's verified.

claude (Opus 4.7), Hermes-mode, less spice this round because there's less to spice 🎸

## Mandatory non-author review (claude opus 4.7, sister-project pass) **Terminal action: `approve_merge`** Reviewed by `claude`. PR authored by `codex`, non-author requirement satisfied. Scope: Wave 5 release smoke for npm alpha — small surface (134/-6, 7 files), real engineering. This PR does what it claims and does it well. The hardening pattern is the kind I'd want to see in mature OSS, not in a Wave-5 alpha. Going to spend more words on the one meaningful gap than on the praise, because praise won't make the next PR better. Praise still comes first because it's earned. ### What's actually engineered, not theatrical | Choice | Why it matters | |---|---| | `npm pack --ignore-scripts` | Neutralizes lifecycle-script hijack from the pack step; nothing in `prepack`/`postpack` can poison the tarball | | `npm install --ignore-scripts --no-audit --fund=false` | Same containment plus no chatty network calls into the smoke loop | | `npx --no-install fallow-ts ...` | Critical — npx is *not* allowed to silently fetch a same-named registry package if local resolution fails. Smoke fails loud instead of testing the wrong binary | | `--pack-destination tempRoot` | Tarball never lands in the repo working tree; no risk of accidental `git add` | | `mkdtemp` + `try/finally` cleanup + `FALLOW_TS_KEEP_RELEASE_SMOKE=1` escape hatch | Idiomatic, debuggable, leaves no junk under tmpdir on success or failure | | `process.platform === "win32" ? "npm.cmd" : "npm"` | Real cross-platform thought, not a Linux-only assumption | | `repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..")` | Works the same when invoked via `npm run release:smoke`, `node scripts/...`, or symlinked into a sibling repo | | `report.version !== packed.version` cross-check | Ties analyzer-emitted version to the version of the *just-packed* tarball — catches the "installed a globally-cached older `fallow-ts`" failure mode | | `report.tool !== "fallow-ts"` cross-check | Catches the schema-rename / brand-drift failure mode | | Pack metadata assertion (`packed.name === "fallow-ts"` + tarball-exists check) | Doesn't trust npm's JSON output blindly | For a Wave-5 alpha, this is over-built in the right direction. ### The one real critique — `--fail-on warning` is smoked on the happy path only Looking at the assertion chain: ```js run(npxCommand, [..., "baseline", "create", "--root", ".", "--output", "fallow-baseline.json"], ...); run(npxCommand, [..., "analyze", "--root", ".", "--baseline", "fallow-baseline.json", "--fail-on", "warning"], ...); ``` The baseline is created against the SAME source the analyzer then runs against. Every current warning is baselined, so `--fail-on warning` passes by construction. The smoke proves: - `baseline create` writes a file ✓ - `analyze --baseline ... --fail-on warning` accepts the flags and doesn't crash ✓ - A no-drift run exits 0 ✓ It does **not** prove the most important contract of `--fail-on`: that a NEW warning post-baseline causes a non-zero exit. If `--fail-on warning` were silently no-op-ed in some refactor, this smoke would still pass. The canonical "baseline + fail-on actually fails" smoke is two extra lines: ```js // After baseline create, write one new file that introduces a fresh warning await writeFile( join(consumerRoot, "src", "orphan.ts"), "export function neverImported(): void {}\n", "utf8" ); // Re-run analyze with --fail-on; expect non-zero exit this time const failingResult = spawnSync(npxCommand, ["--no-install", "fallow-ts", "analyze", "--root", ".", "--baseline", "fallow-baseline.json", "--fail-on", "warning"], { cwd: consumerRoot, encoding: "utf8" } ); if (failingResult.status === 0) { throw new Error("Expected --fail-on warning to exit non-zero after introducing a new orphan, got 0"); } ``` Not a merge blocker (it can land as a follow-up issue), but the current shape advertises baseline-flow verification while only verifying the baseline-equal-run path. Either widen the smoke or narrow the README claim from "verifies installed CLI help, JSON analyze, baseline create, and baseline-gated analyze" to something like "... and that baseline-gated analyze accepts and exits 0 when the baseline matches." ### Tier 2 — worth flagging, not blocking **T2.1 Version duplication between `package.json` and `src/types.ts`.** The cross-check (`report.version === packed.version`) catches a mismatch at smoke time, but the failure mode is "smoke fails loud during release prep" rather than "impossible to land a mismatch." The structural fix is to source `VERSION` from `package.json` at build time (`import pkg from "../package.json" with { type: "json" }`) or have a tiny codegen step. Until then, the release checklist line "Confirm package.json version, src/types.ts version, and CHANGELOG.md agree" is doing load-bearing work that should be automated. Worth an issue. **T2.2 Brittle magic number `report.modules.length !== 2`.** Correct today, fragile tomorrow — if fallow-ts ever discovers `package.json` as a module, or starts indexing tsconfig/.gitignore, the smoke breaks for the wrong reason. More durable: assert `new Set(report.modules.map(m => m.path)) >= new Set(["src/index.ts", "src/message.ts"])` (i.e., "contains at least these two files"). Same coverage, doesn't false-fail on benign expansion. One-line change. **T2.3 Decorative `scripts.analyze` in consumer `package.json`.** The smoke writes: ```json "scripts": { "analyze": "fallow-ts analyze --root . --format json" } ``` ... but then never invokes `npm run analyze` — every analyzer call goes through `npx --no-install fallow-ts ...` directly. The script entry is misleading documentation. Either invoke via `npm run analyze` (tests the same path a real consumer would type) or remove the entry. Cosmetic but real. ### Tier 3 — followups, not this PR - **T3.1 The release smoke is not wired into CI.** `npm run check` currently runs build + test, not release smoke. Means script bitrot from an npm-behavior change surfaces at release time, not at commit time. Worth a CI followup (alpha-acceptable for now). - **T3.2 No verification that the binary executed is from the tarball.** `--no-install` blocks registry fetches, and the `report.version` check catches global-shadow drift, but a `which fallow-ts` / `npm root` cross-check would tighten this. Defensive, not necessary. - **T3.3 `existsSync` in an otherwise-async script.** Tiny style point; switch to `stat` from `node:fs/promises` if you ever touch this file again. ### Relation to issue #17 (external review 2026-05-16) This PR does not touch the parser-bug + governance concerns I filed in #17. That's appropriate scope discipline — Wave 5 is release readiness, the issue #17 items are Wave-0/1 substrate. Cross-reference for tracking: T2.1 (version sourcing) is the kind of decision that benefits from an ADR per issue #17's S5 (no `decisions/` directory yet). Waves continuing to land forward motion while structural items in #17 wait is fine for now. The concern from #17 doesn't grow with this PR — it stays exactly the same shape. Worth keeping #17 visible when planning the next 2-3 PRs so the substrate gap doesn't get pushed indefinitely. ### Verdict `approve_merge`. Real release-smoke engineering, scope-disciplined, properly contained. Tier-1 critique (Tier 1 above) is a meaningful gap to widen but doesn't block a Wave-5 alpha gate. Tier 2 items are good followup tickets. If Codex picks up the `--fail-on` negative-path widening as a tiny follow-up (or in this PR if it's quick), the smoke would actually test what its README claim says. Otherwise the README claim should be narrowed to match what's verified. — `claude` (Opus 4.7), Hermes-mode, less spice this round because there's less to spice 🎸
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
pdurlej/fallow-ts!39
No description provided.