Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .erpaval/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ development sessions. Solutions are reusable; specs are per-feature.
- [Add typed kind-filtered enumeration to IGraphStore once 3+ packages need it](solutions/architecture-patterns/storage-list-nodes-over-scattered-sql.md) — `listNodes()` collapses N raw-SQL call sites into one typed rehydration; cross-adapter parity test catches schema drift.
- [Lift pure helpers to the deepest shared workspace dependency to break future cycles](solutions/architecture-patterns/lift-pure-functions-to-shared-dep-to-break-cycles.md) — `mcp → pack → mcp` was averted by lifting `classifyDependencies` into `@opencodehub/analysis` (the LCA dep). 30-LOC mechanical chore commit.
- [Worktree isolation — pin pwd at task start and exclude worktrees from biome v2](solutions/best-practices/worktree-isolation-pwd-pin-and-biome-exclusion.md) — gitignore is not enough for biome v2; scope to `packages/` or add `experimentalScannerIgnores`. Always `pwd && git rev-parse --show-toplevel` at task start.
- [pnpm install hangs on an Amazon EFS-mounted workdir](solutions/best-practices/pnpm-install-on-efs.md) — EFS home puts the pnpm CAS store on NFS (~22 ms/lookup, 100× the local cost) and the AL2023 `io_uring` cleanup bug makes Node look hung. Fix in user-global `~/.npmrc`: pin `store-dir` to local disk + set `UV_USE_IO_URING=0`.
- [Use finch as a drop-in docker via a PATH shim on AL2023 devboxes](solutions/best-practices/finch-as-docker-shim.md) — tools that shell out to `docker` (e.g. `tree-sitter build --wasm`, which runs `docker run emscripten/emsdk`) don't know about finch. A 3-line shim that `exec sudo … finch "$@"` symlinked as `docker` on a prepended PATH unblocks them without aliases the child process can't see.
- [Resolve milestone-old spec drifts inline with the implementing commit](solutions/best-practices/spec-drift-amend-inline-with-implementing-commit.md) — amend spec wording in the same commit that implements the resolution; record drifts with `recommend` in explore-delta so Gate 0 is a confirmation, not a fresh debate.
- [Segregate graph-only and tabular-only stores at the interface boundary](solutions/architecture-patterns/igraphstore-itemporalstore-segregation.md) — when one type extends multiple sub-interfaces and a concrete implementor can't honestly satisfy all, segregate at the interface, not the class. `IGraphStore` + `ITemporalStore` + `openStore()` composition factory.
- [Replace raw-SQL escape hatches with typed finders on the storage interface](solutions/architecture-patterns/typed-finders-replace-raw-sql-in-consumers.md) — 108 raw-SQL sites collapse into 15 named finders. Adapters internalize dialect; consumers stay backend-agnostic. Liskov-clean parity harness via public-method rebuilder.
Expand Down Expand Up @@ -57,9 +59,12 @@ development sessions. Solutions are reusable; specs are per-feature.
- [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.

- [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.
- [tree-sitter-wasms catalog package is unusable with web-tree-sitter 0.26+](solutions/architecture-patterns/tree-sitter-wasms-catalog-incompat.md) — `tree-sitter-wasms@0.1.13` ships `.wasm` built with `tree-sitter-cli@0.20.8` (legacy 6-byte `dylink` section); `web-tree-sitter@0.26+` hard-requires the 8-byte `dylink.0` and throws `need the dylink section to be first`. Build the grammar `.wasm` yourself for grammars missing a vendored blob; don't reach for the shared catalog.

- [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).

- [release-please: a single root package with a scoped name cannot create a release](solutions/conventions/release-please-single-root-package-cannot-release.md) — a scoped single root package can't release: `this.component = options.component || normalizeComponent(packageName)` falsy-coerces `component:""`, derives `cli`, and `buildRelease`'s single-entry standalone check rejects the component-less PR → `release_created=false` → npm never publishes. Keep TWO components (root + cli) + the `linked-versions` plugin so the published CLI bumps in lockstep with root on every commit.

- [The MCP surface is read-only: tools write artifacts, never a user's source](solutions/architecture-patterns/mcp-surface-is-read-only-artifacts-not-source.md) — no source-mutating MCP tool, period (`rename` + `remove_dead_code` removed). `readOnlyHint:false` is fine for artifact writers (scan/pack_codebase/group_sync write to `.codehub/`); `destructiveHint:true` (source mutation) is the forbidden flag. Pin the 28-name set + "no destructiveHint tool" in a server contract test. Includes the full tool-removal surface checklist.

- [Rip a superseded never-wired subsystem — but record the capability gap](solutions/architecture-patterns/rip-superseded-subsystem-record-the-gap.md) — stack-graphs (~1,900 LOC, test-only) was sound by design but superseded by SCIP-as-oracle and orphaned by upstream archival. Rip test: dead-at-runtime + superseded + upstream-dead. Investigate intent first (docstrings, vendor README, git log, prior research notes); record the precision gap the rip leaves (non-SCIP languages lose the precision overlay) in the commit/ADR so a future session doesn't revive the dead end.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ session: session-88b46e
related:
- release-published-event-needs-pat-or-inline
- vendored-artifact-bump-must-revendor-in-same-pr
- release-please-single-root-package-cannot-release
- workflow-call-permissions-ceiling
---

# npm trusted publisher matches the ENTRY workflow, not the reusable one
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
---
name: release-please-single-root-package-cannot-release
description: release-please v17 CANNOT create a release for a single root package with a scoped package-name. `this.component = options.component || normalizeComponent(packageName)` falsy-coerces `component:""`, derives `cli` from `@opencodehub/cli`, and buildRelease's standalone-component check then rejects the component-less release PR (`PR component: undefined does not match configured component: cli`) → release_created=false → npm never publishes. For "one published CLI that bundles N private libs," keep TWO components (root + cli) and use the linked-versions plugin so cli bumps with root on every commit.
metadata:
type: convention
category: conventions
tags: [release-please, monorepo, linked-versions, component, npm, publish, single-package, buildRelease]
discovered: 2026-06-11
session: session-f12592
related:
- tsup-collapse-monorepo-to-single-cli
- npm-trusted-publisher-matches-entry-workflow-not-reusable
- release-published-event-needs-pat-or-inline
- workflow-call-permissions-ceiling
---

# release-please: a single root package with a scoped name cannot create a release

## Symptom

Published npm package stuck at an old version even though tags/commits advance.
release-please run logs, in the "Building releases" phase:

```
✔ Building release for path: .
⚠ PR component: undefined does not match configured component: cli
```

Result: zero candidate releases built, nothing tagged, `release_created=false`,
the `workflow_call` to release.yml never fires, npm never publishes. Downstream
you also see `⚠ There are untagged, merged release PRs outstanding - aborting`
— that is a SYMPTOM (createPullRequests refusing to open a new PR while a merged
`autorelease: pending` PR exists), not the cause.

## Root cause (release-please v17.6.0 source)

`src/strategies/base.ts`: `this.component = options.component || this.normalizeComponent(this.packageName)`.
The `||` (not `??`) **falsy-coerces `component: ""`** away, so a scoped
`package-name: "@opencodehub/cli"` derives component `cli` (Node strategy's
`normalizeComponent` strips the `@scope/`). Then `buildRelease`'s standalone
check (fires only when the PR has a SINGLE release entry) compares the release
PR's branch component (`undefined` — branch `release-please--branches--main`
carries no `--components--` segment) against the derived `cli`. `"" !== "cli"` →
warn + `return` → zero releases.

`include-component-in-tag: false` does NOT help: it only governs TAG naming
(`getComponent`), not the matcher, which uses `getBranchComponent` (ignores that
flag). So you cannot get (clean `vX.Y.Z` tag) + (component-less PR) + (passing
matcher) simultaneously for a single scoped root package. `component: ""` is
provably impossible via the `||`.

## Fix — keep TWO components + linked-versions

With 2 components the aggregate release PR has `releaseData.length === 2`, so the
broken single-entry standalone-component check is SKIPPED entirely. This is why
the 2-component scheme released reliably for 20+ versions and the single-component
collapse broke it.

```jsonc
"packages": {
".": { "package-name": "opencodehub", "component": "root" },
"packages/cli": { "package-name": "@opencodehub/cli", "component": "cli" }
},
"plugins": [
{ "type": "linked-versions", "groupName": "opencodehub", "components": ["root", "cli"] }
]
```

`linked-versions` solves the ORIGINAL starvation bug (root `.` receives every
commit so it always bumps; the published cli only saw `packages/cli/**` and
starved): it syncs all listed components to the highest version, so any commit
that bumps root bumps cli in lockstep → cli publishes on every release.

## Migration mechanics that bit us

- Manifest must list BOTH components at versions whose **component-format tags
exist** (`root-v0.8.5`, `cli-v0.7.4`) — release-please uses the manifest for the
current version but needs a matching tag for the SHA boundary. Seeding a
version with no matching tag → "No latest release found" → giant-changelog risk.
- linked-versions syncs to the HIGHEST member, so root 0.8.5 + cli 0.7.4 → both
next bump to 0.8.6. The published cli jumps 0.7.4 → 0.8.6 (forward, npm-valid).
Accept the number jump; boundary-safety (both tags exist) beats a pretty number.
- Do NOT bootstrap a new tag scheme by hand-creating `vX.Y.Z` tags + GitHub
Releases: release-please's manual-vs-owned-release reconciliation is fragile,
AND a `gh release`/`workflow_dispatch` recovery makes `release.yml` the OIDC
entry workflow → fails the trusted-publisher match (registered for
`release-please.yml`). See [[npm-trusted-publisher-matches-entry-workflow-not-reusable]].
Only the automated `release-please.yml → workflow_call → release.yml` chain
publishes. Recovery must go through that chain.

## Verified

After restoring 2 components + linked-versions: release PR bumped BOTH to 0.8.6,
merge → `release_created=true`, tags `root-v0.8.6`+`cli-v0.8.6`, automated chain
ran `npm publish (OIDC + provenance) => success`, `npm view @opencodehub/cli
version` → 0.8.6 (was stuck at 0.7.4). Confirmed via cache-busted
`npx @opencodehub/cli@latest --version`.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ type: knowledge
tags: [github-actions, release-please, release-published, github-token, sbom, code-pack, ci]
session: session-85faf1
ac: AC-D-4
related:
- npm-trusted-publisher-matches-entry-workflow-not-reusable
- workflow-call-permissions-ceiling
- release-please-single-root-package-cannot-release
---

## Context
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ created: 2026-05-15
session: session-569b82
track: bug
category: conventions
related:
- release-published-event-needs-pat-or-inline
- npm-trusted-publisher-matches-entry-workflow-not-reusable
- release-please-single-root-package-cannot-release
---

# GitHub Actions: top-level `permissions:` is a hard ceiling
Expand Down
Loading