Skip to content

fix(release): stop shipping devDependencies on release tags - #501

Open
imnavanath wants to merge 7 commits into
stagingfrom
fix/dist-manifest-drop-devdependencies
Open

imnavanath wants to merge 7 commits into
stagingfrom
fix/dist-manifest-drop-devdependencies

Conversation

@imnavanath

Copy link
Copy Markdown
Contributor

The problem

Consumers install force-ui as a GitHub dependency:

"@bsf/force-ui": "github:brainstormforce/force-ui#1.8.1"

npm's git fetcher runs a nested npm install --force --include=dev --include=peer --include=optional
inside any git dependency whose manifest declares an install script (pacote/lib/git.js),
and our postinstall (the Lexical patches) makes force-ui exactly that. The --force and the
--include=dev are hardcoded in pacote — a consumer cannot opt out of them.

bin/release.sh copies package.json onto the release branch verbatim, and the release branch ships
no lockfile. So that nested install re-resolves all ~76 of our dependencies — including 55
devDependencies a consumer has no use for — live from the registry, on every consumer CI run.

That is the actual defect: our dev toolchain is on every consumer's install path, unpinned. Any
publish by any third party into that tree can turn every consumer red simultaneously, with no change
on their side. On 2026-09-03 one did, and npm's arborist started crashing with
Cannot read properties of null (reading 'edgesOut') (npm/cli#9787). SureRank's CI was
red on every branch for five days.

What this PR changes

bin/release.sh now trims the manifest it writes onto the release branch, between the rsync and the
commit:

  • devDependencies dropped, and scripts reduced to postinstall alone. A consumer never runs
    our build, storybook, lint or test scripts, and every one of them references a dependency that is
    now gone.
  • The remaining 20 runtime dependencies are resolved once, here, and the lockfile ships with the
    tag. A package published between releases can no longer change what consumers resolve. files
    keeps that lockfile out of the packed tarball, so it only ever affects the git-dependency path.
  • Two guards refuse to write a manifest that has lost scripts.postinstall or the
    patch-package dependency, because those two are what apply the Lexical patches in a consumer's
    node_modules.

Relationship to #500

They fix different things and both are worth having.

#500 removes the current trigger by pinning the storybook family and moving vite to ^6, so
today's specific peer conflict cannot form. This PR removes the class: once devDependencies are not
shipped and the runtime tree is locked, no publish anywhere in our dev toolchain can reach a consumer
at all — including the next one we have not predicted.

I have deliberately not touched package.json here, so this does not conflict with #500. Merge order
does not matter.

Verification

Built two local git repos — one with the real 1.8.1 release tree, one with this patch's output — and
installed each into a consumer project over git+file://:

Scenario npm 9.5.0 (Node 18.15) npm 10.9.8 (Node 22)
current release tree ❌ edgesOut crash ❌ edgesOut crash
this patch's output ✅ 221 packages ✅ 220 packages, 5s

The Lexical patches still apply in the consumer's tree — node_modules/lexical/Lexical.dev.js reads
let subTreeTextStyle = null; (upstream ships = ''), i.e. all 6 patches landed.

Also checked: bash -n passes; the trim is idempotent (re-running is byte-identical, so no
diff churn between releases); all three failure modes — missing postinstall, missing
patch-package, malformed JSON — exit 1 and leave the manifest untrimmed rather than shipping a
broken one.

Two things reviewers should not "simplify"

  • The explicit || { ...; exit 1; } handlers. tag-release.yml invokes this as
    bash bin/release.sh, which ignores the -e in the shebang. Without them, a failed trim is
    committed and tagged silently. A blanket set -e is not the answer either: the TAG_EXISTS grep
    on line 25 exits 1 on every genuine release, so it would abort every release.
  • --ignore-scripts on the lockfile step. Without it, --package-lock-only runs force-ui's own
    postinstall inside BUILD_DIR.

Scope

This affects future tags only. Existing tags (1.7.5 → 1.8.1) keep the fat manifest, so consumers
pinned to them still need a workaround until they move to a re-cut tag. SureRank has already switched
to the release tarball URL, which bypasses the git fetcher entirely.

Not included, happy to add if wanted: .distignore still ships gulpfile.js, tsconfig*.json,
vite.config.ts and vitest.config.ts to consumers; and publish-public-build.yml duplicates this
trim logic for the mirror repo, so one shared generator would stop the two drifting.

🤖 Generated with Claude Code

hello-bsf and others added 7 commits June 11, 2026 17:24
Add GitHub Actions workflow for automated deployment to production server.

Features:
- Automated deployment on push to master
- Manual deployment trigger via workflow_dispatch
- Builds library and Storybook on GitHub runner
- Rsync deployment over SSH
- Automatic PM2 process restart
- Secure SSH key handling with cleanup

Configuration required:
- PRODUCTION_SSH_KEY: SSH private key
- PRODUCTION_HOST: Server IP address
- PRODUCTION_USER: SSH username
- PRODUCTION_PATH: Deployment directory path

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Adding CICD worflow to forceUI
Consumers install force-ui as a GitHub dependency, e.g.
"@bsf/force-ui": "github:#1.8.1". npm's git fetcher
runs a nested `npm install --force --include=dev --include=peer
--include=optional` inside any git dependency whose manifest declares an
install script (pacote/lib/git.js), and our postinstall (the Lexical patches)
makes force-ui exactly that. bin/release.sh copies package.json onto the
release branch verbatim, and the release branch carries no lockfile - so that
nested install re-resolved all ~76 of our dependencies, including 55
devDependencies a consumer has no use for, live from the registry on every
consumer CI run.

On 2026-09-03 the vitest 5.0.0 family published, and @chromatic-com/vitest
1.0.1 widened its peer range to "^4 || ^5". That let vitest 5 into a tree
whose devDeps pin @vitest/* at exact ^4.1.1, and npm's arborist began
crashing on the resulting peer set with "Cannot read properties of null
(reading 'edgesOut')" (npm/cli#9787). Every consumer's CI went red at once,
on every branch, with no change on their side. SureRank was red for five
days.

This trims the manifest written onto the release branch to what a consumer
actually needs: devDependencies dropped, scripts reduced to postinstall. It
then resolves the remaining 20 runtime dependencies once, here, and ships the
lockfile, so a package published between releases cannot change what
consumers resolve. "files" keeps that lockfile out of the packed tarball, so
it only affects the git-dependency path.

Two guards refuse to write a manifest that has lost scripts.postinstall or
the patch-package dependency, since those are what apply the Lexical patches
in a consumer's node_modules. Failures exit non-zero explicitly because
tag-release.yml invokes this as `bash bin/release.sh`, which ignores the
shebang's -e; a blanket `set -e` is not an option here because the
TAG_EXISTS grep on line 25 exits 1 on every genuine release.

Verified on node 18.15.0 / npm 9.5.0 and node 22 / npm 10.9.8, both of which
crash today: installing the trimmed tree over git+file:// succeeds (221
packages) where the current tree fails, and the Lexical patches still apply -
node_modules/lexical/Lexical.dev.js reads "let subTreeTextStyle = null;".

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants