Skip to content

Commit 6a59d38

Browse files
docs(repo): add 2 release-recovery durable lessons (v0.6.2→v0.6.3) (#149)
## Summary Docs-only Compound follow-on to the v0.6.2 → v0.6.3 release recovery. Two durable ERPAVal lessons capturing the pre-existing bugs that triggering the release surfaced. - **`vendored-artifact-bump-must-revendor-in-same-pr`** — PR #137 bumped `web-tree-sitter` 0.26.8→0.26.9 without re-running the vendor script. It passed all CI (the `prepublishOnly` guard isn't a CI step) and aborted the dependency-ordered publish at release time. Re-vendor vendored-artifact deps in the same PR; ideally promote the guard into CI so the bump fails red, not the release. - **`npm-trusted-publisher-matches-entry-workflow-not-reusable`** — npm OIDC matches the **entry** workflow (`release-please.yml`), not the reusable one that runs `npm publish` (`release.yml`). Registering the wrong one 404s the token exchange; only manual `workflow_dispatch` runs ever published, so npm lagged the git tags for a full release cycle. Config is web-UI-only, passkey-gated, one entry per package (17 here). ## Test plan - [x] commitlint `docs(repo):` accepted - [x] No code changes
1 parent cfb3be6 commit 6a59d38

3 files changed

Lines changed: 128 additions & 0 deletions

File tree

.erpaval/INDEX.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ development sessions. Solutions are reusable; specs are per-feature.
5252

5353
- [Collapse parallel switches into a Record registry](solutions/architecture-patterns/collapse-parallel-switches-into-record-registry.md) — when 2+ functions each switch over the same closed union (one per derived attribute), fold them into `Record<Union, Entry>`. tsc preserves exhaustiveness, the functions become one-line lookups, honest `| null` replaces placeholder lies, and ONE table-driven test with a `Record<Union, Expected>` fixture pins every (key, attribute) pair — better coverage than the zero direct tests the switches had.
5454

55+
- [A vendored-artifact dep bump must re-vendor in the same PR](solutions/conventions/vendored-artifact-bump-must-revendor-in-same-pr.md) — bumping a dep that has a committed vendored artifact (`web-tree-sitter``vendor/wasms/*.wasm` + manifest) without re-running the vendor script passes ALL CI (the `prepublishOnly` guard isn't a CI step) and detonates at `npm publish`, aborting the dependency-ordered multi-package release mid-stream. Scan Dependabot consolidations for vendored-artifact deps and re-vendor before merge.
56+
57+
- [npm trusted publisher matches the ENTRY workflow, not the reusable one](solutions/conventions/npm-trusted-publisher-matches-entry-workflow-not-reusable.md) — npm OIDC matches "Workflow filename" against the workflow that INITIATED the run, not the one running `npm publish`. With `release-please.yml``workflow_call``release.yml`, register `release-please.yml`. Wrong registration silently 404s the token exchange; only `workflow_dispatch` runs (entry = release.yml) ever publish, so npm lags the tags. Config is web-UI-only, passkey-gated, one entry per package (17 here).
58+
5559
## Specs
5660

5761
- [001-scip-replaces-lsp](specs/001-scip-replaces-lsp/spec.md) — rip-and-replace LSP with SCIP for TS/Py/Go/Rust/Java. Task map: [tasks.md](specs/001-scip-replaces-lsp/tasks.md).
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
---
2+
name: npm-trusted-publisher-matches-entry-workflow-not-reusable
3+
description: npm OIDC trusted publishing matches the "Workflow filename" against the ENTRY workflow that initiated the run, NOT the reusable workflow where `npm publish` actually executes. If release.yml is invoked via `workflow_call` from release-please.yml, you must register release-please.yml as the trusted publisher — registering release.yml silently 404s the OIDC token exchange and falls back to an unauthenticated (failing) publish.
4+
metadata:
5+
type: convention
6+
category: conventions
7+
tags: [release, npm, oidc, trusted-publishing, github-actions, workflow-call, provenance]
8+
discovered: 2026-05-28
9+
session: session-88b46e
10+
related:
11+
- release-published-event-needs-pat-or-inline
12+
- vendored-artifact-bump-must-revendor-in-same-pr
13+
---
14+
15+
# npm trusted publisher matches the ENTRY workflow, not the reusable one
16+
17+
## The symptom
18+
19+
npm publish via OIDC trusted publishing fails with:
20+
21+
```
22+
WARN Skipped OIDC: ERR_PNPM_AUTH_TOKEN_EXCHANGE: Failed token exchange request ... (status code 404)
23+
📦 @opencodehub/scanners@0.2.0 → https://registry.npmjs.org/
24+
ERR E404 404 Not Found - PUT https://registry.npmjs.org/@opencodehub%2fscanners - Not found
25+
```
26+
27+
The 404 on the **token exchange** is the tell: the OIDC claim npm received didn't match any configured trusted publisher, so pnpm got no token, fell back to an unauthenticated publish, and the `PUT` 404'd.
28+
29+
## Root cause
30+
31+
npm matches the trusted-publisher "Workflow filename" against the **workflow that initiated the run** (`workflow_ref` / the entry workflow), NOT the workflow that contains the `npm publish` command. npm's own docs call this out: *"Some workflows use `workflow_call` to invoke other workflows that run `npm publish`... validation checks the calling workflow's name instead of the workflow that actually contains the publish command."*
32+
33+
This repo's release flow is:
34+
35+
```
36+
push:main → release-please.yml (ENTRY workflow — what npm sees in the OIDC claim)
37+
└─ uses: ./.github/workflows/release.yml (workflow_call → reusable)
38+
└─ npm-publish job runs `pnpm -r publish --provenance`
39+
```
40+
41+
So the OIDC claim carries `release-please.yml`. The trusted publisher was registered for `release.yml` (where publish *runs*) → no match → 404.
42+
43+
## Why it was invisible
44+
45+
The ONLY release.yml runs that ever published successfully were `workflow_dispatch` (manual) — because a manual dispatch makes `release.yml` itself the entry workflow, which matched the registration. Every automated `push → release-please → workflow_call` run silently failed to publish. Result: npm sat on `@opencodehub/cli@0.4.0` while the repo had long since tagged 0.5.x. Check this whenever "the tags exist but npm is behind."
46+
47+
## The fix
48+
49+
Register the **entry** workflow as the trusted publisher filename:
50+
51+
```
52+
npm package → Settings → Trusted Publisher → Workflow filename:
53+
release.yml ✗ (where publish runs)
54+
release-please.yml ✓ (what triggers the run)
55+
```
56+
57+
Also required (npm enforces it): `id-token: write` on BOTH the parent job (the `release` job in release-please.yml that does `uses: ./.github/workflows/release.yml`) AND the child (the npm-publish job in release.yml). This repo already had both.
58+
59+
## Operational pain to plan for
60+
61+
- Trusted-publisher config is **web-UI only**, no API/CLI, and **passkey/2FA-gated per save**. With N packages it's N manual saves. This monorepo has **17 publishable packages** (`packages/*` minus `docs`, which is `private: true`), so it's 17 saves. Do them back-to-back to ride the authenticator's warm-credential window.
62+
- **Each package has exactly one trusted publisher** — no org-level or account-level setting applies to all at once. Changing the filename means re-saving all 17.
63+
- Tradeoff: registering `release-please.yml` means a manual `workflow_dispatch` of `release.yml` (entry = release.yml) will STOP matching. The automated flow is the one that matters, so that's the right trade; treat manual dispatch as admin-only.
64+
65+
## Why not just add a PAT instead?
66+
67+
A `RELEASE_PLEASE_PAT` would let release-please cut a release that fires `release: published`, making `release.yml` the entry workflow (matching the old registration). But that reintroduces a long-lived token this repo deliberately removed (see [[release-published-event-needs-pat-or-inline]] — the `workflow_call` design exists precisely to avoid the PAT). For an OIDC-only, Sigstore + SLSA-L3 repo, fixing the npm registration is the hardening-preserving choice; the PAT is a regression.
68+
69+
## Linked
70+
71+
- [[release-published-event-needs-pat-or-inline]] — the sibling decision: why the flow uses `workflow_call` (no PAT) in the first place. THIS lesson is the npm-side consequence of that choice.
72+
- npm docs: https://docs.npmjs.com/trusted-publishers
73+
- Session 2026-05-28: diagnosed during the v0.6.2 → v0.6.3 release recovery.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
name: vendored-artifact-bump-must-revendor-in-same-pr
3+
description: A Dependabot/manual bump of a dependency that has a committed vendored artifact (a copied .wasm, a generated lockfile-derived blob, a snapshotted schema) must re-run the vendoring script IN THE SAME PR. The prepublishOnly guard that checks artifact-vs-pin drift does NOT run in CI — it only fires at `npm publish`, so the bump passes every gate and detonates at release time, aborting a dependency-ordered multi-package publish mid-stream.
4+
metadata:
5+
type: convention
6+
category: conventions
7+
tags: [release, dependabot, vendored-wasm, prepublish, web-tree-sitter, monorepo-publish]
8+
discovered: 2026-05-28
9+
session: session-88b46e
10+
related:
11+
- doctor-probe-drift-after-rip-and-replace
12+
- release-published-event-needs-pat-or-inline
13+
---
14+
15+
# A vendored-artifact dependency bump must re-vendor in the same PR
16+
17+
## What bit us
18+
19+
PR #137 (consolidated Dependabot bumps) moved `web-tree-sitter` 0.26.8 → 0.26.9 in `packages/ingestion/package.json`. It did NOT re-run `scripts/build-vendor-wasms.sh`, so:
20+
21+
- `packages/ingestion/vendor/wasms/web-tree-sitter.wasm` stayed the 0.26.8 blob (sha `082795b…`, not the 0.26.9 `1ed02fe…`)
22+
- `vendor/wasms/manifest.json` still recorded `"web-tree-sitter": "0.26.8"`
23+
24+
PR #137 passed **every CI gate** — lint, typecheck, all 1959 tests, banned-strings, CodeQL — and merged clean. The drift only surfaced 4 PRs later at `npm publish` time, where ingestion's `prepublishOnly` guard (`verify-vendor-wasms.mjs`) compares the manifest version string against the package.json pin and `process.exit(1)` on mismatch.
25+
26+
Because `pnpm -r publish` runs in **dependency order** and ingestion is upstream of cli/mcp/pack, that one guard failure **aborted the entire publish mid-stream** — the v0.6.2 release published 11 leaf packages then died, leaving cli/ingestion/mcp/pack/cobol-proleap unpublished. Recovery took a fix PR (#147) + a second release (0.6.3).
27+
28+
## Why CI doesn't catch it
29+
30+
The guard runs as `prepublishOnly`, NOT as a test or a CI step. By design: it's a release-time integrity check, and re-vendoring needs a toolchain (docker/emcc for build-from-source grammars) that CI deliberately doesn't provision. So the guard is invisible until `npm publish` — exactly the same failure-shape as [[doctor-probe-drift-after-rip-and-replace]] (a check that only fires in an environment CI doesn't exercise).
31+
32+
## The rule
33+
34+
When bumping a dependency that has a committed vendored artifact, the SAME PR must regenerate the artifact:
35+
36+
1. Identify vendored-artifact deps before merging a bump. In this repo: `web-tree-sitter` (and the `tree-sitter-*` grammars) → `packages/ingestion/vendor/wasms/`. Any dep whose version is mirrored into a committed blob + a manifest is in this class.
37+
2. After the bump, run the vendor script (`bash scripts/build-vendor-wasms.sh`) — or, if only the runtime blob changed and the build-from-source grammars are unmoved, do the targeted copy the script's tail does: `cp node_modules/.pnpm/<dep>@<ver>/node_modules/<dep>/<artifact>.wasm vendor/wasms/` + bump the manifest string.
38+
3. Run the guard directly as the gate: `node packages/ingestion/scripts/verify-vendor-wasms.mjs` must exit 0.
39+
4. Commit the regenerated artifact + manifest alongside the package.json bump.
40+
41+
### Make Dependabot bumps of these deps fail loud in CI
42+
The real fix is to stop relying on the publish-time guard. Add the guard (or a content-hash check) to the normal CI test job for `@opencodehub/ingestion`, so a stale vendored artifact turns a PR red instead of a release. A bump that touches a vendored-artifact dep should never be green in CI while the artifact is stale. Until that's wired, treat every `web-tree-sitter` / `tree-sitter-*` Dependabot PR as "must re-vendor before merge" and check the manifest in review.
43+
44+
## How to apply during a Dependabot consolidation
45+
46+
When squashing Dependabot bumps (the recurring task in this repo — see the history-rewrite + consolidation playbook), scan the bump list for vendored-artifact deps FIRST. For each one, re-vendor in the consolidation branch before running `pnpm run check`. The 0.26.8→0.26.9 bump shipped in a consolidation PR precisely because the consolidation step didn't have this check.
47+
48+
## Linked
49+
50+
- [[doctor-probe-drift-after-rip-and-replace]] — same family: a check that only runs outside CI drifts silently.
51+
- PR #137 (the bump that drifted), #147 (the re-vendor fix), v0.6.3 (the recovery release).

0 commit comments

Comments
 (0)