From 712440456f4be107c2881dc5d387d1ed4ce21e97 Mon Sep 17 00:00:00 2001 From: "alban.pasquelin" Date: Wed, 2 Sep 2026 13:08:30 +0200 Subject: [PATCH] =?UTF-8?q?Un=20tag=20pouss=C3=A9=20deux=20fois=20ne=20pub?= =?UTF-8?q?lie=20plus=20qu'une=20fois?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le tag v0.3.3 est parti deux fois : deux runs, même SHA, même seconde. Le premier a publié, le second a rejoué toute la chaîne pour se heurter au registre — « cannot publish over the previously published versions » — et signaler en rouge une release qui avait réussi. Un job `guard` demande maintenant à npm si la version existe déjà, et le job `publish` ne démarre que sinon. Un job séparé plutôt qu'une condition posée sur chaque étape : le doublon se lit alors comme ignoré et non comme cassé, et une notification d'échec qui ment finit par apprendre à ignorer la prochaine qui dit vrai. Le test porte sur la sortie de `npm view`, pas sur son code de retour : celui-ci vaut E404 aussi bien pour une version absente que pour un paquet qui n'existe pas encore, le jour de la première publication. Au passage, les actions rejoignent celles de `ci.yml` et `pages.yml` — v7, et la version de Node lue dans `.nvmrc` au lieu d'un 22 écrit en dur qui pouvait diverger. `id-token: write` descend au job qui publie ; le garde ne lit que le dépôt. Claude-Session: https://claude.ai/code/session_01N9dogM3o4ejFdnYtDjPQkb --- .github/workflows/release.yml | 72 +++++++++++++++++++++++++++-------- 1 file changed, 57 insertions(+), 15 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 38f4966..61869ac 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,17 +15,69 @@ on: permissions: contents: read - id-token: write # required for `npm publish --provenance` +# Two pushes of the same tag produce two runs. `cancel-in-progress: false` queues the second +# rather than killing a publish half-written to the registry — the right trade, but it means the +# duplicate still runs. The `guard` job below is what makes that duplicate harmless. concurrency: group: release-${{ github.ref }} cancel-in-progress: false jobs: + # A cheap gate ahead of the real work: it decides whether there is anything to publish at all. + # Its own job, and not an `if:` on four steps, so a duplicate tag push reports as *skipped* + # instead of a failed run — a red notification for a release that already succeeded is noise + # that trains you to ignore the next real one. + guard: + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + publish: ${{ steps.check.outputs.publish }} + steps: + - uses: actions/checkout@v7 + + - uses: actions/setup-node@v7 + with: + node-version-file: .nvmrc + + - id: check + name: Check the tag, and whether npm already has this version + run: | + set -euo pipefail + + # The tag must match package.json, or we would publish a version that does not match + # the code the tag points at. This one is a hard error: the mismatch is a mistake. + TAG="${GITHUB_REF_NAME#v}" + NAME="$(node -p "require('./package.json').name")" + PKG="$(node -p "require('./package.json').version")" + if [ "$TAG" != "$PKG" ]; then + echo "::error::Tag v$TAG does not match package.json $PKG" + exit 1 + fi + + # `npm view pkg@version version` prints the version and exits 0 only when that exact + # version is published; on a missing version — or a package that does not exist yet, on + # the very first release — it exits non-zero. Hence the empty-output test rather than + # the exit code, and the `|| true` so `set -e` does not turn "not published" into a + # failed job. + PUBLISHED="$(npm view "$NAME@$PKG" version 2>/dev/null || true)" + if [ -n "$PUBLISHED" ]; then + echo "::notice::$NAME@$PKG is already on npm — nothing to publish." + echo "publish=false" >> "$GITHUB_OUTPUT" + else + echo "publish=true" >> "$GITHUB_OUTPUT" + fi + publish: + needs: guard + if: needs.guard.outputs.publish == 'true' runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + id-token: write # required for `npm publish --provenance` steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - uses: pnpm/action-setup@v6 @@ -33,12 +85,12 @@ jobs: # `${NODE_AUTH_TOKEN}`, a secret that does not exist here — npm then authenticates with the # literal placeholder instead of exchanging the OIDC token, and the registry answers 404 # "you do not have permission" after having happily signed the provenance. - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v7 with: - node-version: 22 + node-version-file: .nvmrc cache: pnpm - # OIDC trusted publishing needs npm >= 11.5.1; Node 22 ships npm 10. + # OIDC trusted publishing needs npm >= 11.5.1; the Node 22 pinned by .nvmrc ships npm 10. - name: Update npm (required for OIDC) run: npm install -g npm@latest @@ -50,16 +102,6 @@ jobs: - run: pnpm install --frozen-lockfile - # The tag must match package.json, or we would publish a version that does not match the - # code the tag points at. - - name: Check the tag matches package.json - run: | - TAG="${GITHUB_REF_NAME#v}" - PKG="$(node -p "require('./package.json').version")" - if [ "$TAG" != "$PKG" ]; then - echo "::error::Tag v$TAG does not match package.json $PKG"; exit 1 - fi - # The same barrier as CI, before anything leaves for the registry. - name: Validate run: pnpm validate