Skip to content

fix(source-dalux): route a different-node Dalux download link through… #2374

fix(source-dalux): route a different-node Dalux download link through…

fix(source-dalux): route a different-node Dalux download link through… #2374

Workflow file for this run

name: Release
# PUBLISH AUTHENTICATION — Phase B trusted publishing
#
# Package registries — short-lived credentials minted via OIDC at runtime:
#
# npm — Trusted Publisher configured per `@ifc-lite/*` package on
# npmjs.com. `id-token: write` + npm CLI ≥ 11.5.1 +
# NPM_CONFIG_PROVENANCE=true completes the OIDC handshake
# automatically; no NPM_TOKEN needed.
# crates.io — Trusted Publisher configured per crate on crates.io.
# rust-lang/crates-io-auth-action exchanges the OIDC token
# for a 30-minute CARGO_REGISTRY_TOKEN.
#
# Git side (push of `changeset-release/main` + opening the version PR) uses
# a fine-grained PAT (`secrets.RELEASE_PAT`) — see the inline comment on
# the Checkout step for why a PAT and not GITHUB_TOKEN or a GitHub App.
#
# The release job runs in the `release-publish` GitHub Environment so any
# residual repo-scoped secrets are unreachable from other jobs. See
# docs/security/incident-response.md (Phase E) for the rotation runbook.
on:
push:
branches:
- main
concurrency: ${{ github.workflow }}-${{ github.ref }}
permissions:
contents: read
jobs:
release:
name: Release
runs-on: ubuntu-latest
environment: release-publish
permissions:
contents: write
pull-requests: write
id-token: write
outputs:
published: ${{ steps.changesets.outputs.published }}
version: ${{ steps.get-version.outputs.version }}
# Should the publish verifiers run for this commit? See the comment on
# `version_changed` in the `pre` step, and on `verify-npm-publish`.
verify: ${{ steps.changesets.outputs.published == 'true' || steps.pre.outputs.version_changed == 'true' }}
steps:
# AUTH for the changesets version PR (`chore: version packages`):
# we use a fine-grained PAT (`secrets.RELEASE_PAT`), NOT the default
# GITHUB_TOKEN, because GitHub suppresses pull_request/push workflow
# triggers on GITHUB_TOKEN-authored events (recursion guard) so the
# required `Build + WASM + Rust + Node` check would never run on a
# version PR and it would sit permanently "Expected" (issue #766).
#
# Previously this used an installation token from a GitHub App
# (`ifc-lite-release-bot-org`) but the install didn't carry over when
# the repo moved from `louistrue/ifc-lite` to `LTplus-AG/ifc-lite`,
# and the App's installation token on the new owner lost
# `pull_requests:write` — every Release run after that failed with
# "Resource not accessible by integration" on the create-PR API call.
#
# A fine-grained PAT scoped to this repo with `contents: read+write`
# and `pull_requests: read+write` survives org/repo moves and removes
# the App-management surface entirely. Rotate the PAT in the repo
# secrets when it expires; no workflow change needed for rotation.
- name: Checkout Repo
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
lfs: false
# Persist the PAT so the version-packages branch push made by
# changesets/action is PAT-authored too — not just the PR API call.
# Both must be PAT-authored for the pull_request event to fire.
token: ${{ secrets.RELEASE_PAT }}
# Read the release state BEFORE `changesets/action` runs. That step invokes
# `pnpm run version` when changesets are pending, which CONSUMES
# `.changeset/*.md` and bumps `package.json` in this same working tree. A
# gate reading either of those afterwards sees "no changesets pending" and
# the NEW, not-yet-released version — and would tag a version whose
# packages have not been published, on the very run that only opened the
# version PR.
#
# This sits IMMEDIATELY after the checkout, and must stay there. Its
# outputs gate both publish-verification jobs, and an output is unset
# when the step never runs: every step placed above it — pnpm/Node
# setup, the WASM toolchain, `pnpm install`, the fixtures, `pnpm build`,
# the crates.io OIDC exchange — would take `verify` down with it, and
# both verifiers would skip DESPITE `always()`, on exactly the broken
# release that needs them. It needs no toolchain to do its job: `node`
# and `find` only, both present on the runner image before any setup
# step, and reading earlier only strengthens the
# "before changesets/action mutates the tree" premise above.
- name: Capture pre-release state
id: pre
run: |
echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT"
echo "sb_version=$(node -p "require('./packages/server-bin/package.json').version")" >> "$GITHUB_OUTPUT"
echo "pending=$(find .changeset -maxdepth 1 -name '*.md' ! -name 'README.md' | wc -l | tr -d ' ')" >> "$GITHUB_OUTPUT"
# Does THIS commit carry a version bump?
#
# This is what gates the publish verifiers below, and it answers a
# different question from `changesets.outputs.published`. `published`
# is "did this RUN publish something", which is false on the re-run
# that RECOVERS a partial publish (npm has nothing left to publish
# by then) — so the verifier was skipped on exactly the attempt that
# needed checking (#3181, v6.0.0 attempt 2). "Is this the release
# commit" stays true across every re-run of it, and stays false for
# the ~50 ordinary pushes a day that publish nothing, so the
# verifiers neither skip a recovery nor fire vacuously.
#
# The comparison lives in a script, not inline here, because the
# obvious inline spelling — root `package.json` version vs `HEAD~1` —
# is WRONG on this repo and looked right: `sync-versions.js` pins the
# root version to the HIGHEST workspace version and does not lockstep
# the rest, so the root version only moves when that one top package
# is bumped. Replayed over the last 60 `chore: version packages`
# commits it opened on 16; the whole-workspace comparison the script
# does opens on 60/60, and on 0/40 recent ordinary pushes. See the
# header of the script and its test for the measurement.
# The script fails OPEN internally, but a bare assignment from a
# command substitution takes the substitution's exit status, and the
# Actions default shell is `bash --noprofile --norc -eo pipefail`.
# So ANY non-zero `node` exit — the script renamed or absent, an
# import-time throw, OOM, node failing to start — would abort this
# STEP, leaving `version_changed` unwritten; `verify` would then read
# `''` and both verifiers would skip DESPITE `always()`, which is the
# exact failure the placement above exists to prevent. The `||` keeps
# the step alive; the second line makes anything that is not
# literally `false` — including the empty string an exit-0 run that
# printed nothing would leave — open the gate.
VERSION_CHANGED=$(node scripts/release-version-changed.mjs) || VERSION_CHANGED=true
[ "${VERSION_CHANGED}" = "false" ] || VERSION_CHANGED=true
echo "version_changed=${VERSION_CHANGED}" >> "$GITHUB_OUTPUT"
- name: Setup pnpm
# v6 reads the version from the `packageManager` field in
# package.json (`pnpm@10.8.1`); specifying `with.version` here
# would conflict and the action errors with ERR_PNPM_BAD_PM_VERSION.
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
# Node 24 ships npm ≥ 11.5.1 out of the box, which is what npm
# trusted publishing (OIDC handshake) needs. We tried staying on
# Node 22 and self-upgrading npm at runtime in Release #426/#427,
# but `npm install -g npm@latest` (with and without --force)
# leaves the live npm process unable to resolve its own bundled
# deps mid-install — `Cannot find module 'promise-retry'` from
# @npmcli/arborist/lib/arborist/rebuild.js. Node 24 sidesteps
# the self-upgrade entirely.
node-version: 24
registry-url: 'https://registry.npmjs.org'
# rust-toolchain.toml pins nightly-2025-11-15 + wasm32-unknown-unknown.
# `rustup show` installs that exact toolchain on GHA runners; matches
# test.yml so the published bundle is byte-identical
# to what CI tested. Plain @stable would leave the nightly missing and
# `wasm-pack build` would fail with "toolchain not installed" — exactly
# the regression observed on Release #403 after #657 untracked the wasm.
# Rust + wasm-pack for the wasm32 build (pure-Rust kernel — no C++
# toolchain since M9). Single-sourced in
# .github/actions/setup-wasm-build (shared with test.yml / sdk-canary.yml).
- name: Setup WASM build toolchain
uses: ./.github/actions/setup-wasm-build
with:
cache-prefix: release
- name: Install Dependencies
run: pnpm install --frozen-lockfile
# Cache + fetch fixtures so cross-package tests that load
# tests/models/ifc5/*.ifcx (pointcloud, export, ifcx) can run.
# Mirrors the Test workflow; key changes only when the manifest does.
- name: Cache test fixtures
id: fixtures-cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: tests/models
# Namespace cache key separately from `test.yml` (which uses
# `ci-fixtures-…`). Phase A hardening — keeps the privileged
# release runner from ever reading a cache writeable by a
# fork-triggered job. See AGENTS.md security notes.
key: release-fixtures-${{ runner.os }}-${{ hashFiles('tests/models/manifest.json') }}
- name: Fetch fixtures
if: steps.fixtures-cache.outputs.cache-hit != 'true'
run: pnpm fixtures
- name: Verify fixtures
run: pnpm fixtures:check
# Build the publishable artifacts. Typecheck / ESM-resolution /
# unit tests are NOT re-run here: branch protection requires
# test.yml's `Build + WASM + Rust + Node` check to be green on
# the PR before merge, so the SHA reaching `push: main` has
# already passed all of them. Repeating them would add ~8 min
# per release without changing the verdict.
- name: Build Packages
run: pnpm build
# Exchange the workflow's OIDC ID-token for a short-lived (30-minute)
# crates.io access token. A Trusted Publisher must be configured on
# crates.io for EACH published crate (ifc-lite-core, -geometry, -clash,
# -processing, -ffi, -wasm) pointing at this repo + this workflow
# before its first automated publish. A brand-new crate name (e.g.
# ifc-lite-ffi) must additionally be bootstrapped once with a personal
# API token — `cargo publish -p <crate> --token …` — to claim the name,
# since a Trusted Publisher can only be configured on an existing crate.
- name: Authenticate to crates.io (OIDC)
id: crates-auth
uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1.0.5
# SEMVER GATE for the crates (issue #3216). `pnpm release` runs
# `check:rust-semver` before it publishes anything; these two steps put
# the tool it needs on PATH.
#
# A stable toolchain, not the dated nightly in rust-toolchain.toml:
# cargo-semver-checks refuses a nightly outright ("rustc version is not
# high enough: >=1.93.0 needed, got 1.93.0-nightly"). The gate invokes it
# as `cargo +stable semver-checks`, so this only has to exist; it does not
# become the default toolchain and nothing else in the release uses it.
- name: Install stable Rust for the crate semver gate
uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # master 2026-05
with:
toolchain: stable
# Prebuilt binary. `cargo install cargo-semver-checks --locked` compiles
# it from source (~5 min) on a runner that is already the slowest job in
# the repo.
- name: Install cargo-semver-checks
uses: taiki-e/install-action@4f5ee4a4759116abc6b10af05f20374e179d8ca3 # cargo-semver-checks (snapshot pinned 2026-08)
- name: Create Release Pull Request or Publish
id: changesets
uses: changesets/action@8488615a623b1b9c987934bb89eae8af6a946ac1 # v2.1.1
with:
# v2 RENAMED every one of these. The old spellings were `version`,
# `publish`, `commit` and `title`, and GitHub Actions IGNORES an
# unknown `with:` key rather than failing — so carrying them across
# the major bump would leave the action with no version script and
# no publish script, and it would report success having published
# nothing. A release that goes green and ships nothing is worse
# than one that fails, so these names are load-bearing.
version-script: pnpm run version
publish-script: pnpm run release
commit-message: 'chore: version packages'
pr-title: 'chore: version packages'
# Also moved: v2 reads the token from this INPUT and no longer from
# `env.GITHUB_TOKEN`. It defaults to the automatic `github.token`,
# whose generated events do not trigger workflows — see the comment
# on the Checkout step. The PAT must stay, and it must be here.
github-token: ${{ secrets.RELEASE_PAT }}
env:
# npm publishes use OIDC trusted publishing — no NPM_TOKEN.
# `id-token: write` (set at job level) + npm CLI ≥ 11.5.1 +
# NPM_CONFIG_PROVENANCE complete the handshake automatically
# and attach SLSA provenance to every tarball.
NPM_CONFIG_PROVENANCE: 'true'
# crates.io token minted via OIDC above; expires in 30 minutes.
CARGO_REGISTRY_TOKEN: ${{ steps.crates-auth.outputs.token }}
# Whether to create this version's release artifacts.
#
# This used to be `steps.changesets.outputs.published == 'true'`, which
# asks "did npm publish in THIS run?" — a different question from "does
# this released version still need its tag?". The two came apart on
# 2026-08-12 (v4.4.1): `changeset publish` published every package, then
# the step failed while pushing tags on a transient TLS error
# ("server certificate verification failed"). `published` was therefore
# not 'true', so the v-tag steps skipped — and every later run skipped
# them too, because by then npm had nothing left to publish. v4.4.0 and
# v4.4.1 both ended up with no `v*` tag or GitHub release at all, and no
# run would ever create them.
#
# So: publish-in-this-run still qualifies, and a released version whose
# tag is missing now qualifies too. "Released" is read from the PRE-action
# state (see `steps.pre`): with changesets pending, this run only opened
# the version PR, and the post-action tree already carries the bumped,
# unpublished version.
#
# Root and server-bin are tracked SEPARATELY. A run that created the root
# release and then failed must still be able to backfill the server-bin
# one, which a single shared flag would skip forever — the same
# "one flag, two questions" mistake in miniature.
- name: Decide release artifacts
id: release-gate
run: |
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "sb_version=${SB_VERSION}" >> $GITHUB_OUTPUT
# Is each version actually OUT, as opposed to merely versioned?
#
# An empty `.changeset` directory does not answer that. After a
# version PR merges its changesets are already consumed, so a run
# whose publish fails outright — bad credentials, a build error, the
# npm 404 on an unbootstrapped package that stranded crates.io for
# nine days — still arrives here with PENDING=0. Inferring "released"
# from that would tag versions that never shipped.
#
# npm is the durable evidence, and each tag is gated on the version
# IT stands for: the root tag on whichever published package carries
# the root version (sync-versions.js keeps them aligned), the
# server-bin tag on @ifc-lite/server-bin itself. `published == true`
# short-circuits it — that IS this run publishing.
root_released=false
sb_released=false
if [ "${PUBLISHED}" = "true" ]; then
echo "npm published in this run"
root_released=true
sb_released=true
elif [ "${PENDING}" != "0" ]; then
echo "${PENDING} changeset(s) were pending — this run opened the version PR, nothing to tag"
else
ROOT_PKG=$(node -e '
const fs = require("fs");
const want = process.env.VERSION;
const hit = fs.readdirSync("packages")
.map((d) => { try { return JSON.parse(fs.readFileSync(`packages/${d}/package.json`, "utf8")); } catch { return null; } })
.find((p) => p && p.private !== true && p.version === want);
process.stdout.write(hit ? hit.name : "");
')
if [ -n "${ROOT_PKG}" ] && npm view "${ROOT_PKG}@${VERSION}" version > /dev/null 2>&1; then
echo "${ROOT_PKG}@${VERSION} is on npm — v${VERSION} is genuinely released"
root_released=true
else
echo "no published package carries v${VERSION} on npm — NOT tagging (publish likely failed)"
fi
if npm view "@ifc-lite/server-bin@${SB_VERSION}" version > /dev/null 2>&1; then
echo "@ifc-lite/server-bin@${SB_VERSION} is on npm"
sb_released=true
else
echo "@ifc-lite/server-bin@${SB_VERSION} is not on npm — NOT tagging it"
fi
fi
# Root and server-bin are tracked SEPARATELY. A run that created the
# root release and then failed must still be able to backfill the
# server-bin one, which a single shared flag would skip forever.
root=false
server=false
[ "$root_released" = "true" ] && { gh release view "v${VERSION}" > /dev/null 2>&1 || root=true; }
[ "$sb_released" = "true" ] && { gh release view "v${SB_VERSION}" > /dev/null 2>&1 || server=true; }
# When the two versions coincide, the ONE release must be created by
# the server-bin step, which uses the PAT. A GITHUB_TOKEN-created
# release emits no `release: published` event (GitHub recursion
# guard), so server-binaries.yml would never upload the archives and
# `npx @ifc-lite/server-bin` would 404 against an empty release.
if [ "${VERSION}" = "${SB_VERSION}" ] && [ "$server" = "true" ]; then
echo "root and server-bin versions coincide — letting the PAT-authored server-bin step create v${VERSION}"
root=false
fi
echo "run_root=${root}" >> $GITHUB_OUTPUT
echo "run_server=${server}" >> $GITHUB_OUTPUT
echo "root=${root} server=${server}"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Passed as env, not interpolated into the script: `${{ }}` expansion
# inside `run:` splices the value into the shell source itself
# (zizmor template-injection).
VERSION: ${{ steps.pre.outputs.version }}
SB_VERSION: ${{ steps.pre.outputs.sb_version }}
PENDING: ${{ steps.pre.outputs.pending }}
PUBLISHED: ${{ steps.changesets.outputs.published }}
- name: Get Version
id: get-version
if: steps.release-gate.outputs.run_root == 'true' || steps.release-gate.outputs.run_server == 'true'
run: |
# The pre-action root version — NOT a fresh read of package.json, which
# `changesets/action` may have bumped in this same tree.
echo "version=${VERSION}" >> $GITHUB_OUTPUT
env:
VERSION: ${{ steps.release-gate.outputs.version }}
- name: Create GitHub Release
if: steps.release-gate.outputs.run_root == 'true'
run: |
# Create the v* tag (changesets creates @scope/package@version tags, not v* tags)
#
# `|| true` on `git tag` is deliberate: a backfill or a re-run hits an
# existing tag and idempotency is the point. It is NOT on the push.
# A swallowed push is not "no release" -- `gh release create` CREATES a
# missing tag itself, and with no `--target` it does so from the latest
# state of the default branch (its own `--help` says so). So the failure
# mode was a green release pointing at a DIFFERENT commit than the
# packages published from it, which `packages/server-bin/src/binary.ts`
# then resolves its download URL against (#3202).
#
# The backfill path added after 2026-08-12 is the one most exposed to
# this: a backfill by definition runs after `main` has moved on.
#
# THE COMMIT IS NAMED EXPLICITLY (#3209). `git tag <name>` with no
# commit-ish tags the current checkout HEAD, and this job checks out
# with no `ref:` — so HEAD is the commit that TRIGGERED the run. Every
# push to `main` triggers a Release run, so a correct tag was only ever
# a coincidence: it held when the trigger happened to be the version
# commit. On v6.0.1 it did not, and the tag landed on an unrelated
# test-only commit fourteen minutes later, green and unreported.
#
# The resolver also REFUSES when the tag already exists on a different
# commit, which is why `|| true` below is still safe: by the time it
# runs, any existing tag has been proven to name this same commit. The
# `$( )` assignment carries the script's exit status under the Actions
# default shell (`bash --noprofile --norc -eo pipefail`), so a refusal
# kills the step before anything is pushed.
VERSION_COMMIT=$(node scripts/release-tag-commit.mjs package.json "${VERSION}" "v${VERSION}")
git tag "v${VERSION}" "${VERSION_COMMIT}" || true
git push origin "v${VERSION}"
# Create release, skipping if it already exists
if gh release view "v${VERSION}" > /dev/null 2>&1; then
echo "Release v${VERSION} already exists, skipping"
else
# --verify-tag: refuse to invent the tag. The push above is the only
# thing allowed to create it, and it is now unguarded, so a failure is
# loud where it happens instead of being reasoned about downstream.
gh release create "v${VERSION}" \
--verify-tag \
--title "v${VERSION}" \
--notes "See CHANGELOG.md for details"
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ steps.get-version.outputs.version }}
# @ifc-lite/server-bin downloads its native archive from a GitHub
# release tagged `v<its-own-version>` (packages/server-bin/src/binary.ts).
# The root `v<version>` release above is keyed to the *highest* workspace
# package version, which is usually NOT server-bin's — so without a
# matching tag `npx @ifc-lite/server-bin` 404s on download, and native
# binaries never carry geometry fixes that bump only server-bin/wasm.
# Create a release at server-bin's own version: that `release: published`
# event triggers server-binaries.yml to build + upload the per-platform
# archives to this tag, so the download URL resolves with fresh binaries.
- name: Create server-bin binary release
if: steps.release-gate.outputs.run_server == 'true'
run: |
# SB_VERSION arrives via env (see below), not spliced into this script.
TAG="v${SB_VERSION}"
# Skip when the tag already exists (server-bin unchanged this cycle,
# or it happens to equal the root release already created above).
if gh release view "$TAG" > /dev/null 2>&1; then
echo "Release $TAG already exists, skipping (server binaries already published)"
else
# Same shape as the root release above: idempotent tag, UNGUARDED
# push, and `--verify-tag` so `gh` cannot invent the ref at
# default-branch head if the push failed (#3202). Getting this wrong
# here is worse than above -- server-binaries.yml uploads the
# per-platform archives to whatever commit this tag names.
#
# Resolved from server-bin's OWN manifest, independently of the root
# version: the two are usually different versions and therefore
# different commits. `v1.16.7` is the standing proof — it points at
# db35b4a6, 35 commits after 44c4201e where server-bin's version was
# actually set, and both commits report 1.16.7, so nothing noticed
# for nine days (#3209). The resolver refuses rather than reusing a
# tag that names a different commit, which turns that damage into a
# red job the next time a run touches it.
SB_COMMIT=$(node scripts/release-tag-commit.mjs packages/server-bin/package.json "${SB_VERSION}" "$TAG")
git tag "$TAG" "${SB_COMMIT}" || true
git push origin "$TAG"
gh release create "$TAG" \
--verify-tag \
--title "$TAG" \
--notes "Native server binary for @ifc-lite/server-bin v${SB_VERSION}. See CHANGELOG.md."
fi
env:
# Use the fine-grained PAT, NOT GITHUB_TOKEN: a release created with
# GITHUB_TOKEN does NOT emit a `release: published` event that starts new
# workflow runs (GitHub recursion guard). server-binaries.yml uploads
# binaries only on `release: published`, so a GITHUB_TOKEN-created release
# would sit empty and downloads would still 404. The PAT is a real
# identity, so the event fires and binaries build + upload to this tag.
GITHUB_TOKEN: ${{ secrets.RELEASE_PAT }}
SB_VERSION: ${{ steps.release-gate.outputs.sb_version }}
# ── Publish verification ────────────────────────────────────────────────
#
# Verify every published package/crate is actually reachable on its
# registry. Catches a partial publish before users hit it.
#
# THE GATE, and why it is not `published`:
#
# These used to be gated `if: always() && needs.release.outputs.published
# == 'true'`. That asks "did THIS RUN publish something", which is false on
# the re-run that RECOVERS a partial publish — npm has nothing left to
# publish by then. #3181: on v6.0.0, attempt 2 was the attempt that fixed
# the partial publish, and it is the attempt the gate skipped. A verifier
# that cannot run on a recovery is not watching the case it exists for.
#
# `needs.release.outputs.verify` asks the question that survives a re-run
# instead: "is this commit a version bump, or did this run publish?" Both
# verifiers re-derive the expected versions from package.json / Cargo.toml
# at the checked-out commit, so they never needed to know whether THIS run
# is what published them.
#
# `always()` is the other load-bearing half. A failed `needs:` skips the
# dependent whatever the condition says, so the job that exists to confirm
# packages reached the registry could not run on the case it exists for: a
# PARTIAL publish. On 2026-08-14 nine packages went out, @ifc-lite/
# source-dalux 404'd, the release job failed — and this was skipped, so
# nothing named the missing package. It was found by hand afterwards.
#
# These stay JOBS IN THIS RUN rather than a separate `workflow_run`
# workflow. #3181's original report argued for moving them out because
# attempt 1's verifier had been "starved of a runner by the tag-push
# flood"; that was measured afterwards and is false — it queued 7m46s, ran,
# and passed, one second before the attempt was cancelled by hand. The
# remaining structural argument does not hold either: `concurrency:` above
# has no `cancel-in-progress`, which defaults to false, so a newer push
# QUEUES behind this run rather than cancelling it — GitHub never cancels
# these jobs with their parent. And a separate workflow queues into the
# same account-level runner pool, so being separate would confer no
# scheduling priority in any case. Staying in-run keeps the verdict on the
# Release run itself, keeps the YAML and `scripts/` at one commit, and
# cannot fire on the ~50 pushes a day that publish nothing.
verify-npm-publish:
name: Verify npm publish
needs: release
if: always() && needs.release.outputs.verify == 'true'
runs-on: ubuntu-latest
# Bounded on purpose. The retry budget below is per PACKAGE and the walk
# is sequential: 40-odd packages x 11 waits x 20s is over two hours with
# npm down, and without this the job would hold a runner for the 6-hour
# default before saying so. The cap has to clear the case this job exists
# for, though — a PARTIAL publish, where the missing packages each burn
# the full ~3.7-minute budget and the run must survive long enough to name
# them — so it is set to allow roughly a dozen missing packages rather
# than to the tightest value that fits a healthy run.
timeout-minutes: 60
steps:
- name: Checkout Repo
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
lfs: false
persist-credentials: false
- name: Setup pnpm
# v6 reads the version from the `packageManager` field in
# package.json (`pnpm@10.8.1`); specifying `with.version` here
# would conflict and the action errors with ERR_PNPM_BAD_PM_VERSION.
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
registry-url: 'https://registry.npmjs.org'
- name: Verify all packages on npm
# New packages can take longer to appear in npm metadata after publish.
# Retry for up to ~4 minutes before failing.
run: node scripts/verify-npm-publish.js --retries 12 --delay 20000
# npm completing tells you NOTHING about crates.io — they are two
# independent registries published by `release-all.mjs`. #3181: the v6.0.0
# release published all 34 npm packages while only 3 of 7 crates reached
# crates.io (#3180), and there was no crates.io-side check at all to catch
# it. That absence, not any scheduling problem, is why the half-publish
# went unnoticed.
verify-crates-publish:
name: Verify crates.io publish
needs: release
if: always() && needs.release.outputs.verify == 'true'
runs-on: ubuntu-latest
# Same reasoning as the npm job: 7 crates x 11 waits x 20s is ~26 minutes
# of pure waiting during a registry outage, plus an artifact download per
# crate. Cap it rather than holding a runner for the 6-hour default.
timeout-minutes: 35
steps:
- name: Checkout Repo
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
lfs: false
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
# Deliberately the same 22 as the npm verify job above, not the 24
# the `release` job needs. 24 is there for npm ≥ 11.5.1's OIDC
# trusted publishing; a verifier publishes nothing, so it has no
# such requirement and the two verify lanes are better kept on one
# runtime than silently drifting apart.
node-version: 22
registry-url: 'https://registry.npmjs.org'
- name: Verify all crates on crates.io
# Crates.io propagation is normally fast, but give it the same
# ~4-minute retry budget as the npm check for a slow moment.
run: node scripts/verify-crates-publish.js --retries 12 --delay 20000