From 93a053eabb55e1d5479b78d6e0669a4bfd51fff0 Mon Sep 17 00:00:00 2001 From: Michael Turner Date: Wed, 29 Jul 2026 10:30:20 -0500 Subject: [PATCH 1/8] consolidate npm publishing into npm-publish.yml release.yml no longer publishes to npm directly; after the GitHub release is created it dispatches npm-publish.yml, which publishes from the release assets. A single publishing workflow file is a prerequisite for npm trusted publishing, whose per-package publisher config matches on workflow filename. --- .github/workflows/npm-publish.yml | 11 ++++--- .github/workflows/release.yml | 49 ++++++------------------------- 2 files changed, 16 insertions(+), 44 deletions(-) diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 3ad720f..66757b4 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -1,9 +1,12 @@ name: Publish to npm -# Recovery/backfill path: publishes the npm packages from the binaries already -# attached to an existing GitHub release, without cutting a new release. The -# normal path is the npm job in release.yml; use this one when that job failed -# partway, or to publish npm packages for a release that predates them. +# The single npm publishing path: publishes the npm packages from the binaries +# attached to an existing GitHub release, without cutting a new release. +# Normally dispatched by release.yml after it publishes the GitHub release; +# run it manually to recover from a partial publish or to backfill npm +# packages for a release that predates them. Keeping all publishing in one +# workflow file lets npm's trusted publisher config (one per package, matched +# by workflow filename) cover every publish. on: workflow_dispatch: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 72f1009..3b84dd6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -173,46 +173,15 @@ jobs: npm: name: Publish to npm - needs: [prepare, build] + needs: [prepare, release] runs-on: ubuntu-latest + permissions: + actions: write steps: - - uses: actions/checkout@v4 - with: - ref: ${{ needs.prepare.outputs.tag }} - - - uses: actions/setup-node@v4 - with: - node-version: "20" - registry-url: "https://registry.npmjs.org" - - - name: Download build artifacts - uses: actions/download-artifact@v4 - with: - pattern: release-* - path: artifacts - - - name: Extract binaries by target - run: | - # artifacts/release-/aleo-devnode--.zip - # -> bins//aleo-devnode[.exe] - for dir in artifacts/release-*; do - target="${dir#artifacts/release-}" - mkdir -p "bins/$target" - unzip -o "$dir"/*.zip -d "bins/$target" - done - - - name: Build npm packages - run: node scripts/build-npm.mjs --version "${{ needs.prepare.outputs.version }}" --artifacts bins --out dist-npm - - - name: Publish - run: | - set -e - # Platform packages first, then the main launcher last, so main's - # exact-pinned optionalDependencies always resolve on install. - for pkg in dist-npm/*/; do - [ "$pkg" = "dist-npm/main/" ] && continue - npm publish "$pkg" --access public - done - npm publish dist-npm/main/ --access public + # All npm publishing lives in npm-publish.yml so npm's trusted publisher + # config can point at a single workflow file. It publishes from the + # release assets, so it must run after the GitHub release exists. + - name: Dispatch npm publish workflow + run: gh workflow run npm-publish.yml -f tag="${{ needs.prepare.outputs.tag }}" -R "${{ github.repository }}" env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + GH_TOKEN: ${{ github.token }} From 91048225f77803eb96fa866a21b31cf9d51156ff Mon Sep 17 00:00:00 2001 From: Michael Turner Date: Wed, 29 Jul 2026 10:53:42 -0500 Subject: [PATCH 2/8] make npm publish fail the release run, publish from the tag, and retry safely - release.yml now watches the dispatched npm-publish run to completion and propagates its result, so a failed npm publish fails the release run. - npm-publish.yml checks out the release tag instead of the default branch: the tag's tree is what gets published. This requires the tag to contain the npm packaging scripts. - Retrying a version older than the current npm latest publishes under a temporary dist-tag so it cannot steal the "latest" dist-tag from a newer release. --- .github/workflows/npm-publish.yml | 31 +++++++++++++++++++++++-------- .github/workflows/release.yml | 22 ++++++++++++++++++++-- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 66757b4..2a9b7c2 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -1,10 +1,12 @@ name: Publish to npm -# The single npm publishing path: publishes the npm packages from the binaries -# attached to an existing GitHub release, without cutting a new release. -# Normally dispatched by release.yml after it publishes the GitHub release; -# run it manually to recover from a partial publish or to backfill npm -# packages for a release that predates them. Keeping all publishing in one +# The single npm publishing path: publishes the npm packages for an existing +# GitHub release from the binaries attached to it, without cutting a new +# release. Normally dispatched by release.yml after it publishes the GitHub +# release; run it manually against the same tag to retry a partial or failed +# publish — already-published packages are skipped, so a retry only fills in +# what is missing, and retrying a version older than the current npm latest +# will not steal the `latest` dist-tag. Keeping all publishing in one # workflow file lets npm's trusted publisher config (one per package, matched # by workflow filename) cover every publish. @@ -23,9 +25,12 @@ jobs: name: Publish to npm runs-on: ubuntu-latest steps: - # Deliberately the default branch, not the tag: the npm packaging - # scripts must be present regardless of what the tag's tree contains. + # The tag's tree is what gets published: packaging scripts and package + # metadata come from the tag itself, so a republish is reproducible. + # Consequence: this only works for tags that contain scripts/build-npm.mjs. - uses: actions/checkout@v4 + with: + ref: ${{ env.TAG }} - uses: actions/setup-node@v4 with: @@ -60,10 +65,20 @@ jobs: # so main's exact-pinned optionalDependencies always resolve on # install. publish() { - local name + local name latest name="$(node -p "require('./$1/package.json').name")" if npm view "${name}@${VERSION}" version >/dev/null 2>&1; then echo "${name}@${VERSION} already published, skipping" + return + fi + # When retrying a version older than what is already on npm, + # publish under a temporary dist-tag so `latest` keeps pointing + # at the newer version, then drop the temporary tag. + latest="$(npm view "$name" dist-tags.latest 2>/dev/null || true)" + if [ -n "$latest" ] && [ "$(printf '%s\n%s\n' "$latest" "$VERSION" | sort -V | tail -n1)" != "$VERSION" ]; then + npm publish "$1" --access public --tag backfill + npm dist-tag rm "$name" backfill \ + || echo "::warning::failed to remove temporary dist-tag 'backfill' from ${name}" else npm publish "$1" --access public fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3b84dd6..93cdbaf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -181,7 +181,25 @@ jobs: # All npm publishing lives in npm-publish.yml so npm's trusted publisher # config can point at a single workflow file. It publishes from the # release assets, so it must run after the GitHub release exists. - - name: Dispatch npm publish workflow - run: gh workflow run npm-publish.yml -f tag="${{ needs.prepare.outputs.tag }}" -R "${{ github.repository }}" + # `gh workflow run` is fire-and-forget and does not report the run it + # created, so poll for that run and watch it to completion — a failed + # npm publish must fail this job (and the release run) too. + - name: Run npm publish workflow + run: | + DISPATCHED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + gh workflow run npm-publish.yml -f tag="${{ needs.prepare.outputs.tag }}" -R "${{ github.repository }}" + RUN_ID="" + for _ in 1 2 3 4 5 6 7 8 9 10 11 12; do + sleep 5 + RUN_ID="$(gh run list -R "${{ github.repository }}" --workflow=npm-publish.yml \ + --event=workflow_dispatch --created ">=$DISPATCHED_AT" \ + --json databaseId --jq '.[0].databaseId // empty')" + [ -n "$RUN_ID" ] && break + done + if [ -z "$RUN_ID" ]; then + echo "::error::Dispatched npm-publish.yml but could not find the run it created" + exit 1 + fi + gh run watch "$RUN_ID" -R "${{ github.repository }}" --exit-status env: GH_TOKEN: ${{ github.token }} From 953b00bbfd0f88cfc25cd84989b7d5f157f04a1c Mon Sep 17 00:00:00 2001 From: Michael Turner Date: Wed, 29 Jul 2026 11:20:34 -0500 Subject: [PATCH 3/8] address review findings: clock skew, prerelease semver, shell interpolation - Backdate the dispatch timestamp by a minute so clock skew between the runner and GitHub cannot hide the dispatched run from the --created filter, which would fail the release while the publish proceeds. - Compare versions with real semver (npx semver) instead of sort -V, which ranks prereleases above their release (0.3.0-rc.1 > 0.3.0) and would let an RC keep the "latest" dist-tag. - Pass the tag and repository into the dispatch script via env instead of expression interpolation; tag names may contain shell metacharacters. --- .github/workflows/npm-publish.yml | 6 ++++-- .github/workflows/release.yml | 14 ++++++++++---- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 2a9b7c2..1c958d8 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -73,9 +73,11 @@ jobs: fi # When retrying a version older than what is already on npm, # publish under a temporary dist-tag so `latest` keeps pointing - # at the newer version, then drop the temporary tag. + # at the newer version, then drop the temporary tag. Compared + # with real semver (not sort -V), which ranks prereleases below + # their release: 0.3.0-rc.1 < 0.3.0. latest="$(npm view "$name" dist-tags.latest 2>/dev/null || true)" - if [ -n "$latest" ] && [ "$(printf '%s\n%s\n' "$latest" "$VERSION" | sort -V | tail -n1)" != "$VERSION" ]; then + if [ -n "$latest" ] && [ "$(npx --yes semver "$latest" "$VERSION" | tail -n1)" != "$VERSION" ]; then npm publish "$1" --access public --tag backfill npm dist-tag rm "$name" backfill \ || echo "::warning::failed to remove temporary dist-tag 'backfill' from ${name}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 93cdbaf..9063070 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -186,12 +186,14 @@ jobs: # npm publish must fail this job (and the release run) too. - name: Run npm publish workflow run: | - DISPATCHED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)" - gh workflow run npm-publish.yml -f tag="${{ needs.prepare.outputs.tag }}" -R "${{ github.repository }}" + # Backdated a minute so clock skew between this runner and GitHub's + # createdAt cannot hide the run from the --created filter below. + DISPATCHED_AT="$(date -u -d '1 minute ago' +%Y-%m-%dT%H:%M:%SZ)" + gh workflow run npm-publish.yml -f tag="$TAG" -R "$REPO" RUN_ID="" for _ in 1 2 3 4 5 6 7 8 9 10 11 12; do sleep 5 - RUN_ID="$(gh run list -R "${{ github.repository }}" --workflow=npm-publish.yml \ + RUN_ID="$(gh run list -R "$REPO" --workflow=npm-publish.yml \ --event=workflow_dispatch --created ">=$DISPATCHED_AT" \ --json databaseId --jq '.[0].databaseId // empty')" [ -n "$RUN_ID" ] && break @@ -200,6 +202,10 @@ jobs: echo "::error::Dispatched npm-publish.yml but could not find the run it created" exit 1 fi - gh run watch "$RUN_ID" -R "${{ github.repository }}" --exit-status + gh run watch "$RUN_ID" -R "$REPO" --exit-status env: GH_TOKEN: ${{ github.token }} + # Passed via env, not ${{ }} interpolation into the script: tag + # names may contain shell metacharacters. + TAG: ${{ needs.prepare.outputs.tag }} + REPO: ${{ github.repository }} From 7be58e36a89198a3173394aa383dc7e0e7c531c8 Mon Sep 17 00:00:00 2001 From: Michael Turner Date: Wed, 29 Jul 2026 11:23:44 -0500 Subject: [PATCH 4/8] identify the dispatched npm-publish run by id instead of timestamp guessing npm-publish.yml accepts an optional dispatch_id input and embeds it in its run name; release.yml passes its own run id and polls for the run whose name carries it. Discovery of the dispatched run is now exact: immune to clock skew and to concurrent releases matching each other's runs. The discovery window is also widened from 1 to 2 minutes to ride out API propagation delays. --- .github/workflows/npm-publish.yml | 9 +++++++++ .github/workflows/release.yml | 13 ++++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 1c958d8..f7cc16e 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -16,6 +16,15 @@ on: tag: description: "Existing GitHub release tag whose binaries to publish (e.g. v0.2.1)" required: true + dispatch_id: + description: "Opaque id embedded in the run name so a dispatching workflow can find this exact run (leave empty for manual runs)" + required: false + default: "" + +# The dispatch_id in the run name is what release.yml greps for to identify +# the run it dispatched — do not reword the "dispatch " marker without +# updating the matching filter there. +run-name: "Publish to npm (${{ inputs.tag }}${{ inputs.dispatch_id != '' && format(', dispatch {0}', inputs.dispatch_id) || '' }})" env: TAG: ${{ github.event.inputs.tag }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9063070..2c0f9bd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -182,20 +182,23 @@ jobs: # config can point at a single workflow file. It publishes from the # release assets, so it must run after the GitHub release exists. # `gh workflow run` is fire-and-forget and does not report the run it - # created, so poll for that run and watch it to completion — a failed - # npm publish must fail this job (and the release run) too. + # created, so this run's id is embedded in the dispatched run's name + # ("dispatch ") and polled for, then the run is watched to + # completion — a failed npm publish must fail this job (and the + # release run) too. - name: Run npm publish workflow run: | # Backdated a minute so clock skew between this runner and GitHub's # createdAt cannot hide the run from the --created filter below. DISPATCHED_AT="$(date -u -d '1 minute ago' +%Y-%m-%dT%H:%M:%SZ)" - gh workflow run npm-publish.yml -f tag="$TAG" -R "$REPO" + gh workflow run npm-publish.yml -f tag="$TAG" -f dispatch_id="$GITHUB_RUN_ID" -R "$REPO" RUN_ID="" - for _ in 1 2 3 4 5 6 7 8 9 10 11 12; do + for _ in $(seq 1 24); do sleep 5 RUN_ID="$(gh run list -R "$REPO" --workflow=npm-publish.yml \ --event=workflow_dispatch --created ">=$DISPATCHED_AT" \ - --json databaseId --jq '.[0].databaseId // empty')" + --json databaseId,displayTitle \ + --jq "[.[] | select(.displayTitle | contains(\"dispatch $GITHUB_RUN_ID\"))][0].databaseId // empty")" [ -n "$RUN_ID" ] && break done if [ -z "$RUN_ID" ]; then From 8fa4edece11a552463e6d5fcf68df06add4cd581 Mon Sep 17 00:00:00 2001 From: Michael Turner Date: Wed, 29 Jul 2026 11:44:58 -0500 Subject: [PATCH 5/8] restrict npm publishing to stable tags and never publish backwards - npm-publish.yml fails immediately for any tag that is not exactly vX.Y.Z; prerelease and other tag shapes must be published manually. release.yml skips the dispatch for such tags so they still get a GitHub release without failing the run. - Per package, publish only when the version is strictly newer than the package's current npm latest; skip otherwise. This replaces the temporary "backfill" dist-tag mechanism: the workflow can now never move `latest` backwards, at the cost that filling version gaps older than latest is a manual operation. --- .github/workflows/npm-publish.yml | 39 ++++++++++++++++++------------- .github/workflows/release.yml | 7 ++++++ 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index f7cc16e..40c1f68 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -4,11 +4,13 @@ name: Publish to npm # GitHub release from the binaries attached to it, without cutting a new # release. Normally dispatched by release.yml after it publishes the GitHub # release; run it manually against the same tag to retry a partial or failed -# publish — already-published packages are skipped, so a retry only fills in -# what is missing, and retrying a version older than the current npm latest -# will not steal the `latest` dist-tag. Keeping all publishing in one -# workflow file lets npm's trusted publisher config (one per package, matched -# by workflow filename) cover every publish. +# publish. Only stable vX.Y.Z tags are accepted — prereleases and other tag +# shapes must be published manually. Per package, the version is published +# only if it is strictly newer than the package's current npm `latest`; +# anything already published or older is skipped, so the workflow can never +# move `latest` backwards and a retry only fills in what is missing. Keeping +# all publishing in one workflow file lets npm's trusted publisher config +# (one per package, matched by workflow filename) cover every publish. on: workflow_dispatch: @@ -34,6 +36,13 @@ jobs: name: Publish to npm runs-on: ubuntu-latest steps: + - name: Require stable vX.Y.Z tag + run: | + if ! printf '%s' "$TAG" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "::error::Tag '$TAG' is not a stable vX.Y.Z release tag. Prerelease and other tags must be published manually." + exit 1 + fi + # The tag's tree is what gets published: packaging scripts and package # metadata come from the tag itself, so a republish is reproducible. # Consequence: this only works for tags that contain scripts/build-npm.mjs. @@ -80,19 +89,17 @@ jobs: echo "${name}@${VERSION} already published, skipping" return fi - # When retrying a version older than what is already on npm, - # publish under a temporary dist-tag so `latest` keeps pointing - # at the newer version, then drop the temporary tag. Compared - # with real semver (not sort -V), which ranks prereleases below - # their release: 0.3.0-rc.1 < 0.3.0. + # Never publish backwards: unless this version is strictly newer + # than the package's current npm latest, skip it — publishing + # would move the `latest` dist-tag onto an older version. Real + # semver comparison (not sort -V), so a manually published + # prerelease latest ranks below its release: 0.3.0-rc.1 < 0.3.0. latest="$(npm view "$name" dist-tags.latest 2>/dev/null || true)" - if [ -n "$latest" ] && [ "$(npx --yes semver "$latest" "$VERSION" | tail -n1)" != "$VERSION" ]; then - npm publish "$1" --access public --tag backfill - npm dist-tag rm "$name" backfill \ - || echo "::warning::failed to remove temporary dist-tag 'backfill' from ${name}" - else - npm publish "$1" --access public + if [ -n "$latest" ] && ! npx --yes semver -r ">${latest}" "$VERSION" >/dev/null 2>&1; then + echo "${name}: npm latest (${latest}) >= ${VERSION}, skipping" + return fi + npm publish "$1" --access public } for pkg in dist-npm/*/; do [ "$pkg" = "dist-npm/main/" ] && continue diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2c0f9bd..9f07042 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -188,6 +188,13 @@ jobs: # release run) too. - name: Run npm publish workflow run: | + # npm publishing is for stable releases only; npm-publish.yml + # rejects other tags, so don't dispatch it for them. Prerelease + # tags still get a GitHub release, just no npm packages. + if ! printf '%s' "$TAG" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "Tag '$TAG' is not a stable vX.Y.Z release tag; skipping npm publish." + exit 0 + fi # Backdated a minute so clock skew between this runner and GitHub's # createdAt cannot hide the run from the --created filter below. DISPATCHED_AT="$(date -u -d '1 minute ago' +%Y-%m-%dT%H:%M:%SZ)" From 75734a53ee0640f21b45177ea186430056a6344a Mon Sep 17 00:00:00 2001 From: Michael Turner Date: Wed, 29 Jul 2026 11:54:28 -0500 Subject: [PATCH 6/8] only the newest stable tag may be published to npm The per-package guard compared against each package's own npm latest, so retrying an old tag could still "forward-publish" a package that had missed several releases, leaving cross-package version gaps (a launcher whose exact-pinned platform packages do not exist). The workflow now refuses any tag that is not the repository's newest stable vX.Y.Z tag: retrying the newest tag fills in exactly the packages a partial run missed, and anything older is a manual operation. --- .github/workflows/npm-publish.yml | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 40c1f68..aab548d 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -4,13 +4,14 @@ name: Publish to npm # GitHub release from the binaries attached to it, without cutting a new # release. Normally dispatched by release.yml after it publishes the GitHub # release; run it manually against the same tag to retry a partial or failed -# publish. Only stable vX.Y.Z tags are accepted — prereleases and other tag -# shapes must be published manually. Per package, the version is published -# only if it is strictly newer than the package's current npm `latest`; -# anything already published or older is skipped, so the workflow can never -# move `latest` backwards and a retry only fills in what is missing. Keeping -# all publishing in one workflow file lets npm's trusted publisher config -# (one per package, matched by workflow filename) cover every publish. +# publish. Only the repository's newest stable vX.Y.Z tag is accepted — +# prereleases, malformed tags, and older releases are refused and must be +# published manually. Per package, already-published versions are skipped +# and the version must be strictly newer than the package's current npm +# `latest`, so a retry publishes exactly the packages a partial earlier run +# missed and the workflow can never move `latest` backwards. Keeping all +# publishing in one workflow file lets npm's trusted publisher config (one +# per package, matched by workflow filename) cover every publish. on: workflow_dispatch: @@ -36,12 +37,24 @@ jobs: name: Publish to npm runs-on: ubuntu-latest steps: - - name: Require stable vX.Y.Z tag + - name: Require the newest stable vX.Y.Z tag run: | if ! printf '%s' "$TAG" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then echo "::error::Tag '$TAG' is not a stable vX.Y.Z release tag. Prerelease and other tags must be published manually." exit 1 fi + # Only the newest stable tag may be published: retrying it fills in + # packages a partial earlier run missed, but older tags are refused + # outright — backpublishing is a manual operation. sort -V is safe + # here because the grep leaves only plain X.Y.Z versions. + NEWEST="$(gh api "repos/${GITHUB_REPOSITORY}/tags" --paginate --jq '.[].name' \ + | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n1)" + if [ "$TAG" != "$NEWEST" ]; then + echo "::error::Tag '$TAG' is not the newest stable release tag ('$NEWEST'). Backpublishing older releases must be done manually." + exit 1 + fi + env: + GH_TOKEN: ${{ github.token }} # The tag's tree is what gets published: packaging scripts and package # metadata come from the tag itself, so a republish is reproducible. From 99682f3ca4cb0d73be03c4bd2e6eea3f3ff7d79e Mon Sep 17 00:00:00 2001 From: Michael Turner Date: Wed, 29 Jul 2026 12:01:26 -0500 Subject: [PATCH 7/8] preinstall semver and match the dispatch id exactly - Install the semver CLI in its own step instead of npx-on-demand in the publish loop: a transient fetch failure there was indistinguishable from "version not newer" and would silently skip publishing a package while the run stayed green. As a dedicated step, a fetch failure fails the run loudly. - Match the dispatched run by "dispatch )" including the closing paren, so a run id that is a string-prefix of a concurrent release's run id cannot match the wrong run. --- .github/workflows/npm-publish.yml | 14 +++++++++++--- .github/workflows/release.yml | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index aab548d..b3e511f 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -25,8 +25,9 @@ on: default: "" # The dispatch_id in the run name is what release.yml greps for to identify -# the run it dispatched — do not reword the "dispatch " marker without -# updating the matching filter there. +# the run it dispatched — it matches "dispatch )" including the closing +# paren, so the id must stay immediately before the final ")". Do not reword +# without updating the matching filter there. run-name: "Publish to npm (${{ inputs.tag }}${{ inputs.dispatch_id != '' && format(', dispatch {0}', inputs.dispatch_id) || '' }})" env: @@ -68,6 +69,13 @@ jobs: node-version: "20" registry-url: "https://registry.npmjs.org" + # Installed up front so a fetch failure is a loud step failure. If the + # publish loop fetched it via npx on demand, a transient network error + # would be indistinguishable from "version not newer" and would + # silently skip publishing a package. + - name: Install semver CLI + run: npm install -g semver + - name: Download release assets run: gh release download "$TAG" --pattern '*.zip' --dir artifacts env: @@ -108,7 +116,7 @@ jobs: # semver comparison (not sort -V), so a manually published # prerelease latest ranks below its release: 0.3.0-rc.1 < 0.3.0. latest="$(npm view "$name" dist-tags.latest 2>/dev/null || true)" - if [ -n "$latest" ] && ! npx --yes semver -r ">${latest}" "$VERSION" >/dev/null 2>&1; then + if [ -n "$latest" ] && ! semver -r ">${latest}" "$VERSION" >/dev/null; then echo "${name}: npm latest (${latest}) >= ${VERSION}, skipping" return fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9f07042..2f45fd9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -205,7 +205,7 @@ jobs: RUN_ID="$(gh run list -R "$REPO" --workflow=npm-publish.yml \ --event=workflow_dispatch --created ">=$DISPATCHED_AT" \ --json databaseId,displayTitle \ - --jq "[.[] | select(.displayTitle | contains(\"dispatch $GITHUB_RUN_ID\"))][0].databaseId // empty")" + --jq "[.[] | select(.displayTitle | contains(\"dispatch ${GITHUB_RUN_ID})\"))][0].databaseId // empty")" [ -n "$RUN_ID" ] && break done if [ -z "$RUN_ID" ]; then From 4e697dcfed4dd87568613b88e6d1cb308eb401ca Mon Sep 17 00:00:00 2001 From: Michael Turner Date: Wed, 29 Jul 2026 12:05:40 -0500 Subject: [PATCH 8/8] pin the semver CLI to an exact version The publish job is the sole holder of npm publish rights for the @provablehq packages, and semver is the only third-party code it fetches at run time. Pinning to an exact, immutable version closes off behavioral drift and package-takeover via a future semver release. --- .github/workflows/npm-publish.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index b3e511f..81e61ad 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -72,9 +72,12 @@ jobs: # Installed up front so a fetch failure is a loud step failure. If the # publish loop fetched it via npx on demand, a transient network error # would be indistinguishable from "version not newer" and would - # silently skip publishing a package. + # silently skip publishing a package. Pinned to an exact version: this + # is the only third-party code fetched into the job that holds publish + # rights for the @provablehq packages, and npm versions are immutable, + # so a pin closes off both drift and package-takeover of new releases. - name: Install semver CLI - run: npm install -g semver + run: npm install -g semver@7.8.5 - name: Download release assets run: gh release download "$TAG" --pattern '*.zip' --dir artifacts