Releases and npm publishing

SkillDev tools

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.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Releases and npm publishing skill

What this skill tells your AI

The instructions your AI receives, as published by andymai/brepjs in .claude/skills/release-publishing/SKILL.md and read by ahel’s review.

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-opencascadepermanently held (manual, expensive WASM build).
    • root branch release-please--branches--main--components--brepjsgh 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-<pkg>.yml with -f dry_run=false --ref <release-tag>. (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

RuleWhyWhere
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 and every packages/* workspace, so a satellite-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.release-please-config.json:8-21
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-<pkg>.yml -f dry_run=false --ref <release-tag>

brepjs-opencascade needs publishing. Manual only:

gh workflow run publish-opencascade.yml -f dry_run=false --ref <release-tag>

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

PackageManaged?Auto-merge?Auto-publish?Publish route
brepjs (root)yesyes (first)yes, inlinerelease-please.yml job publish-brepjs (OIDC)
brepjs-cadyesyes (after root)yes, dispatchedpublish-brepjs-cad.yml
brepjs-bimyesyes (after root)yes, dispatchedpublish-brepjs-bim.yml
brepjs-sheetmetalyesyes (after root)yes, dispatchedpublish-brepjs-sheetmetal.yml
brepjs-opencascadeyesheld (manual)manual onlypublish-opencascade.yml (Docker WASM)
brepjs-voxel-wasmyes (bumps Cargo.toml)yes (after root)no workflow, not on npmnone
brepjs-viewernon/amanualpublish-brepjs-viewer.yml
brepjs-voxelnon/anot publishednone

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.
  • 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.

Signals

GitHub stars
101
Forks
9
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
release-publishing
Source
github.com/andymai/brepjs