From ad42f298dc356a6adfab40e51e42535b86b90ddb Mon Sep 17 00:00:00 2001 From: Ruben Nogueira <40404708+rubnogueira@users.noreply.github.com> Date: Tue, 19 May 2026 19:18:57 +0100 Subject: [PATCH] feat(ci): auto-tag and release on version bump to master (v2.4.3) Previously a release required pushing a `v*` tag manually, and a separate gate-release job verified the tag was reachable from master. Drop that whole flow in favour of a simpler model: 1. PR is opened: matrix runs for verification only. 2. PR merges to master: matrix runs again. If every job succeeds AND the package.json version on the merge commit has no matching `v` tag on origin yet, the release job: - creates the tag pointing at the merge commit - creates a GitHub Release named after the tag - uploads the prebuild tarballs as release assets The tag is created by softprops/action-gh-release using the built-in GITHUB_TOKEN, so it does not cascade back into the workflow. 3. Master push without a version bump: matrix runs, release step sees the tag already exists, skips. No-op release. Cutting the first release of this flow by bumping to 2.4.3. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/prebuild.yml | 72 ++++++++++++++++++---------------- package-lock.json | 4 +- package.json | 2 +- 3 files changed, 42 insertions(+), 36 deletions(-) diff --git a/.github/workflows/prebuild.yml b/.github/workflows/prebuild.yml index e3ea197..9b21ef9 100644 --- a/.github/workflows/prebuild.yml +++ b/.github/workflows/prebuild.yml @@ -3,7 +3,6 @@ name: prebuild on: push: branches: [master, main] - tags: ['v*'] pull_request: workflow_dispatch: @@ -135,44 +134,45 @@ jobs: if-no-files-found: error retention-days: 30 - # Gate the release on tag being reachable from master. If the tag was - # pushed from a feature branch (or any non-master commit), this job - # outputs on_master=false and the release job is skipped (not failed). - gate-release: - name: gate release on master ancestry + # Release flow: only fires on push to master, only after every matrix + # job succeeded, and only when the package.json version on the new + # commit does not yet have a matching `v` tag on origin. + # That makes the release a function of "merged version bump to master" + # — no manual tagging required. + release: + name: auto-release on version bump needs: prebuild - if: startsWith(github.ref, 'refs/tags/v') + if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main') runs-on: ubuntu-latest - outputs: - on_master: ${{ steps.check.outputs.on_master }} + permissions: + contents: write steps: - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - id: check + + - name: Read version from package.json + id: version + run: | + set -euo pipefail + v=$(node -p "require('./package.json').version") + echo "version=$v" >> "$GITHUB_OUTPUT" + echo "tag=v$v" >> "$GITHUB_OUTPUT" + echo "Detected package version: $v (target tag: v$v)" + + - name: Check whether tag already exists on origin + id: check_tag run: | set -euo pipefail - git fetch origin master - if git merge-base --is-ancestor "$GITHUB_SHA" origin/master; then - echo "Tag $GITHUB_REF_NAME (commit $GITHUB_SHA) is on master — release will publish" - echo "on_master=true" >> "$GITHUB_OUTPUT" + tag="${{ steps.version.outputs.tag }}" + if git ls-remote --tags origin "refs/tags/$tag" | grep -q .; then + echo "exists=true" >> "$GITHUB_OUTPUT" + echo "Tag $tag already exists on origin — nothing to release." else - echo "Tag $GITHUB_REF_NAME (commit $GITHUB_SHA) is NOT on master — release will be skipped" - echo "Merge your PR to master first, then re-tag the merge commit." - echo "on_master=false" >> "$GITHUB_OUTPUT" + echo "exists=false" >> "$GITHUB_OUTPUT" + echo "Tag $tag does not exist yet — will create release." fi - release: - name: attach prebuilds to GitHub Release - needs: [prebuild, gate-release] - if: needs.gate-release.outputs.on_master == 'true' - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v5 - - name: Download all prebuild artifacts + if: steps.check_tag.outputs.exists == 'false' uses: actions/download-artifact@v8 with: pattern: prebuilds-* @@ -180,14 +180,20 @@ jobs: merge-multiple: true - name: List downloaded assets + if: steps.check_tag.outputs.exists == 'false' run: ls -la artifacts/ - - name: Create / update Release and upload assets + - name: Create tag and Release, upload assets + if: steps.check_tag.outputs.exists == 'false' uses: softprops/action-gh-release@v2 with: - # Auto-uses the tag from github.ref. Idempotent: re-runs on the same - # tag will update the existing release and overwrite asset names - # that collide. + # softprops/action-gh-release creates the tag itself when given + # `tag_name` + `target_commitish`. It uses GITHUB_TOKEN, so the + # resulting tag push does NOT re-trigger this workflow (GH's + # rule: events from GITHUB_TOKEN don't cascade). + tag_name: ${{ steps.version.outputs.tag }} + target_commitish: ${{ github.sha }} + name: ${{ steps.version.outputs.tag }} files: artifacts/*.tar.gz generate_release_notes: true fail_on_unmatched_files: true diff --git a/package-lock.json b/package-lock.json index c2c3088..823d1e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-expat", - "version": "2.4.2", + "version": "2.4.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "node-expat", - "version": "2.4.2", + "version": "2.4.3", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 39416a1..ad617e0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-expat", - "version": "2.4.2", + "version": "2.4.3", "main": "./lib/node-expat", "gypfile": true, "description": "NodeJS binding for fast XML parsing.",