fix/semrel-parity-and-hardening #40

Merged
ryangr0 merged 10 commits from fix/semrel-parity-and-hardening into main 2026-08-09 07:12:17 +00:00
Owner
No description provided.
npm sends _authToken unconditionally once configured, so an under-scoped
caller token (a repo-scoped bot PAT without package read access — e.g. the
agent-builder token erfbeeld's dry-run validate gates pass as CI_TOKEN)
turns the PUBLIC @webgrip/semantic-release-config package into npm error
E401, and the fallback toolchain then masks it as the config's
MODULE_NOT_FOUND (erfbeeld runs 1205/1206/1222). The registry allows
anonymous reads, so drop the credential and retry once before declaring
the config unavailable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ploeg run 122 cut v0.2.0-rc.9 correctly and still read like a failure: an npm
EUSAGE wall, two full installs (2m + 21s) for one release, an audit summary
nobody will act on, and 3m45s of setup for ~25s of work. None of that was the
release going wrong — it was semantic-release/action.yml being a generation
behind semantic-release-monorepo/action.yml, which has since learned all of it.
This backports that line and closes the gaps that were latent in BOTH.

Correctness first:

* the prebaked toolchain is now VERIFIED before it is used. SEMREL_PREBAKED was
  taken on faith, and the bake is sr24 while the shared config pins sr25 — a
  release that runs to completion against a config from a different generation.
  The monorepo composite routed around it with a comment; both now check the
  major and, for a shared-config repo, that the prebake actually contains the
  config, then fall back to installing. NODE_PATH is exported alongside PATH,
  without which a bare require() in the consumer's .releaserc.cjs cannot resolve
  a prebaked tree at all — i.e. the fast path could not have worked as written.
* `npm install --no-save` had no --no-package-lock in either composite's
  fallback, so a release rewrote (or, on ploeg, CREATED) package-lock.json in
  the consumer's working tree. For a repo whose @semantic-release/git assets
  include the lockfile, the release commit ships 400 semrel deps.
* the config install now retries anonymously here too (f9d35a8 landed it on the
  monorepo side only), and the toolchain choice is driven by which config the
  repo actually loads rather than by whether an install happened to succeed:
  installing the shared config AND the legacy pins re-introduced exactly the
  skew the pins exist to prevent.
* GITHUB_OUTPUT: $GITHUB_OUTPUT is gone. It is a literal, not an expression;
  the act-based runner re-exports its own value afterwards so the outputs
  survive, but on a runner where step env wins, the exec successCmd writes
  `version=` to a file named `$GITHUB_OUTPUT` and every output is silently
  empty. It bought nothing either way.
* the reusable serializes on the ref (no cancel-in-progress: a release killed
  mid-push leaves a tag with no release) and realigns to the remote branch tip,
  the same "behind remote -> publishes nothing, does not self-heal because the
  winner's commit is [skip ci]" trap already documented for monorepo trains.
  Its job id no longer collides with the caller job ids Forgejo v15 flattens
  into one run, and it finally exposes `tag`, which the composite has had all
  along and which tag-pinned follow-up jobs need.

Then the noise and the clock:

* npm ci / npm run build were unconditional under `|| true`. They now run only
  where they mean something (lockfile present, build script defined) and are
  opt-out/strict via install-dependencies. A Go repo carrying a package.json
  gets silence instead of a 30-line npm error block on a green release.
* one install instead of two, ~/.npm cached, --no-audit --no-fund, node 24 to
  match the monorepo composite. Setup drops from ~3m45s toward ~40s, and to
  ~0 once ADR-0005's image lands.
* every release now writes a step summary naming the version and linking the
  release, so the run page says what happened without opening the log.

rust-semantic-release was two generations back and referenced by nothing, so it
gets the same shape: it would have died on the node-20 engine check b132c59
fixed (it installs unpinned semantic-release = 25.x with no node check at all),
its `version` output read steps.<id>.outputs.nextRelease.version — an output
that does not exist, so it was always empty — and actions-rs/toolchain is
archived and still ships a node16 entrypoint. It also had no --repository-url,
so changelog links pointed at the in-cluster host.

yq is now checksum-verified against the hashes published with v4.44.3.
Unrecorded versions warn rather than fail, so the renovate comment above the
pin cannot turn an automated bump into an org-wide release outage.

ADR-0005 records where this is actually going: a toolchain image built from a
locked package.json, Renovate owning the lockfile, SEMREL_PREBAKED as the
contract — the same pattern rust-releaser already uses. The pinned lists here
are the fallback of last resort, not the destination.

.github/ is untouched: ADR-0002 freezes it byte-for-byte. It carries the same
GITHUB_OUTPUT line, where the consequence is real rather than theoretical.

Verified by driving the release script's six toolchain branches against stubbed
npm/git/node (shared/inline x no-prebake/sr24/sr25/sr25-without-config): one
install per run or none, the direct binary in every case, credentials cleaned
up on both success and failure; and the dependency step across four repo shapes
(no package.json, package.json without lockfile, with lockfile, build script
present or not).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous commit pinned what every release job installs. This deletes the
install: the release jobs now run IN a toolchain image, built and released in
webgrip/infrastructure (ops/docker/semantic-release{,-monorepo,-rust}), each
from a committed package-lock.json.

  semantic-release           node + git + yq + semantic-release 25 +
                             @webgrip/semantic-release-config + plugins
  semantic-release-monorepo  FROM the base, + semantic-release-monorepo
  semantic-release-rust      FROM the base, + cargo + semantic-release-cargo

They are built THERE and not here on purpose. Building them in this repo would
have meant release jobs running in an image whose build is orchestrated by this
repo's own reusable workflows — a circle with no answer to which came first,
where a broken toolchain image cannot be rebuilt by the pipeline that needs it,
and where someone tracing a failed release ends up back where they started.
Images are artifacts; infrastructure builds the estate's artifacts and consumes
this library like every other repo. ADR-0005 records that as a rejected option
with its reason, so the next person to propose it finds the argument.

What stays here is the CONTRACT: SEMREL_PREBAKED — an installed node_modules,
with PATH and NODE_PATH into it. Not a new interface; it is the one the
ci-runner's undocumented /opt/semrel bake already used, except that bake is sr24
against an sr25 config and never exported NODE_PATH, so a consumer's bare
require('@webgrip/semantic-release-config') could not resolve and the fast path
could not have worked as written.

What this removes from every release, permanently: two npm installs and ~430
packages resolved from the network on the critical path, an audit summary nobody
reads, a yq download-and-checksum, a node version check plus setup-node
fallback, and an npmrc holding a release token in a $HOME that outlives the job.
Setup goes from ~3m45s to ~0. The @webgrip scope reads anonymously, so the image
path writes no npm credential anywhere.

The yq business was the clearest symptom: downloading a binary and verifying its
checksum is BUILD work, and it was being done on every release in every repo.

Composites, measured against origin/main: semantic-release 10 steps -> 6,
semantic-release-monorepo 5 -> 2 (one run block plus a summary). What is left is
release semantics only — checkout, branch-tip alignment, baseline seed, the
credential the public host needs, --repository-url, the run, the summary.
rust-semantic-release goes 12 -> 5 and gains lines rather than losing them,
because it never had the release semantics: no baseline seed, no
--repository-url (its changelog links pointed at the in-cluster host), no push
credential, and a `version` output reading an output that does not exist.

The image is the path, not a requirement. A job run outside one still releases:
the composites fall back to an install and say so with a :⚠️: — unlocked
and visible rather than fatal. Consumers needing more than the image has (a
prepareCmd shelling out to helm, php, the docker CLI) build FROM it and pass
toolchain-image, which puts environment needs in an image instead of in ad-hoc
job steps.

Tagging follows infrastructure's per-image train: it publishes
webgrip/<image>:<version> (plus :latest on a final release), so the reusables
pin an exact version in the toolchain-image default with a renovate annotation
watching it — exactly how techdocs-runner pins techdocs-builder. A toolchain
upgrade is two reviewed commits, the lockfile there and the pin here, and never
moves under a repo's feet. renovate.json gains the custom manager that makes
that pin visible: Renovate's workflow manager pins `uses:` refs but is blind to
an image tag handed to a job through an input default, so nothing would have
watched the toolchain a release actually runs in.

MERGE ORDER: the infrastructure PR first, and the base image published before
the two derived ones. The reusables default to these images, so a missing one
fails at the container pull.

Verified: all three images build and were smoke-tested — semantic-release 25.0.8
resolvable, require('@webgrip/semantic-release-config') resolving from an
arbitrary cwd (i.e. NODE_PATH works), yq 4.44.3, git, semantic-release-monorepo
present only in the monorepo image, cargo 1.97.1 + semantic-release-cargo 2.4.2
only in the rust image. Both toolchain branches of both composites driven
against stubbed npm/git/node: image path installs nothing, fallback warns and
installs, and the monorepo path cleans up its fabricated package.json and
removes the credential either way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The base image's train cut semantic-release-v0.1.0, not v1.0.0 — a fresh image
dir in webgrip/infrastructure starts from a seeded 0.0.0, where a feat is a MINOR.
I guessed 1.0.0 while writing these defaults and infrastructure run 166 proved it
wrong: both derived images died in dockerVerifyGate at
"harbor.webgrip.dev/webgrip/semantic-release:1.0.0: not found".

All six references (two workflow input defaults, three composite descriptions,
three fallback warnings) now name :0.1.0. The monorepo and rust images have not
cut their first release yet — their trains failed on the same wrong pin — so
those two defaults are still a prediction and must be checked against the actual
tags before this merges. That check is the merge order this PR already documents.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
semantic-release-v0.1.0's distribute run was killed mid-build on the in-cluster
runner pool, so that tag has a Forgejo release and no image in Harbor. 0.1.1 is
the first base image that actually shipped (sha256:0bb4037a…, signed and
attested by infrastructure run 170), so that is what these defaults name.

The monorepo and rust defaults stay 0.1.0 and remain a PREDICTION: those trains
have never completed a release, so their versions do not exist yet. Verify both
against the real tags before merging this PR — the last time I guessed a version
here it cost two failed runs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The record described three images sharing layers through a FROM chain. That
chain cost three runs in webgrip/infrastructure and has been removed, so the
decision it documents is no longer what shipped.

A FROM on a sibling makes that sibling's PUBLISHED artifact a build dependency,
and a distribute run that dies on a shared runner pool leaves a release with no
image behind it — which is exactly what happened to semantic-release-v0.1.0. The
third of those runs had the correct pin and still failed, which is the part
worth recording: pinning is not a fix for an artifact that does not exist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The guard that hashed it was machinery to protect a comment and has been dropped
in webgrip/infrastructure; the record should not claim an enforcement that does
not exist. The build-time assertions in each image still catch anything that
matters.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Each image is its own directory in webgrip/infrastructure with its own
Dockerfile and lockfile, and the lockfile is folded into the same package.json
that carries the release version. The 'one Dockerfile, three targets' shape
described here was the version that lived in this repo before the build moved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
All three images are now released and in Harbor, so the defaults name real tags
instead of predictions:

  semantic-release:0.1.2            sha256:6c37accd…  (+ :latest)
  semantic-release-monorepo:0.1.0   sha256:96cc4e1a…  (+ :latest)
  semantic-release-rust:0.1.0       sha256:d830a841…  (+ :latest)

The monorepo and rust defaults were already 0.1.0 and turn out to be right. Only
the base moves: 0.1.1 was the last pre-flatten build, 0.1.2 is the one built
from the folded package.json. Same toolchain either way — this points at the
current one rather than a superseded layout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two defects in the image-first composites, both of which only bite outside the container.

The prebake guard tested for a FILE, not a toolchain:

    sr="${SEMREL_PREBAKED:-/nonexistent}/.bin/semantic-release"
    if [[ ! -x "$sr" ]]; then ...install... fi

Every pre-ADR-0005 ci-runner in the pool also exports SEMREL_PREBAKED, pointing at a
hand-pinned tree that is semantic-release 24 with no @webgrip/semantic-release-config and
no NODE_PATH. That file is executable, so the guard would take the fast path and die at
config load with MODULE_NOT_FOUND. ploeg never saw it because ploeg runs in the image;
every repo that does not would have regressed from "installs and works" to "broken".

Probing what the tree actually holds — semantic-release major, the shared config, and the
variant plugin — makes the rollout order-free in both directions. An old runner installs
and says why; a new runner or a container goes straight to the locked binary. It also
gets the rust case right for free: a ci-runner baking the monorepo toolchain has no
semantic-release-cargo, so a rust release on that host correctly falls through.

Verified against all four toolchain shapes: no prebake -> install; ci-runner:1.2.0 (the
sr24 landmine) -> rejected loudly, install; semantic-release:0.1.2 -> fast path; a
ci-runner built from the toolchain image -> fast path.

Second, this branch had dropped the monorepo composite's concurrent-release retry loop.
It is load-bearing: Forgejo IGNORES strategy.max-parallel — infrastructure run 177 ran
three Release jobs that all started at 18:48:57 under max-parallel: 1. Without the loop
the behind-remote symptom exits 0 without publishing, which is how run 141 silently
dropped two releases. Restored, including the log grep, since rc alone cannot see it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ryangr0 force-pushed fix/semrel-parity-and-hardening from 2c5c68c9be to ee06ca519d
Some checks failed
docker-build-and-push-harbor-fast.yml / Merge pull request 'fix/semrel-parity-and-hardening' (#40) from fix/semrel-parity-and-hardening into main (pull_request) Failing after 0s
docker-build-and-push-harbor.yml / Merge pull request 'fix/semrel-parity-and-hardening' (#40) from fix/semrel-parity-and-hardening into main (pull_request) Failing after 0s
docker-build-and-push-registry-fast.yml / Merge pull request 'fix/semrel-parity-and-hardening' (#40) from fix/semrel-parity-and-hardening into main (pull_request) Failing after 0s
docker-build-and-push-registry.yml / Merge pull request 'fix/semrel-parity-and-hardening' (#40) from fix/semrel-parity-and-hardening into main (pull_request) Failing after 0s
docker-build-and-push.yml / Merge pull request 'fix/semrel-parity-and-hardening' (#40) from fix/semrel-parity-and-hardening into main (pull_request) Failing after 0s
forgejo-distribute.yml / Merge pull request 'fix/semrel-parity-and-hardening' (#40) from fix/semrel-parity-and-hardening into main (pull_request) Failing after 0s
github-issue-create-by-prompt.yml / Merge pull request 'fix/semrel-parity-and-hardening' (#40) from fix/semrel-parity-and-hardening into main (pull_request) Failing after 0s
github-issues-create-by-prompt.yml / Merge pull request 'fix/semrel-parity-and-hardening' (#40) from fix/semrel-parity-and-hardening into main (pull_request) Failing after 0s
helm-chart-deploy.yml / Merge pull request 'fix/semrel-parity-and-hardening' (#40) from fix/semrel-parity-and-hardening into main (pull_request) Failing after 0s
helm-chart-push.yml / Merge pull request 'fix/semrel-parity-and-hardening' (#40) from fix/semrel-parity-and-hardening into main (pull_request) Failing after 0s
helm-charts-deploy.yml / Merge pull request 'fix/semrel-parity-and-hardening' (#40) from fix/semrel-parity-and-hardening into main (pull_request) Failing after 0s
helm-charts-push.yml / Merge pull request 'fix/semrel-parity-and-hardening' (#40) from fix/semrel-parity-and-hardening into main (pull_request) Failing after 0s
laravel-quality.yml / Merge pull request 'fix/semrel-parity-and-hardening' (#40) from fix/semrel-parity-and-hardening into main (pull_request) Failing after 0s
php-application-static-analysis.yml / Merge pull request 'fix/semrel-parity-and-hardening' (#40) from fix/semrel-parity-and-hardening into main (pull_request) Failing after 0s
rust-semantic-release.yml / Merge pull request 'fix/semrel-parity-and-hardening' (#40) from fix/semrel-parity-and-hardening into main (pull_request) Failing after 0s
semantic-release-monorepo.yml / Merge pull request 'fix/semrel-parity-and-hardening' (#40) from fix/semrel-parity-and-hardening into main (pull_request) Failing after 0s
semantic-release.yml / Merge pull request 'fix/semrel-parity-and-hardening' (#40) from fix/semrel-parity-and-hardening into main (pull_request) Failing after 0s
setup-repository-bootstrap.yml / Merge pull request 'fix/semrel-parity-and-hardening' (#40) from fix/semrel-parity-and-hardening into main (pull_request) Failing after 0s
setup-repository-copilot-files.yml / Merge pull request 'fix/semrel-parity-and-hardening' (#40) from fix/semrel-parity-and-hardening into main (pull_request) Failing after 0s
setup-repository-create-from-template.yml / Merge pull request 'fix/semrel-parity-and-hardening' (#40) from fix/semrel-parity-and-hardening into main (pull_request) Failing after 0s
spa-preview.yml / Merge pull request 'fix/semrel-parity-and-hardening' (#40) from fix/semrel-parity-and-hardening into main (pull_request) Failing after 0s
static-analysis.yml / Merge pull request 'fix/semrel-parity-and-hardening' (#40) from fix/semrel-parity-and-hardening into main (pull_request) Failing after 0s
sync-template-files.yml / Merge pull request 'fix/semrel-parity-and-hardening' (#40) from fix/semrel-parity-and-hardening into main (pull_request) Failing after 0s
techdocs-deploy-codeberg.yml / Merge pull request 'fix/semrel-parity-and-hardening' (#40) from fix/semrel-parity-and-hardening into main (pull_request) Failing after 0s
wordpress-plugin-release-distribute.yml / Merge pull request 'fix/semrel-parity-and-hardening' (#40) from fix/semrel-parity-and-hardening into main (pull_request) Failing after 0s
wordpress-plugin-release.yml / Merge pull request 'fix/semrel-parity-and-hardening' (#40) from fix/semrel-parity-and-hardening into main (pull_request) Failing after 0s
techdocs-deploy-gh-pages.yml / Merge pull request 'fix/semrel-parity-and-hardening' (#40) from fix/semrel-parity-and-hardening into main (pull_request) Failing after 0s
tests.yml / Merge pull request 'fix/semrel-parity-and-hardening' (#40) from fix/semrel-parity-and-hardening into main (pull_request) Failing after 0s
update_mkdocs.yml / Merge pull request 'fix/semrel-parity-and-hardening' (#40) from fix/semrel-parity-and-hardening into main (pull_request) Failing after 0s
update_techdocs.yml / Merge pull request 'fix/semrel-parity-and-hardening' (#40) from fix/semrel-parity-and-hardening into main (pull_request) Failing after 0s
2026-07-31 04:56:10 +00:00
Compare
webgrip-ci referenced this pull request from a commit 2026-08-09 07:13:26 +00:00
Sign in to join this conversation.
No reviewers
No labels
pull-request
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/workflows!40
No description provided.