Skip to content
Merged
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
47 changes: 42 additions & 5 deletions .github/workflows/bench.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,49 @@ on:
# the same commit (every main commit has one, from its push run).
workflow_dispatch:

# Cancel in-flight benches when a new push lands on the same PR / branch.
# Killing a bench mid-run is safe for the shared-box mutex: flock releases
# with the process (see tools/ci/with-nashua-lock.sh).
# Cancel in-flight benches when a new push lands on the same PR. Killing a
# bench mid-run is safe for the shared-box mutex: flock releases with the
# process (see tools/ci/with-nashua-lock.sh).
#
# NEVER on a push to main, because on a push `head_ref` is empty and every main
# push therefore shares the group `bench-refs/heads/main`. With
# cancel-in-progress: true, the release workflow's own version commit — pushed
# about five minutes after the merge that triggered it, into a bench that takes
# eleven — entered that group, CANCELLED the merge commit's bench, and was then
# skipped itself by the gate below. The merge produced no baseline at all:
#
# 21:45 16f50e3 Expand `hrtime` utility... (#70) cancelled
# 21:50 91d91bc chore(release): publish skipped
# 16:56 21d4749 fix: consolidated codec fixes (#73) cancelled
# 17:01 7abaaa9 chore(release): publish skipped
#
# The gate's guard exists to stop the version commit seeding a DUPLICATE
# baseline; paired with unconditional cancellation it destroyed the real one and
# supplied nothing in its place. Every PR opened afterwards compared against
# whatever CodSpeed still held for each benchmark, which is how a diff touching
# no runtime code drew a two-fold "regression".
#
# This was masked while releases were broken: a release that dies before the
# push never cancels anything, so `bac71dd` kept its baseline by accident. Fixing
# the release makes the version commit land reliably, which would have made this
# fire on most merges from now on.
#
# cancel-in-progress: false is NOT sufficient on its own, which is why the group
# is per-commit off a PR rather than per-branch. A concurrency group holds one
# running run and at most one PENDING run; queueing a third CANCELS the pending
# one, whatever cancel-in-progress says. A bench takes eleven minutes, so two
# merges inside that window would have put the second in the pending slot and
# let a third evict it -- the same lost baseline this is meant to fix, reached
# by a different route.
#
# Keying non-PR runs by github.sha gives every main commit its own group, so no
# main bench can ever cancel another. They serialise anyway: the bench box takes
# a mutex (tools/ci/with-nashua-lock.sh), which queues them without discarding
# any. PRs keep the branch-level group, where superseding an in-flight bench with
# a newer push is exactly what is wanted.
concurrency:
group: bench-${{ github.head_ref || github.ref }}
cancel-in-progress: true
group: bench-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

permissions:
contents: read
Expand Down
18 changes: 12 additions & 6 deletions .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -378,14 +378,20 @@ jobs:
# Exercises the release scripts on every PR. They are otherwise only
# ever executed by a live release — after the 8-job build matrix, and
# partway through mutating manifests, which is a poor place to discover
# a syntax error or a bad tag walk. Mutates nothing (--dry-run), and
# publish-order.mjs additionally re-checks the topological order and
# that every package shipping dist/ actually has one.
# a syntax error or a bad tag walk. Mutates nothing and publishes
# nothing.
#
# release:preflight re-checks the topological order and that every
# package shipping dist/ actually has one, then asks the registry
# whether each package can actually be published. A package that is not
# on npm at all is only a WARNING here — the PR adding a codec is
# exactly when it legitimately does not exist yet — but it is the
# warning that was missing when libjxl was merged in #88 and broke the
# next four releases. In the release workflow the same finding is fatal.
run: |
set -euo pipefail
node tools/release/version.mjs --dry-run
echo "Publish order:"
node tools/release/publish-order.mjs
pnpm run release:plan
pnpm run release:preflight

