fix(release): drive the composite install from a manifest so overrides apply #133

Closed
ryangr0 wants to merge 1 commit from fix/composite-manifest-install into main
Owner

Unblocks every image release, and reverts my own bad fix.

What was wrong

npm install --no-save --no-package-lock has no manifest, so an overrides block cannot apply to it. That diagnosis was right. The fix I pushed for it (e57b6f2, adding conventional-changelog-conventionalcommits@^9.3.1 as an install argument) was wrong in two independent ways.

It could not work. 15.0.0-beta.2 does not satisfy semantic-release's ^14, so npm must nest, and the nested copy is the one semantic-release loads:

npm i --no-save --no-package-lock semantic-release rng@15.0.0-beta.2
  -> root                              15.0.0-beta.2
  -> semantic-release/node_modules/    14.1.1        <- what actually gets loaded

semantic-release/lib/plugins/utils.js resolves resolveFrom.silent(__dirname, name) || resolveFrom(cwd, name) — its own directory wins. Only overrides rewrites that dependency edge.

It was actively unsafe. The generator and preset are a pair and both mismatches throw. Verified by rendering real commits through generateNotes():

generator preset result
14.1.1 10.4.0 throws — requires conventional-changelog-writer@9 or newer
14.1.1 9.3.1 renders
15.0.0-beta.2 10.4.0 renders — the pair this repo ships
15.0.0-beta.2 9.3.1 throws — headerPartial is not a function

^9.3.1 next to the config's generator 15 is that last row.

The fix

Install into a scratch prefix against a real manifest, then link the tree back into the package dir. Same install, driven by a manifest with overrides, collapses to one copy of each:

release-notes-generator          15.0.0-beta.2   (one copy, none nested)
conventional-changelog-...       10.2.1
commit-analyzer                  14.0.0-beta.3

The manifest is derived from ops/docker/semantic-release-monorepo/package.json — the image that already ships this exact toolchain, and that Renovate's semantic-release toolchain rule already keeps current — with only @webgrip/semantic-release-config overlaid from the action input. One source of truth, so the composite cannot drift from the image. The got/tar/globby overrides ride along, which the old form also silently dropped.

The image's own package.json is never touched; it stays the release manifest @semantic-release/git commits.

Guards

The failure this replaces was silent for months, so the install now asserts:

  • the resolved pair is one of the two that render;
  • no nested release-notes-generator survived the overrides.

Verification

Locally, against the real derived manifest (private config package dropped, everything else identical): single copies, guard passes, nothing nested, and generateNotes() renders a populated changelog. The live check is the first image release off this branch — notes must be non-empty.

This preserves the toolchain decision from webgrip/semantic-release-config#10 rather than reversing it.

