From 64fd88fb0807c6666f594f62b5f78c0e6b18b2dd Mon Sep 17 00:00:00 2001 From: galangel Date: Tue, 18 Aug 2026 15:22:52 +0300 Subject: [PATCH 1/2] fix(ci): gate publishing on the registry instead of a git diff The old check compared package.json at HEAD against HEAD~1. That made a version bump a one-shot trigger: if the publish that followed it failed, the bump was already consumed and no later push would ever retry it. Three versions were stranded that way, each by a different publish failure: 1.1.1 bumped in #16, publish failed on an expired npm token 1.2.0 bumped in #15, publish failed on an expired npm token 1.2.1 bumped in #18, publish failed on npm 12 requiring Node >= 22 Ask the registry instead: GET registry.npmjs.org// and read the status. 200 means published, so skip; 404 means it is not there, so publish; anything else fails the job rather than guessing. This is idempotent, so a failed publish just retries on the next push. Consequences: - workflow_dispatch no longer needs to bypass the gate. Bypassing would now mean attempting to republish an existing version, which npm rejects anyway. Dispatch is still useful for retrying without pushing. - fetch-depth: 2 is gone; no history is needed. - Outputs renamed to say what they mean: version_changed -> should_publish, new_version -> version. Verified by running the step's script locally against the live registry: 1.2.1 returns 200 and yields should_publish=false, a synthetic 9.9.9 returns 404 and yields should_publish=true, and the scope is URL-encoded correctly as @galangel%2freact-tip-magic. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/publish.yml | 66 +++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 30 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1d0ece5..4450e0a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -3,7 +3,7 @@ name: Publish to npm on: push: branches: [main] - # Lets a failed publish be retried without inventing a new version number. + # Lets a failed publish be retried without pushing anything. workflow_dispatch: concurrency: @@ -15,44 +15,50 @@ jobs: name: Check Version Change runs-on: ubuntu-latest outputs: - version_changed: ${{ steps.version.outputs.changed }} - new_version: ${{ steps.version.outputs.version }} + should_publish: ${{ steps.version.outputs.should_publish }} + version: ${{ steps.version.outputs.version }} steps: - name: Checkout repository uses: actions/checkout@v4 - with: - fetch-depth: 2 - - name: Check for version change + # Asks the registry whether this exact version exists, instead of diffing + # package.json against HEAD~1. The old diff treated a version bump as a + # one-shot trigger: when the publish that followed it failed, the bump was + # spent and no later push would retry it. That stranded 1.1.1, 1.2.0 and + # 1.2.1 unpublished. This check is idempotent, so a failed publish simply + # retries on the next push. + - name: Check whether this version is already published id: version run: | - # Get current version from package.json - CURRENT_VERSION=$(node -p "require('./package.json').version") - - # Get previous version from parent commit - git checkout HEAD~1 -- package.json 2>/dev/null || true - PREVIOUS_VERSION=$(node -p "require('./package.json').version" 2>/dev/null || echo "0.0.0") - git checkout HEAD -- package.json - - echo "Current version: $CURRENT_VERSION" - echo "Previous version: $PREVIOUS_VERSION" - - echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT - - if [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then - echo "Version changed from $PREVIOUS_VERSION to $CURRENT_VERSION" - echo "changed=true" >> $GITHUB_OUTPUT - else - echo "Version unchanged" - echo "changed=false" >> $GITHUB_OUTPUT - fi + NAME=$(node -p "require('./package.json').name") + VERSION=$(node -p "require('./package.json').version") + echo "version=$VERSION" >> $GITHUB_OUTPUT + + URL="https://registry.npmjs.org/${NAME//\//%2f}/$VERSION" + CODE=$(curl -sS -o /dev/null -w '%{http_code}' "$URL") + echo "GET $URL -> $CODE" + + case "$CODE" in + 200) + echo "$NAME@$VERSION is already published; nothing to do" + echo "should_publish=false" >> $GITHUB_OUTPUT + ;; + 404) + echo "$NAME@$VERSION is not on the registry; will publish" + echo "should_publish=true" >> $GITHUB_OUTPUT + ;; + *) + echo "::error::Unexpected HTTP $CODE from the registry; refusing to guess whether $VERSION is published" + exit 1 + ;; + esac publish: name: Publish to npm runs-on: ubuntu-latest needs: check-version - if: needs.check-version.outputs.version_changed == 'true' || github.event_name == 'workflow_dispatch' + if: needs.check-version.outputs.should_publish == 'true' # Must match the "Environment name" on the npmjs.com trusted publisher. # GitHub only adds an `environment` claim to the OIDC token when the job @@ -120,10 +126,10 @@ jobs: - name: Create GitHub Release uses: softprops/action-gh-release@v1 with: - tag_name: v${{ needs.check-version.outputs.new_version }} - name: Release v${{ needs.check-version.outputs.new_version }} + tag_name: v${{ needs.check-version.outputs.version }} + name: Release v${{ needs.check-version.outputs.version }} generate_release_notes: true draft: false - prerelease: ${{ contains(needs.check-version.outputs.new_version, '-') }} + prerelease: ${{ contains(needs.check-version.outputs.version, '-') }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 63e8dd42be0f875da589e2bef8a7c5c2e3a5f652 Mon Sep 17 00:00:00 2001 From: galangel Date: Tue, 18 Aug 2026 15:23:27 +0300 Subject: [PATCH 2/2] chore(ci): bump actions off the deprecated Node 20 runtime Every recent run warned that actions/checkout, actions/setup-node and softprops/action-gh-release target Node 20, which GitHub has deprecated and is already force-running on Node 24. Bumped across all three workflows, to the current major of each: actions/checkout v4 -> v7 actions/setup-node v4 -> v7 actions/upload-artifact v4 -> v7 actions/configure-pages v4 -> v6 actions/upload-pages-artifact v3 -> v5 actions/deploy-pages v4 -> v5 codecov/codecov-action v4 -> v7 softprops/action-gh-release v1 -> v3 Every input in use is stable across these majors: token/files/ fail_ci_if_error for codecov, name/path/retention-days for upload-artifact, and tag_name/name/generate_release_notes/draft/prerelease for the release step. No input changes were needed. Confirmed each referenced major tag resolves in its upstream repository and that all three workflows still parse. Note on coverage: ci.yml runs on pull_request, so checkout, setup-node and upload-artifact are exercised by this PR. deploy-storybook.yml and publish.yml only run on pushes to main, and the codecov step is gated on github.event_name == 'push', so those three are not verified until this lands on main. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci.yml | 18 +++++++++--------- .github/workflows/deploy-storybook.yml | 10 +++++----- .github/workflows/publish.yml | 8 ++++---- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 43606cd..1f2219e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,10 +17,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v7 with: node-version: '20' cache: 'npm' @@ -41,7 +41,7 @@ jobs: run: npm run test:coverage - name: Upload coverage reports - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v7 if: github.event_name == 'push' with: token: ${{ secrets.CODECOV_TOKEN }} @@ -55,10 +55,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v7 with: node-version: '20' cache: 'npm' @@ -70,7 +70,7 @@ jobs: run: npm run build - name: Upload build artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: dist path: dist/ @@ -83,10 +83,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v7 with: node-version: '20' cache: 'npm' @@ -98,7 +98,7 @@ jobs: run: npm run build:storybook - name: Upload Storybook artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: storybook-static path: storybook-static/ diff --git a/.github/workflows/deploy-storybook.yml b/.github/workflows/deploy-storybook.yml index e8bde4b..1e76751 100644 --- a/.github/workflows/deploy-storybook.yml +++ b/.github/workflows/deploy-storybook.yml @@ -21,10 +21,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v7 with: node-version: '20' cache: 'npm' @@ -36,10 +36,10 @@ jobs: run: npm run build:storybook - name: Setup Pages - uses: actions/configure-pages@v4 + uses: actions/configure-pages@v6 - name: Upload artifact - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@v5 with: path: storybook-static @@ -54,5 +54,5 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 4450e0a..94377d3 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 # Asks the registry whether this exact version exists, instead of diffing # package.json against HEAD~1. The old diff treated a version bump as a @@ -73,7 +73,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 # Deliberately no registry-url. It makes setup-node write an .npmrc with # //registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN} and export the @@ -82,7 +82,7 @@ jobs: # never attempts the OIDC exchange, failing with a masked 404. # registry.npmjs.org is npm's default registry, so nothing is lost. - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v7 with: node-version: '20' cache: 'npm' @@ -124,7 +124,7 @@ jobs: run: npm publish --provenance --access public - name: Create GitHub Release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v3 with: tag_name: v${{ needs.check-version.outputs.version }} name: Release v${{ needs.check-version.outputs.version }}