From 449c01de7e0825e72f7c7fa15bea2cc982afbb25 Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Fri, 13 Mar 2026 17:21:45 +0800 Subject: [PATCH] fix(release): finalize repository releases after version PR merges Detect release PR merge ranges separately from package publish success. Allow the workflow to complete the root changelog, tag, and GitHub Release when the version PR landed but the package version already exists. --- .github/workflows/release.yml | 71 ++++++++++++++++++++++++++++++----- README.md | 12 ++++-- 2 files changed, 70 insertions(+), 13 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 94a6a9f..c1ecfd3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -68,11 +68,62 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Determine Release Finalization + id: finalize + if: steps.changesets.outputs.hasChangesets == 'false' + env: + BEFORE_SHA: ${{ github.event.before }} + PUBLISHED: ${{ steps.changesets.outputs.published }} + run: | + RELEASE_VERSION=$(node -p "require('./package.json').version") + echo "release_version=${RELEASE_VERSION}" >> "$GITHUB_OUTPUT" + + CHANGE_RANGE_START="${BEFORE_SHA}" + if [ -z "${CHANGE_RANGE_START}" ] || [ "${CHANGE_RANGE_START}" = "0000000000000000000000000000000000000000" ]; then + CHANGE_RANGE_START="$(git rev-list --max-count=1 HEAD^ 2>/dev/null || true)" + fi + + if [ -n "${CHANGE_RANGE_START}" ]; then + CHANGED_FILES="$(git diff --name-only "${CHANGE_RANGE_START}" "${GITHUB_SHA}")" + else + CHANGED_FILES="$(git show --pretty='' --name-only "${GITHUB_SHA}")" + fi + + ROOT_VERSION_CHANGED=false + PACKAGE_RELEASE_FILES_CHANGED=false + + if printf '%s\n' "${CHANGED_FILES}" | grep -qx 'package.json'; then + ROOT_VERSION_CHANGED=true + fi + + if printf '%s\n' "${CHANGED_FILES}" | grep -Eq '^packages/[^/]+/(CHANGELOG\.md|package\.json)$|^NEXT-CHANGELOG-ENTRY\.md$'; then + PACKAGE_RELEASE_FILES_CHANGED=true + fi + + TAG_EXISTS=false + if git rev-parse -q --verify "refs/tags/${RELEASE_VERSION}" >/dev/null; then + TAG_EXISTS=true + fi + + SHOULD_FINALIZE=false + if [ "${PUBLISHED}" = "true" ]; then + SHOULD_FINALIZE=true + elif [ "${ROOT_VERSION_CHANGED}" = "true" ] && [ "${PACKAGE_RELEASE_FILES_CHANGED}" = "true" ] && [ "${TAG_EXISTS}" = "false" ]; then + SHOULD_FINALIZE=true + fi + + echo "root_version_changed=${ROOT_VERSION_CHANGED}" >> "$GITHUB_OUTPUT" + echo "package_release_files_changed=${PACKAGE_RELEASE_FILES_CHANGED}" >> "$GITHUB_OUTPUT" + echo "tag_exists=${TAG_EXISTS}" >> "$GITHUB_OUTPUT" + echo "should_finalize=${SHOULD_FINALIZE}" >> "$GITHUB_OUTPUT" + - name: Publishing Process id: publish - if: steps.changesets.outputs.published == 'true' + if: steps.finalize.outputs.should_finalize == 'true' + env: + RELEASE_VERSION: ${{ steps.finalize.outputs.release_version }} + TAG_EXISTS: ${{ steps.finalize.outputs.tag_exists }} run: | - RELEASE_VERSION=$(node -p "require('./package.json').version") echo "release_version=${RELEASE_VERSION}" >> "$GITHUB_OUTPUT" if [ -s NEXT-CHANGELOG-ENTRY.md ]; then @@ -81,19 +132,21 @@ jobs: pnpm exec git-cliff --unreleased --output RELEASE_NOTES.md --strip header --bump fi - pnpm exec git-cliff --unreleased --bump --output CHANGELOG.md - : > NEXT-CHANGELOG-ENTRY.md + if [ "${TAG_EXISTS}" = "false" ]; then + pnpm exec git-cliff --unreleased --bump --output CHANGELOG.md + : > NEXT-CHANGELOG-ENTRY.md - git add CHANGELOG.md NEXT-CHANGELOG-ENTRY.md - git commit -m "chore(release): update changelog for ${RELEASE_VERSION}" + git add CHANGELOG.md NEXT-CHANGELOG-ENTRY.md + git commit -m "chore(release): update changelog for ${RELEASE_VERSION}" - git tag -a "${RELEASE_VERSION}" -m "Release ${RELEASE_VERSION}" - git push origin main --follow-tags + git tag -a "${RELEASE_VERSION}" -m "Release ${RELEASE_VERSION}" + git push origin main --follow-tags + fi echo "release_body_path=RELEASE_NOTES.md" >> "$GITHUB_OUTPUT" - name: Create GitHub Release - if: steps.changesets.outputs.published == 'true' + if: steps.finalize.outputs.should_finalize == 'true' uses: softprops/action-gh-release@v2 with: tag_name: ${{ steps.publish.outputs.release_version }} diff --git a/README.md b/README.md index 7e882b4..64b5c99 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ Its key feature is a two-step release process: 2. Publish: Once the PR is merged, the action publishes non-private packages, regenerates the root `CHANGELOG.md`, creates a Git tag, and publishes a polished GitHub Release. + If a package version was already published during validation, the workflow still finalizes the + repository release as long as the version PR landed and the root tag does not exist yet. Key Features: - 🚀 **Turborepo-Optimized**: High-speed CI leveraging Turborepo's caching and task orchestration. @@ -88,10 +90,12 @@ graph TD H -- Has changesets --> I["Create or update
Version Packages PR"]; I --> J[PR Merged by User]; - H -- No changesets, publishable package changed --> K["pnpm changeset publish"]; - K --> L["git-cliff regenerates
root CHANGELOG.md"]; - L --> M["Commit changelog
Create and push tag"]; - M --> N[🚀 Create GitHub Release]; + H -- No changesets --> K["pnpm changeset publish"]; + K --> L{"Published now or
version PR landed
without root tag?"}; + L -- Yes --> M["git-cliff regenerates
root CHANGELOG.md"]; + L -- No --> O["No repository release update"]; + M --> P["Commit changelog
Create and push tag"]; + P --> N[🚀 Create GitHub Release]; end E --> F;