From 9f67672764defbe4d8ecd1db739316903f583b13 Mon Sep 17 00:00:00 2001 From: Brandon Corbett Date: Mon, 3 Aug 2026 14:58:20 -0400 Subject: [PATCH] ci: tag and publish the release automatically after the version PR merges The version PR opened by changesets bumped package.json and CHANGELOG.md, but nothing tagged the result, so the image build (which triggers on v* tags) only ran after someone cut the tag and GitHub Release by hand. The version workflow now finishes the job. When no changesets remain, meaning the version PR has just merged, it tags the version in package.json, publishes a GitHub Release using the CHANGELOG section changesets already wrote, and invokes the image build directly. The build is invoked rather than left to the tag-push event because pushes made with GITHUB_TOKEN do not trigger other workflows. Tagging is idempotent, so a commit to main carrying no changeset is a no-op. docker-publish gains a workflow_call trigger so it can be called with an explicit tag, and checks that ref out. Its nightly tag is dropped: it was dead on a tag-only trigger, nothing in the ecosystem consumes it, and once called from a push to main it would have mislabeled every release image. The release job also runs format:check now, matching CI and the org baseline. --- .github/workflows/docker-publish.yml | 18 ++++++-- .github/workflows/release.yml | 65 ++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 065391a..7213418 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -4,6 +4,12 @@ on: push: tags: - 'v*' + workflow_call: + inputs: + image_tag: + description: 'Tag to build and publish (e.g. v1.2.3)' + required: true + type: string permissions: contents: read @@ -17,9 +23,16 @@ jobs: build-and-publish: runs-on: ubuntu-latest + env: + # On a tag push this is the ref (e.g. v1.2.3); when called from the + # version workflow it is the tag that release just created. + IMAGE_TAG: ${{ inputs.image_tag || github.ref_name }} + steps: - name: Checkout repository uses: actions/checkout@v4 + with: + ref: ${{ inputs.image_tag || github.ref_name }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -36,11 +49,10 @@ jobs: uses: docker/metadata-action@v5 with: images: | - ghcr.io/fells-code/seamless-auth-api + ghcr.io/${{ env.IMAGE_NAME }} tags: | - type=ref,event=tag + type=raw,value=${{ env.IMAGE_TAG }} type=raw,value=latest - type=raw,value=nightly,enable={{is_default_branch}} - name: Build and push Docker image uses: docker/build-push-action@v6 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a0bcb6b..3864c0b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,6 +21,9 @@ jobs: runs-on: ubuntu-latest env: HUSKY: 0 + outputs: + new_tag: ${{ steps.tag.outputs.new_tag }} + version: ${{ steps.tag.outputs.version }} steps: - name: Checkout repo @@ -40,6 +43,9 @@ jobs: - name: Lint run: npm run lint + - name: Check formatting + run: npm run format:check + - name: Test run: npm run test:run @@ -50,6 +56,7 @@ jobs: run: npm run build - name: Create or update version and changelog PR + id: changesets uses: changesets/action@v1 with: version: npm run version-packages @@ -57,3 +64,61 @@ jobs: commit: 'chore: update version and changelog' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Once the version PR is merged there are no changesets left, so the + # version in package.json is the one to release. Tag it (idempotently) so + # a versioned image is published. The tag is created here rather than + # relying on the tag-push event because pushes made with GITHUB_TOKEN do + # not trigger other workflows; the image build is invoked directly below. + - name: Tag release + id: tag + if: steps.changesets.outputs.hasChangesets == 'false' + run: | + set -euo pipefail + VERSION="$(node -p "require('./package.json').version")" + TAG="v${VERSION}" + if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then + echo "Tag ${TAG} already exists; nothing to release." + echo "new_tag=false" >> "$GITHUB_OUTPUT" + else + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag "${TAG}" + git push origin "${TAG}" + echo "new_tag=true" >> "$GITHUB_OUTPUT" + echo "version=${TAG}" >> "$GITHUB_OUTPUT" + fi + + # Publish a GitHub Release for the freshly created tag, using the notes + # changesets already wrote to CHANGELOG.md so the release body matches the + # changelog rather than a raw commit list. + - name: Create GitHub Release + if: steps.tag.outputs.new_tag == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + VERSION="$(node -p "require('./package.json').version")" + TAG="v${VERSION}" + awk -v header="## ${VERSION}" ' + $0 == header { capture = 1; next } + capture && /^## / { exit } + capture { print } + ' CHANGELOG.md > release-notes.md + if [ -s release-notes.md ]; then + gh release create "${TAG}" --title "${TAG}" --notes-file release-notes.md + else + echo "No CHANGELOG section for ${VERSION}; using generated notes." + gh release create "${TAG}" --title "${TAG}" --generate-notes + fi + + publish-image: + name: Publish versioned image + needs: version-changelog + if: needs.version-changelog.outputs.new_tag == 'true' + permissions: + contents: read + packages: write + uses: ./.github/workflows/docker-publish.yml + with: + image_tag: ${{ needs.version-changelog.outputs.version }}