LearnNewsExamplesServices
Frontmatter
id19222
titlePre-release packs read a stale .npmignore and drop the Workstation
stateClosed
labels
bugaibuild
assigneesneo-opus-ada
createdAtSep 25, 2026, 4:01 PM
updatedAtSep 25, 2026, 5:47 PM
githubUrlhttps://github.com/neomjs/neo/issues/19222
authorneo-opus-ada
commentsCount0
parentIssue19047
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 25, 2026, 5:24 PM

Pre-release packs read a stale .npmignore and drop the Workstation

neo-opus-ada
neo-opus-ada commented on Sep 25, 2026, 4:01 PM

Reshaped, 2026-09-25 15:15Z. The Fix and AC-2 changed; the first shape is kept below, struck. The operator challenged committing a region generated from .gitignore, and @neo-fable-clio's review amendment on #19223 proposed composing it at pack time. .npmignore is now composed by prepack and restored by postpack, so the committed file is its header alone. A git-dependency install runs no prepack (probe on npm 11.12.1). It packs the same files from the header anyway, because .gitignore matches no tracked file.

Correction, 2026-09-25 14:30Z. The premise changed; the original claim is kept below, struck. The 13.2 release would ship the Workstation. buildScripts/release/prepare.mjs rebuilds .npmignore's copy region from .gitignore, and .gitignore already carries the Workstation's three re-includes. Composed from dev, the release file adds 16 rules the committed copy lacks. The drop is real for every pack before the release step, because they read the committed file: CI's check-package-contents, an engine pinned by git commit (the Institution pins one), and #19047's dry run. That is the defect this ticket now covers.

Deployed from the published package, every one of those links would 404 on neomjs.com.

Context

#19047's AC-4 dry run, 2026-09-25: a scratch clone of neomjs/pages (62cb7a2f) with neo.mjs pinned to github:neomjs/neo#f9a776c50c, built by node buildScripts/updateNeoVersion.mjs --force --engine-ref=f9a776c50c… against the corpus pin 3894176e. The build exits 0 and was not pushed. Served and loaded headless, the portal, a learn route and the dashboard dock demos all mount with no console errors. The Workstation returns 404 in every mode (/apps/…, /dist/{development,production,esm}/…), because the installed package's apps/workstation/ holds only README.md. A git dependency installs through npm pack, which reads the committed .npmignore.

The Problem

.npmignore is an authoritative header plus a copy of .gitignore below # Original content of the .gitignore file. The release step regenerates that copy, but between releases it goes stale, and every pack reads the stale one:

  • The package gate judges a different tarball. check-package-contents runs npm pack --dry-run on the committed file, so its forbidden and required checks certify a file set the release will not publish. findRulesLostOnRelease covers one direction (a header rule a release would delete) and deliberately treats what the copy adds as the sync working.
  • Git-pinned consumers get a different package. On dev today that is the Workstation missing, among 16 drifted rules.
  • A dry run cannot reproduce the release through the ordinary install path.

The Architectural Reality

  • buildScripts/util/npmIgnoreComposition.mjs#composeNpmIgnore is the single composition. The release step ran it; with this fix, prepack does.
  • Drift measured on dev (composeNpmIgnore(dev .npmignore, dev .gitignore) against the committed rules): +16 −1. The additions are the Workstation's three re-includes, resources/data/reports/, .neo-ai-secrets, .neo-ai-data-parity, /opencode.jsonc, /.kimi-code/{mcp.json,config.toml}, .agents/skills, .claude/skills/, .claude/hooks/* (plus its guard re-include), .codex/hooks/, and .kimi-code/hooks/* (plus its README). The removal is resources/content/sandman_handoff.md, which the header's resources/content/ subsumes.
  • The copy only excludes untracked files. git ls-files -ci --exclude-from=.gitignore is empty on dev, and a clean export packs 5,787 files with the copy and 5,787 without it. A working tree differs: there the header alone also ships gitignored local state.
  • npm runs prepack on npm pack and npm publish, never for a git dependency, which runs prepare. The publish job (npm-publish.yml) runs npm ci then npm publish, with scripts enabled.

The Fix

  1. The committed .npmignore is its header, ending at the marker.
  2. prepack composes the file in place (node ./buildScripts/util/npmIgnoreComposition.mjs compose), and postpack writes the header back (restore). findReleaseDrift and the release step's composition go. A pack that passes --ignore-scripts runs neither hook, so check-consumer-runtime-build composes around its pack (withComposedNpmIgnore).
  3. checkPackageContents.spec.mjs arms: the committed file is its header alone; compose then restore is lossless; package.json wires both hooks; a fixture npm pack honours the composition, with and without the lifecycle; .gitignore matches no tracked file.

1. Commit the composition, so the committed file is what the release writes. 2. check-package-contents fails when they differ (findReleaseDrift: rules compared in order, because ignore files resolve last-match-wins) and prints the one-line sync command. 3. checkPackageContents.spec.mjs: arms for a missing rule, for order alone, for the in-sync case, and one reading the real files.

Acceptance Criteria

  • AC-1: npm pack --dry-run at the head lists all 28 tracked apps/workstation/ files, with no forbidden entry and no missing required entry.
  • AC-2: every npm pack and npm publish reads .npmignore composed from the committed header and the current .gitignore, and the committed file carries nothing below the marker. Spec arms pin both, plus the condition that lets a git-pinned install (no prepack) pack the same files: .gitignore matches no tracked file.
  • AC-3: #19047's pages dry run, repeated against a dev commit that carries this, serves the four Workstation URLs with a mounted viewport. This is post-merge and recorded on #19047. Met 2026-09-25 15:45Z at 2bad700dc0 (receipt).

AC-2: check-package-contents fails when the committed .npmignore differs from its release composition. The arm reading the real files is red on dev (+16 −1) and green at the head.

Out of Scope

  • The rest of #19047's AC-4 (the full dry run and its receipts).
  • The portal home's links to unrendered learn pages (#19219).

Related

#19047 (parent) · #19219 · #19052 (the prepare / postinstall split the pages install depends on)

Live latest-open sweep: checked the latest 20 open issues at 2026-09-25T14:00:51Z, plus a search for existing Workstation or package-content reports; no equivalent. A2A in-flight sweep: no claim on packaging or the Workstation's shipping in today's messages. MC sweep: "published npm package omits an app because npmignore allowlist…", 5 results, no prior decision. Own-assignment sweep: #19047 is the parent; nothing else overlaps.

Origin Session ID: 0f80515e-7682-4313-8101-b926da48c55c

tobiu referenced in commit 2bad700 - "fix(build): prepack composes .npmignore, so every pack reads what a publish does (#19222) (#19223) on Sep 25, 2026, 5:24 PM
tobiu closed this issue on Sep 25, 2026, 5:24 PM