Unblocks every image release, and reverts my own bad fix. ## What was wrong `npm install --no-save --no-package-lock` has no manifest, so an `overrides` block cannot apply to it. That diagnosis was right. The fix I pushed for it (e57b6f2, adding `conventional-changelog-conventionalcommits@^9.3.1` as an install argument) was wrong in two independent ways. **It could not work.** `15.0.0-beta.2` does not satisfy semantic-release's `^14`, so npm *must* nest, and the nested copy is the one semantic-release loads: ``` npm i --no-save --no-package-lock semantic-release rng@15.0.0-beta.2 -> root 15.0.0-beta.2 -> semantic-release/node_modules/ 14.1.1 <- what actually gets loaded ``` `semantic-release/lib/plugins/utils.js` resolves `resolveFrom.silent(__dirname, name) || resolveFrom(cwd, name)` — its own directory wins. Only `overrides` rewrites that dependency edge. **It was actively unsafe.** The generator and preset are a pair and both mismatches throw. Verified by rendering real commits through `generateNotes()`: | generator | preset | result | |---|---|---| | 14.1.1 | 10.4.0 | throws — `requires conventional-changelog-writer@9 or newer` | | 14.1.1 | 9.3.1 | renders | | **15.0.0-beta.2** | **10.4.0** | **renders — the pair this repo ships** | | 15.0.0-beta.2 | 9.3.1 | throws — `headerPartial is not a function` | `^9.3.1` next to the config's generator 15 is that last row. ## The fix Install into a scratch prefix against a real manifest, then link the tree back into the package dir. Same install, driven by a manifest with `overrides`, collapses to one copy of each: ``` release-notes-generator 15.0.0-beta.2 (one copy, none nested) conventional-changelog-... 10.2.1 commit-analyzer 14.0.0-beta.3 ``` The manifest is **derived** from `ops/docker/semantic-release-monorepo/package.json` — the image that already ships this exact toolchain, and that Renovate's `semantic-release toolchain` rule already keeps current — with only `@webgrip/semantic-release-config` overlaid from the action input. One source of truth, so the composite cannot drift from the image. The `got`/`tar`/`globby` overrides ride along, which the old form also silently dropped. The image's own `package.json` is never touched; it stays the release manifest `@semantic-release/git` commits. ## Guards The failure this replaces was silent for months, so the install now asserts: - the resolved pair is one of the two that render; - no nested `release-notes-generator` survived the overrides. ## Verification Locally, against the real derived manifest (private config package dropped, everything else identical): single copies, guard passes, nothing nested, and `generateNotes()` renders a populated changelog. The live check is the first image release off this branch — notes must be non-empty. This preserves the toolchain decision from webgrip/semantic-release-config#10 rather than reversing it.
fix(release): drive the composite install from a manifest so overrides apply
All checks were successful
[Workflow] On Source Change / Plan: guards + changed images + release matrix (push) Successful in 3s
a92a99753c
Image releases have been blocked by the config's notes-toolchain guard. The
diagnosis that `overrides` cannot apply to `npm install --no-save
--no-package-lock` was right; the fix I first pushed (e57b6f2, pinning
conventionalcommits@^9.3.1 as an install argument) was wrong, and is reverted
here.

Measured, not reasoned:

  npm i --no-save --no-package-lock semantic-release rng@15.0.0-beta.2
    -> root 15.0.0-beta.2, semantic-release/node_modules/ 14.1.1
  the same install driven by a manifest carrying `overrides`
    -> one copy, 15.0.0-beta.2

An explicit install argument cannot fix this: 15.0.0-beta.2 does not satisfy
semantic-release's ^14, so npm MUST nest, and the nested copy is the one
semantic-release loads (lib/plugins/utils.js prefers resolveFrom(__dirname)
over resolveFrom(cwd)). Only an `overrides` block rewrites that edge, and
npm reads overrides only from the manifest at the install root — which fifteen
of eighteen ops/docker/<image>/package.json files do not have a dependency
block for.

So the install now happens in a scratch prefix against a real manifest, and
the tree is linked back into the package dir. The manifest is DERIVED from
ops/docker/semantic-release-monorepo/package.json rather than hand-written,
so the composite cannot drift from the image that ships the same toolchain,
and Renovate's existing "semantic-release toolchain" rule keeps both current.

e57b6f2 was also actively unsafe. The generator and preset are a pair, and
both mismatches throw — verified by rendering real commits:

  14.1.1        + preset 10.4.0 -> throws "requires conventional-changelog-writer@9"
  14.1.1        + preset  9.3.1 -> renders
  15.0.0-beta.2 + preset 10.4.0 -> renders   <- the pair this repo ships
  15.0.0-beta.2 + preset  9.3.1 -> throws "headerPartial is not a function"

^9.3.1 next to the config's generator 15 is the bottom row.

Two post-install assertions are added, because the failure this replaced was
silent for months: the resolved pair must be one of the two that render, and
no nested generator may survive the overrides.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ryangr0 closed this pull request 2026-08-28 20:00:12 +00:00
All checks were successful
[Workflow] On Source Change / Plan: guards + changed images + release matrix (push) Successful in 3s

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
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
webgrip/infrastructure!133
No description provided.