--- name: release-publishing description: This skill should be used when operating releases or npm publishing in brepjs — cutting or merging a release-please PR, recovering a failed npm publish, or deciding whether a commit may carry a breaking marker. Trigger phrases include "release PR", "release-please", "autorelease: pending", "publish to npm", "npm publish failed", "republish", "ETARGET from a leaf release PR ordered ahead of root", "version bump", "why did the major version bump", "breaking change commit", "trusted publisher", "dry_run", "release tag", "manifest conflict", or editing .github/workflows/publish-*.yml or release-please-config.json. --- # Releases and npm publishing Every release in this repo flows through one workflow: `.github/workflows/release-please.yml`. Its inline comments are the canonical deep documentation — read them before changing anything. This skill is the operator's playbook: how a normal release flows, the hard rules that prevent outages, and the recovery recipes when a step fails. ## Mental model - **release-please, manifest mode.** Versions live in `.release-please-manifest.json`; managed packages are declared in `release-please-config.json`. On every push to `main`, release-please opens/updates a per-package release PR (`separate-pull-requests: true`), bumping the version and CHANGELOG from Conventional Commits. - **Six managed packages, two deliberately unmanaged.** Managed: root `.` (`brepjs`), `packages/brepjs-opencascade`, `packages/brepjs-voxel-wasm`, `packages/brepjs-cad`, `packages/brepjs-bim`, `packages/brepjs-sheetmetal`. NOT managed by release-please: `brepjs-viewer` (versioned/published manually — see below) and `packages/brepjs-voxel` (unpublished workspace consumer). - **node-workspace re-pins cross-deps.** The `node-workspace` plugin rewrites each leaf package's `brepjs` dependency to the version of the pending root release. This is the source of the ETARGET hazard (below). - **npm auth is OIDC trusted publishers, bound to workflow filenames.** Each package's publish authenticates via an npm trusted publisher tied to a specific `.github/workflows/publish-*.yml` filename. Renaming a publish workflow breaks auth silently. ## How a normal release flows 1. Commits land on `main` with Conventional-Commit subjects (enforced by commitlint — see `commitlint.config.js`, `.husky/commit-msg`; format detail lives in the **git-pr-workflow** skill). 2. The `release-please` job (App token, `googleapis/release-please-action@v5`) opens or updates release PRs labeled `autorelease: pending`. 3. The `auto-merge` job serializes **root before leaves**: it lists open `autorelease: pending` PRs and, per branch name: - `*--components--brepjs-opencascade` → **permanently held** (manual, expensive WASM build). - root branch `release-please--branches--main--components--brepjs` → `gh pr merge --auto --squash` (merges first). - any other leaf → **held while a root release PR is open**; merged on a later run after root lands. 4. Root release PR merges. CI passes trivially on release PRs — `ci.yml` skips the paths-filter for `release-please--*` branches, and `ci-pass` treats skipped jobs as pass — so `--auto` completes. 5. `publish-brepjs` runs inline in `release-please.yml`: `npm ci && npm run build && npm publish --provenance` (OIDC). This is the ONLY package published inline. 6. Merging bumps `main`. The next release-please run regenerates each leaf PR with a now-valid `brepjs` pin, auto-merges them, and each **auto-publishing** leaf's job dispatches its own `publish-brepjs-.yml` with `-f dry_run=false --ref `. (`brepjs-opencascade` is the exception — always manual via `publish-opencascade.yml`; `brepjs-voxel-wasm` has no publish workflow.) Leaves dispatch against the **immutable release tag** release-please just created, never `main`: the dispatch API accepts a tag but rejects a raw SHA, and the tag pin avoids publishing the wrong commit if another push lands mid-window. ## Hard rules | Rule | Why | Where | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------- | | Never put `!`/`BREAKING CHANGE` on a commit unless intentionally breaking the `brepjs` public API. | Root is 18.x; any breaking marker majors the library immediately. | `.release-please-manifest.json` | | Root `exclude-paths` are `apps`, `tests` and every `packages/*` workspace, so a satellite-only or test-only commit never bumps root. A breaking commit touching root `docs/`, `README.md`, `scripts/`, or `.github/` still majors root. | Every non-excluded path attributes to the root component, and attribution uses the PR's changed-file list, not the squash commit's (see `publish-pipeline.md`). | `release-please-config.json:8-22` | | Never rename or move a `publish-*.yml` file without re-registering the npm trusted publisher for that package. | OIDC auth is bound to the exact filename; a rename makes `npm publish` fail auth. | comments in each `publish-*.yml` | | Never merge a leaf release PR while the root `brepjs` release PR is open. | node-workspace pins the leaf to an unpublished `brepjs` version → `npm install` ETARGET → Vercel deploys break. Also conflicts the root PR on the shared manifest. | `release-please.yml:57-72` | | Never cancel an in-flight Release Please run. | Cancelling mid-publish leaves a tagged-but-unpublished release; the concurrency group queues, never cancels. | `release-please.yml:17-24` | | Manual publish dispatches default to `dry_run: true` (build-only). A real publish needs `-f dry_run=false` and a ref. | A bare dispatch is a safe smoke build. | every `publish-*.yml` | | `brepjs-opencascade` is always manual. | ~60-min Docker WASM build from `ghcr.io/andymai/opencascade.js:v8` (needs `GHCR_TOKEN`). | `publish-opencascade.yml` | ## Recovery playbook **Root `brepjs` publish failed (release tagged, npm empty).** Do NOT re-run the failed push job — release-please would try to re-tag. Instead re-dispatch the whole workflow in republish mode: ``` gh workflow run release-please.yml -f republish=true ``` This skips the `release-please` job and runs only `publish-brepjs` against the current `main` `package.json` version. Republish covers **root brepjs only**. **A leaf publish failed (`brepjs-cad`/`bim`/`sheetmetal`).** Re-dispatch that leaf's own publish workflow against the release tag: ``` gh workflow run publish-brepjs-.yml -f dry_run=false --ref ``` **`brepjs-opencascade` needs publishing.** Manual only: ``` gh workflow run publish-opencascade.yml -f dry_run=false --ref ``` **Conflicted release PRs (serializer bypassed — e.g. a human merged a leaf early).** Not tooled; manual fix. For each still-open release PR: merge `main` into its branch, take the union of `.release-please-manifest.json` (keep every package's highest intended version), resolve CHANGELOG/package.json, push. Then let the `auto-merge` job re-arm on the next run. **`npm install`/CI fails with ETARGET after a merge.** A leaf on `main` is pinned to a `brepjs` version not yet on npm. Confirm root actually published (`npm view brepjs version`); if root is fine, the leaf pin is ahead — publish root first (republish above), or bump the leaf's pin down to a published version and open a fix PR. Related consumer-side symptoms are covered in **ci-triage** and **companion-packages**. **Pack validation failed (`prepack`).** Root `npm pack`/`npm publish` runs `scripts/validate-pack.sh` (wired as `prepack`, with `prepublishOnly: npm run build`). It fails on more than 500 files or any `.d.ts.map` sidecar. Inspect with `npm pack --dry-run`. `.d.ts.map` files mean `declarationMap` got re-enabled — check `vite.config.ts`. ## Package publish matrix | Package | Managed? | Auto-merge? | Auto-publish? | Publish route | | -------------------- | ------------------------ | ----------------- | --------------------------- | ------------------------------------------------ | | `brepjs` (root) | yes | yes (first) | yes, inline | `release-please.yml` job `publish-brepjs` (OIDC) | | `brepjs-cad` | yes | yes (after root) | yes, dispatched | `publish-brepjs-cad.yml` | | `brepjs-bim` | yes | yes (after root) | yes, dispatched | `publish-brepjs-bim.yml` | | `brepjs-sheetmetal` | yes | yes (after root) | yes, dispatched | `publish-brepjs-sheetmetal.yml` | | `brepjs-opencascade` | yes | **held (manual)** | **manual only** | `publish-opencascade.yml` (Docker WASM) | | `brepjs-voxel-wasm` | yes (bumps `Cargo.toml`) | yes (after root) | **no workflow, not on npm** | none | | `brepjs-viewer` | **no** | n/a | manual | `publish-brepjs-viewer.yml` | | `brepjs-voxel` | no | n/a | not published | none | Build prerequisites baked into the publish workflows: `brepjs-cad` restores OCCT WASM (`scripts/ensure-wasm.sh`, 3× retry) then builds root + `brepjs-viewer` + itself (viewer is a build-time devDep whose dist is bundled into the `brepjs-cad` build output). `bim` and `sheetmetal` build root `brepjs` first (their `vite-plugin-dts` needs root's emitted types). `brepjs-viewer` uses `npm ci --ignore-scripts` and publishes `--access public`. ## Notes and gotchas - **`brepjs-viewer` is intentionally de-managed** from release-please. node-workspace kept re-pinning `brepjs-cad`'s build-time `brepjs-viewer` devDep (`"brepjs-viewer": "*"`) to viewer's pending unpublished version, breaking monorepo `npm ci`. Version and publish it by hand via `publish-brepjs-viewer.yml`. - **The Claude-plugin marketplace version is independent of the npm version.** release-please does not bump `packages/brepjs-cad/.claude-plugin/plugin.json`; the plugin version and the npm `brepjs-cad` version move separately. - **Docs deploy is not release-triggered.** `.github/workflows/docs.yml` chains off successful CI on `main` (`workflow_run`); emergency redeploy is a main-only `workflow_dispatch`. - **`npm unpublish` is never available for `brepjs`.** The published satellites (`brepjs-bim`, `brepjs-families`, `brepjs-sheetmetal`, `brepjs-cad`) declare `brepjs >=18.x`, and npm refuses to unpublish a version that any registry package depends on, even inside the 72-hour window; only `npm deprecate` is offered. A mistaken release therefore stays: annotate its changelog section and GitHub release notes instead (brepjs 20.0.0, 2026-09-21, was a no-op major kept this way). Rewinding the manifest is not an option either, since the version remains on the registry. - **`scripts/publish-all.sh` is a legacy manual OTP fallback** (opencascade → root only, `--no-provenance`). The OIDC workflows supersede it; use only as a local last resort, and note it does not cover cad/bim/sheetmetal/viewer. ## Additional resources - Commit format and the `!` decision: `git-pr-workflow` skill. - CI job failure modes (npm ci EUSAGE/ETARGET, release PR won't merge, publish red): `ci-triage` skill. - Which package to edit and how a change ripples to consumers: `companion-packages` skill. - Canonical deep reference: the inline comments in `.github/workflows/release-please.yml`.