dist-size:
# Binary-size regression gate: compares every shipped dist artifact
Expand Down
57 changes: 20 additions & 37 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ name: Release
# THE FILENAME OF THIS WORKFLOW IS LOAD-BEARING. Each package's npm trusted
# publisher is registered against `release.yml` in cornerstonejs/codecs, so npm
# will reject an OIDC publish coming from any other workflow file. Renaming
# this file means re-running tools/release/setup-trusted-publishing.sh.
# this file means re-running `npm run release:trust`.
#
# This replaces the old CircleCI NPM_PUBLISH job, which needed two long-lived
# personal credentials: an NPM_TOKEN written to ~/.npmrc, and a maintainer's
Expand Down Expand Up @@ -509,55 +509,38 @@ jobs:
cp -r "$d"/* "packages/$pkg/dist/" 2>/dev/null || true
done
ls packages/*/dist 2>/dev/null | head
- name: Compute publish order
# Generated as its own command on purpose. `done < <(cmd)` is process
# substitution, whose exit status `set -e` does not see: if
# publish-order.mjs refused an empty dist, the loop below would read
# nothing and the step would still go green, reporting "Published 0
# packages" after the version commit and tags had already landed on
# main. Running it here makes that failure fail the job.
#
# publish-order.mjs emits the packages in dependency order, so
# dicom-codec goes out after the six siblings whose ranges it carries —
# publishing it alphabetically (third) left a window where those ranges
# resolved to versions that did not exist yet. It also refuses to emit a
# package that claims to ship dist/ but has an empty one.
run: |
set -euo pipefail
node tools/release/publish-order.mjs > publish-order.txt
cat publish-order.txt
- name: Publish to npm
# One node script rather than a bash loop, for a reason the 2026-09-01
# release paid for: `@cornerstonejs/codec-libjxl` was new, npm's OIDC
# trusted publishing cannot create a package that does not exist yet, so
# `npm publish` failed ENEEDAUTH — and `set -e` killed the loop where it
# stood, stranding the four already-registered packages queued behind it.
# publish.mjs resolves every package's registry state BEFORE it publishes
# anything, so a release that cannot fully succeed publishes nothing at
# all and says exactly what a human has to do. See its header.
#
# Deliberately NOT gated on the plan. The source of truth is "every
# workspace package whose current version is not yet on the registry",
# which covers both the normal path (the versions version.mjs just
# wrote) and the retry path: if a previous run committed and tagged but
# died partway through publishing, version.mjs correctly finds nothing
# new to release on the re-run, and this step still finishes the job.
#
# It also emits publish-order.txt for the github-releases job, in
# dependency order, so dicom-codec goes out after the six siblings whose
# ranges it carries — publishing it alphabetically (third) left a window
# where those ranges resolved to versions that did not exist yet — and
# refuses a package that claims to ship dist/ but has an empty one.
#
# No .npmrc and no NODE_AUTH_TOKEN: `npm publish` performs the OIDC
# exchange itself using the id-token permission above. Provenance is
# generated automatically for trusted publishes, which is why every
# package's repository.url must point at this repo.
#
# --ignore-scripts: prepublishOnly re-runs `bash build.sh`, and this
# runner has no emscripten toolchain. The dist being published is the
# artifact the build job produced from this same commit.
run: |
set -euo pipefail
published=0
skipped=0
while read -r name version dir; do
if npm view "$name@$version" version >/dev/null 2>&1; then
echo "$name@$version is already on npm; skipping."
skipped=$((skipped + 1))
continue
fi

echo "Publishing $name@$version"
(cd "packages/$dir" && npm publish --ignore-scripts)
published=$((published + 1))
done < publish-order.txt
echo "::notice::Published $published package(s); $skipped already on npm."
# `npm run` needs no node_modules — npm ships with node, and publish.mjs
# imports nothing but node builtins — so this job still installs no
# dependencies and the OIDC token still has no third-party code beside it.
run: npm run release:publish -- --out publish-order.txt
- name: Upload publish order
# Handed to the github-releases job so it can enumerate the same
# packages without needing the dists (publish-order.mjs inspects them)
Expand Down
32 changes: 32 additions & 0 deletions BENCHMARKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,38 @@ decode is the time spent" for wasm code — that's a fundamental
limitation of wasm-via-V8-via-Cachegrind. For wasm hotspot analysis,
profile the native build with `perf` / Instruments / VTune instead.

### "N benchmarks were skipped, so the baseline results were used instead"
The CodSpeed project holds more benchmark entries than the repo now
defines — as of 2026-09-04, 133 registered against 67 that actually run.
The surplus entries are orphans left by benches that were renamed or deleted
(the cold/warm split, the openjph upstream rework), plus
`libjpeg-turbo-12bit`, whose `bench` script is deliberately a no-op
(`.51` transfer syntax is disabled) so its 9th bench file never executes.

**What to do**: nothing, for correctness. A skipped benchmark reuses its
baseline on *both* sides of the comparison, so its delta is zero and it
can never trigger a regression — the only cost is a misleading total.
Archiving them is **dashboard-only** (Settings → the benchmark → Archive);
there is no repo config, config file or code annotation that clears them,
so this cannot be fixed in a commit.

### A regression on a diff that changes no runtime code
Almost always a stale baseline rather than a real change. The usual cause
was a main baseline that never got measured: until 2026-09-04 the release
workflow's version commit shared `bench.yml`'s push concurrency group with
the merge commit it followed, so it cancelled that bench and then skipped
its own — see the comment on `concurrency:` in
[.github/workflows/bench.yml](.github/workflows/bench.yml). Merges of #70
and #73 produced no baseline at all.

**What to do**: confirm the diff cannot affect the measured path, then let
the next successful **push to main** re-seed the baseline for all live
benchmarks — that happens on its own and needs no dashboard access.
Acknowledging a regression directly *is* dashboard-only and admin-only, but
it is not required to merge: main's ruleset lists no required status checks
(only 1 approving review and code-owner review), so a red CodSpeed check
never blocks a pull request.

### Tier-related regression in `instantiate+destroy X`
The instantiation bench measures whatever V8 tier the embind helpers
(`makeClassHandle`, `RegisteredPointer_fromWireType`, etc.) happen to
Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
"build:publish": "pnpm -r --parallel run build:ci",
"csp:source": "node tools/csp/check-source-js.js",
"docker:build": "bash tools/docker/build.sh",
"release:plan": "node tools/release/version.mjs --dry-run"
"release:plan": "node tools/release/version.mjs --dry-run",
"release:order": "node tools/release/publish-order.mjs",
"release:preflight": "node tools/release/publish.mjs --preflight",
"release:publish": "node tools/release/publish.mjs",
"release:trust": "node tools/release/setup-trusted-publishing.mjs"
}
}
3 changes: 2 additions & 1 deletion packages/libjxl/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
],
"repository": {
"type": "git",
"url": "git+https://github.com/cornerstonejs/codecs.git"
"url": "git+https://github.com/cornerstonejs/codecs.git",
"directory": "packages/libjxl"
}
}
Loading
Loading