CI failure triage
Diagnose a red or stuck brepjs GitHub Actions run. Triage in two moves: first classify which workflow and job failed and on what event (PR vs push-to-main vs manual dispatch vs deployment); then match the symptom to the table below.
Most failure modes are documented in inline comments in the workflow files themselves — cite line refs (e.g. .github/workflows/ci.yml:244-250) rather than re-deriving. This skill is the index and the recovery recipes.
Job map — what runs, when
The single required PR check is ci-pass (ci.yml:431-459). It needs every gate job and fails if any result is failure or cancelled (a skipped result passes). Its needs-list: changes, typecheck, lint, quality, build, playground-build, packages-viewer, packages-verify, packages-sheetmetal, packages-bim, voxel-wasm-rust, test, size, benchmark. Deliberately not in the list: coverage.
The changes job (ci.yml:18-67) path-filters into three outputs — code, playground, voxel — that gate the rest. Branches named release-please--* skip the filter entirely (ci.yml:29-36): release PRs run no code jobs (they touch only CHANGELOG/version/manifest).
Consequence: a PR failure can never come from coverage (push-only); a main-only failure can never come from size/benchmark (PR-only).
Concurrency: ci-${{ github.ref }}, cancel-in-progress on non-main refs only (ci.yml:10-12).
Symptom → cause → fix
Ordered roughly by frequency.
npm ci fails with EUSAGE in every job's setup step
Lock file out of sync with package.json ("lock file does not satisfy"). Classic trigger: a Dependabot group bump silently drops the two top-level @emnapi/* peer-dep entries (package-lock.json node_modules/@emnapi/core + node_modules/@emnapi/runtime, ~lines 1180-1201) that satisfy @napi-rs/wasm-runtime's peerDependencies (~lines 2285-2286). Fix: surgically restore the dropped entries — never regenerate the lockfile from scratch. Run --package-lock-only from main to reproduce the correct shape. See the git-pr-workflow and companion-packages skills for lockfile-surgery discipline.
npm ci fails with ETARGET / 404 on an internal package (in a CI run)
An internal workspace package is pinned to a version not yet on npm. Two causes:
- release-please re-pin: the node-workspace plugin repins a leaf's
brepjs dep to a pending (unmerged) root release version. This is why leaf release PRs are held until root merges+publishes (see auto-merge below).
- 0.x caret exclusion: a
^0.<older> range excludes the next 0.(x+1) minor, so an internal bump 404s any consumer still on the caret.
Fix: use an open floor range, not a pin. Main already does this — packages/brepjs-cad/package.json:60 ("brepjs": ">=18.117.1") and packages/brepjs-voxel/package.json:24 ("brepjs-voxel-wasm": ">=0.2.0"). Full mechanics in the release-publishing skill.
Note: for internal workspace devDeps the "*" wildcard is intentional (e.g. brepjs-cad's "brepjs-viewer": "*", package.json:81) — pinning it to a concrete version is what repeatedly broke npm ci when the pin outran what was published. A Dependabot alert on such a "*" spec is a false positive: raise the floor (>=x.y.z) if it must be cleared, don't pin it or bump the lockfile.
build job fails on git diff --exit-code docs/function-lookup.md
A *Fns.ts change added/renamed an exported function but docs/function-lookup.md wasn't regenerated (the gate at ci.yml:112-117 runs docs:generate-lookup → prettier --write → diff; pre-commit only reminds, so it's easy to miss locally). Fix: regenerate + prettier-align + commit the file — full recipe in the adding-operations skill.
quality job fails check:patterns on code the PR didn't touch
An unbaselined pattern violation reached main and now fails the quality job on every open PR at once. Fix: land a tiny baseline-bump PR first, then rebase the others. Mechanism and baseline mechanics live in the quality-gates skill.
test shard times out or stalls
OCCT WASM linear memory grows monotonically across tests; a long run over-commits the 16 GB runner into swap and a fork stalls past the 90s per-test timeout (#1102, rationale at ci.yml:244-250). The 4-way shard exists precisely so each fork exits before accumulation hits that threshold. Do not "fix" by widening the timeout. Check whether one shard is genuinely slower (real leak / new heavy test) or just an infra blip — re-run the single shard. Coverage can't shard (vitest --merge-reports can't recombine v8 coverage here), which is why the coverage job runs single-runner with a raised timeout. Test-authoring details in the writing-tests skill.
packages-verify fails on smoke or smoke:standalone
smoke runs the built brepjs-cad CLI under plain node (catches bin/TS-load breaks that tsx/vitest hide in-repo). smoke:standalone packs the tarball into a clean project with no brepjs present (catches bundling / fallback-resolve breaks invisible in-repo). A green in-repo test with a red standalone smoke means a packaging or resolution regression, not a logic bug. See the companion-packages skill.
playground-build fails but the library builds fine
Type-resolution or bundler regression scoped to apps/playground (this job mirrors what Vercel runs). Historical example: a duplicate @types/three install broke tsc -b only inside the playground (#963, ci.yml:119-137). See the playground-examples skill.
npm publish failed
Publishes are OIDC-bound to the exact workflow filename — inlining npm publish elsewhere fails auth. Never re-run the failed push-event job; re-dispatch the same workflow. Recovery recipes:
- Root brepjs:
gh workflow run release-please.yml -f republish=true (input at release-please.yml:6-12; publish gate at :146-149).
- A satellite (cad/bim/sheetmetal/viewer/opencascade):
gh workflow run publish-<pkg>.yml -f dry_run=false --ref <release-tag> — dry_run defaults to true, and dispatch against the immutable tag, not main.
Full recipe set and the OIDC-binding rationale: the release-publishing skill.
prepack fails: "Too many files" or ".d.ts.map"
scripts/validate-pack.sh (root prepack hook, package.json:216) caps the packed tarball at MAX_FILES=600 and rejects any .d.ts.map sidecars (count must be 0). It fires during pack/publish, not PR CI, so an overflow first shows up as a red publish-brepjs after the release PR merges: the version is tagged and GitHub-released but missing from npm. rollupTypes: false emits one .d.ts per source module, so every new src/ file adds one packed file. Count with npm run build && npm pack --dry-run --ignore-scripts. If the growth is just new modules, raise MAX_FILES (ci: commit, no release) and then gh workflow run release-please.yml -f republish=true. Fix .d.ts.map by keeping declarationMap off in vite.config.ts.