Skip to content

Commit 8610a7e

Browse files
committed
refactor: Improve release workflow for idempotent tagging and publishing
- Enhanced the release workflow to check for existing tags before creating new ones, preventing duplicate tags during releases. - Added logic to fetch remote tags and verify if a release already exists, ensuring smoother release processes. - Implemented idempotent checks for npm publishing to avoid errors when versions already exist. - Streamlined changelog extraction to always pull from committed files, improving reliability in release notes. These changes enhance the robustness and clarity of the release process, ensuring better version management and user experience.
1 parent 8b1e704 commit 8610a7e

1 file changed

Lines changed: 114 additions & 43 deletions

File tree

.github/workflows/release.yml

Lines changed: 114 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ jobs:
236236
fi
237237
238238
echo "CURRENT_VERSION=$CURRENT_VERSION" >> "$GITHUB_OUTPUT"
239-
echo "TAG=$TAG" >> "$GITHUB_OUTPUT"
239+
echo "TAG=$TAG" >> "$GITHUB_OUTPUT"
240240
241241
- name: 📋 Step - Check for changes
242242
id: changes
@@ -388,12 +388,22 @@ jobs:
388388
echo "✅ Version changes committed"
389389
fi
390390
391-
# Create tag
392-
git tag -a "v$NEW_VERSION" -m "Release v$NEW_VERSION"
393-
echo "✅ Tag v$NEW_VERSION created"
391+
# Create tag (idempotent - skip if already exists)
392+
if git rev-parse "v$NEW_VERSION" >/dev/null 2>&1; then
393+
echo "⚠️ Tag v$NEW_VERSION already exists, skipping creation"
394+
else
395+
git tag -a "v$NEW_VERSION" -m "Release v$NEW_VERSION"
396+
echo "✅ Tag v$NEW_VERSION created"
397+
fi
394398
395-
# Push commit and tag
396-
git push origin main "v$NEW_VERSION"
399+
# Push commit and tag (idempotent - only push if tag doesn't exist remotely)
400+
git push origin main || exit 0
401+
if ! git ls-remote --tags origin "v$NEW_VERSION" | grep -q "v$NEW_VERSION"; then
402+
git push origin "v$NEW_VERSION" || exit 0
403+
echo "✅ Tag v$NEW_VERSION pushed to remote"
404+
else
405+
echo "⚠️ Tag v$NEW_VERSION already exists on remote, skipping push"
406+
fi
397407
echo "✅ Committed and tagged v$NEW_VERSION"
398408
echo "✅ Phase: Version Bump - Complete"
399409
@@ -563,6 +573,12 @@ jobs:
563573
with:
564574
version: latest
565575

576+
- name: 📋 Step - Fetch tags from remote
577+
run: |
578+
echo "⏳ Fetching tags from remote..."
579+
git fetch origin --tags --force
580+
echo "✅ Tags fetched"
581+
566582
- name: 📋 Step - Determine tag to use
567583
id: tag_info
568584
run: |
@@ -588,13 +604,13 @@ jobs:
588604
echo "📋 Using tag from package.json: $TAG_NAME"
589605
fi
590606
591-
# Check if tag exists
607+
# Check if tag exists (after fetching from remote)
592608
if git rev-parse "$TAG_NAME" >/dev/null 2>&1; then
593609
echo "TAG_EXISTS=true" >> "$GITHUB_OUTPUT"
594610
echo "✅ Tag verified: $TAG_NAME (version: $VERSION)"
595611
else
596612
echo "TAG_EXISTS=false" >> "$GITHUB_OUTPUT"
597-
echo "⚠️ Tag '$TAG_NAME' does not exist yet (first release)"
613+
echo "⚠️ Tag '$TAG_NAME' does not exist yet"
598614
echo " Will use --unreleased flag for changelog generation"
599615
fi
600616
@@ -904,60 +920,66 @@ jobs:
904920

905921
- name: 📋 Step - Extract changelog for GitHub Release
906922
id: changelog_extract
907-
if: needs.changelog-generate.result == 'success'
908923
env:
909924
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
910925
run: |
911926
set -e
912927
913928
VERSION="${{ steps.version.outputs.VERSION }}"
929+
TAG="${{ steps.version.outputs.TAG }}"
914930
915931
echo "⏳ Extracting changelog sections for GitHub Release..."
916932
917-
# Use changelog from changelog-generate phase if available
918-
if [ -n "${{ env.CHANGELOG_SECTION }}" ]; then
919-
echo "📋 Using changelog from changelog-generate phase"
920-
echo "${{ env.CHANGELOG_SECTION }}" > /tmp/changelog-section.md
921-
else
922-
# Fallback: extract from existing changelog files
923-
echo "## [$VERSION] - $(date +%Y-%m-%d)" > /tmp/changelog-section.md
924-
echo "" >> /tmp/changelog-section.md
933+
# Always extract from committed changelog files (env vars don't persist across jobs)
934+
echo "## [$VERSION] - $(date +%Y-%m-%d)" > /tmp/changelog-section.md
935+
echo "" >> /tmp/changelog-section.md
925936
926-
# Extract CLI section
927-
if [ -f "packages/opennextjs-cli/CHANGELOG.md" ]; then
928-
CLI_SECTION=$(awk "/^## \[$VERSION\]/,/^## \[|^<!-- generated/" "packages/opennextjs-cli/CHANGELOG.md" | head -n -1 || true)
929-
if [ -n "$CLI_SECTION" ]; then
930-
echo "### @jsonbored/opennextjs-cli" >> /tmp/changelog-section.md
931-
echo "" >> /tmp/changelog-section.md
932-
echo "$CLI_SECTION" | sed '/^## \[/d' >> /tmp/changelog-section.md || true
933-
echo "" >> /tmp/changelog-section.md
934-
fi
937+
# Extract CLI section - try [VERSION] first, then [Unreleased] as fallback
938+
if [ -f "packages/opennextjs-cli/CHANGELOG.md" ]; then
939+
CLI_SECTION=$(awk "/^## \[$VERSION\]/,/^## \[|^<!-- generated/" "packages/opennextjs-cli/CHANGELOG.md" | head -n -1 || true)
940+
# If no version section found, try Unreleased
941+
if [ -z "$CLI_SECTION" ] || [ "$(echo "$CLI_SECTION" | wc -l)" -lt 3 ]; then
942+
CLI_SECTION=$(awk "/^## \[Unreleased\]/,/^## \[|^<!-- generated/" "packages/opennextjs-cli/CHANGELOG.md" | head -n -1 || true)
935943
fi
936-
937-
# Extract MCP section
938-
if [ -f "packages/opennextjs-mcp/CHANGELOG.md" ]; then
939-
MCP_SECTION=$(awk "/^## \[$VERSION\]/,/^## \[|^<!-- generated/" "packages/opennextjs-mcp/CHANGELOG.md" | head -n -1 || true)
940-
if [ -n "$MCP_SECTION" ]; then
941-
echo "### @jsonbored/opennextjs-mcp" >> /tmp/changelog-section.md
942-
echo "" >> /tmp/changelog-section.md
943-
echo "$MCP_SECTION" | sed '/^## \[/d' >> /tmp/changelog-section.md || true
944-
echo "" >> /tmp/changelog-section.md
945-
fi
944+
if [ -n "$CLI_SECTION" ] && [ "$(echo "$CLI_SECTION" | wc -l)" -ge 3 ]; then
945+
echo "### @jsonbored/opennextjs-cli" >> /tmp/changelog-section.md
946+
echo "" >> /tmp/changelog-section.md
947+
echo "$CLI_SECTION" | sed '/^## \[/d' >> /tmp/changelog-section.md || true
948+
echo "" >> /tmp/changelog-section.md
946949
fi
950+
fi
947951
948-
# If no sections, create generic one
949-
if [ ! -s /tmp/changelog-section.md ] || [ "$(wc -l < /tmp/changelog-section.md)" -lt 5 ]; then
950-
echo "Release of OpenNext.js CLI and MCP server packages." >> /tmp/changelog-section.md
952+
# Extract MCP section - try [VERSION] first, then [Unreleased] as fallback
953+
if [ -f "packages/opennextjs-mcp/CHANGELOG.md" ]; then
954+
MCP_SECTION=$(awk "/^## \[$VERSION\]/,/^## \[|^<!-- generated/" "packages/opennextjs-mcp/CHANGELOG.md" | head -n -1 || true)
955+
# If no version section found, try Unreleased
956+
if [ -z "$MCP_SECTION" ] || [ "$(echo "$MCP_SECTION" | wc -l)" -lt 3 ]; then
957+
MCP_SECTION=$(awk "/^## \[Unreleased\]/,/^## \[|^<!-- generated/" "packages/opennextjs-mcp/CHANGELOG.md" | head -n -1 || true)
958+
fi
959+
if [ -n "$MCP_SECTION" ] && [ "$(echo "$MCP_SECTION" | wc -l)" -ge 3 ]; then
960+
echo "### @jsonbored/opennextjs-mcp" >> /tmp/changelog-section.md
961+
echo "" >> /tmp/changelog-section.md
962+
echo "$MCP_SECTION" | sed '/^## \[/d' >> /tmp/changelog-section.md || true
951963
echo "" >> /tmp/changelog-section.md
952964
fi
953965
fi
954966
967+
# If no sections found, create generic one
968+
if [ ! -s /tmp/changelog-section.md ] || [ "$(wc -l < /tmp/changelog-section.md)" -lt 5 ]; then
969+
echo "Release of OpenNext.js CLI and MCP server packages." >> /tmp/changelog-section.md
970+
echo "" >> /tmp/changelog-section.md
971+
echo "This release includes both @jsonbored/opennextjs-cli and @jsonbored/opennextjs-mcp packages." >> /tmp/changelog-section.md
972+
echo "" >> /tmp/changelog-section.md
973+
fi
974+
955975
# Set for GitHub release
956976
echo "CHANGELOG_SECTION<<EOF" >> "$GITHUB_ENV"
957977
cat /tmp/changelog-section.md >> "$GITHUB_ENV"
958978
echo "EOF" >> "$GITHUB_ENV"
959979
960980
echo "✅ Changelog extracted for GitHub Release"
981+
echo " Preview:"
982+
head -20 /tmp/changelog-section.md
961983
962984
- name: 📋 Step - Publish CLI to npm (try OIDC first)
963985
id: publish-cli-oidc
@@ -967,8 +989,12 @@ jobs:
967989
968990
echo "⏳ Publishing @jsonbored/opennextjs-cli to npm (trying OIDC first)..."
969991
992+
# Check if version already exists first (idempotent)
993+
if npm view "@jsonbored/opennextjs-cli@${{ steps.version.outputs.VERSION }}" version >/dev/null 2>&1; then
994+
echo "⚠️ Version ${{ steps.version.outputs.VERSION }} already exists on npm, skipping OIDC publish"
995+
echo "SUCCESS=true" >> "$GITHUB_OUTPUT"
970996
# Try publishing with OIDC (if configured)
971-
if npm publish --access public 2>&1; then
997+
elif npm publish --access public 2>&1; then
972998
echo "✅ Published @jsonbored/opennextjs-cli@${{ steps.version.outputs.VERSION }} to npm (via OIDC)"
973999
echo "SUCCESS=true" >> "$GITHUB_OUTPUT"
9741000
else
@@ -998,8 +1024,18 @@ jobs:
9981024
9991025
# Configure npm to use token
10001026
echo "//registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN" > ~/.npmrc
1001-
npm publish --access public
1027+
# npm publish will fail if version already exists (idempotent behavior)
1028+
if npm publish --access public 2>&1; then
10021029
echo "✅ Published @jsonbored/opennextjs-cli@${{ steps.version.outputs.VERSION }} to npm (via NPM_TOKEN)"
1030+
else
1031+
PUBLISH_ERROR=$?
1032+
if npm view "@jsonbored/opennextjs-cli@${{ steps.version.outputs.VERSION }}" version >/dev/null 2>&1; then
1033+
echo "⚠️ Version ${{ steps.version.outputs.VERSION }} already exists on npm, skipping publish"
1034+
else
1035+
echo "❌ npm publish failed with exit code $PUBLISH_ERROR" >&2
1036+
exit $PUBLISH_ERROR
1037+
fi
1038+
fi
10031039
10041040
- name: 📋 Step - Publish MCP to npm (try OIDC first)
10051041
id: publish-mcp-oidc
@@ -1009,8 +1045,12 @@ jobs:
10091045
10101046
echo "⏳ Publishing @jsonbored/opennextjs-mcp to npm (trying OIDC first)..."
10111047
1048+
# Check if version already exists first (idempotent)
1049+
if npm view "@jsonbored/opennextjs-mcp@${{ steps.version.outputs.VERSION }}" version >/dev/null 2>&1; then
1050+
echo "⚠️ Version ${{ steps.version.outputs.VERSION }} already exists on npm, skipping OIDC publish"
1051+
echo "SUCCESS=true" >> "$GITHUB_OUTPUT"
10121052
# Try publishing with OIDC (if configured)
1013-
if npm publish --access public 2>&1; then
1053+
elif npm publish --access public 2>&1; then
10141054
echo "✅ Published @jsonbored/opennextjs-mcp@${{ steps.version.outputs.VERSION }} to npm (via OIDC)"
10151055
echo "SUCCESS=true" >> "$GITHUB_OUTPUT"
10161056
else
@@ -1040,8 +1080,18 @@ jobs:
10401080
echo "//registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN" > ~/.npmrc
10411081
fi
10421082
1043-
npm publish --access public
1083+
# npm publish will fail if version already exists (idempotent behavior)
1084+
if npm publish --access public 2>&1; then
10441085
echo "✅ Published @jsonbored/opennextjs-mcp@${{ steps.version.outputs.VERSION }} to npm (via NPM_TOKEN)"
1086+
else
1087+
PUBLISH_ERROR=$?
1088+
if npm view "@jsonbored/opennextjs-mcp@${{ steps.version.outputs.VERSION }}" version >/dev/null 2>&1; then
1089+
echo "⚠️ Version ${{ steps.version.outputs.VERSION }} already exists on npm, skipping publish"
1090+
else
1091+
echo "❌ npm publish failed with exit code $PUBLISH_ERROR" >&2
1092+
exit $PUBLISH_ERROR
1093+
fi
1094+
fi
10451095
10461096
echo "" >&2
10471097
echo "💡 After first release, set up OIDC trusted publishing for both packages:" >&2
@@ -1051,7 +1101,28 @@ jobs:
10511101
echo " 4. Both use: repository JSONbored/opennextjs-cli, workflow .github/workflows/release.yml" >&2
10521102
echo " Then you can remove the NPM_TOKEN secret." >&2
10531103
1104+
- name: 📋 Step - Check if GitHub Release exists
1105+
id: check_release
1106+
run: |
1107+
TAG="${{ steps.version.outputs.TAG }}"
1108+
RELEASE_RESPONSE=$(curl -s -w "\n%{http_code}" \
1109+
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
1110+
-H "Accept: application/vnd.github.v3+json" \
1111+
"https://api.github.com/repos/${{ github.repository }}/releases/tags/$TAG" || echo -e "\n404")
1112+
HTTP_CODE=$(echo "$RELEASE_RESPONSE" | tail -n1)
1113+
1114+
if [ "$HTTP_CODE" == "200" ]; then
1115+
echo "⚠️ Release $TAG already exists, will skip creation"
1116+
echo "EXISTS=true" >> "$GITHUB_OUTPUT"
1117+
else
1118+
echo "✅ Release $TAG does not exist, will create"
1119+
echo "EXISTS=false" >> "$GITHUB_OUTPUT"
1120+
fi
1121+
continue-on-error: true
1122+
10541123
- name: 📋 Step - Create GitHub Release
1124+
id: create_release
1125+
if: steps.check_release.outputs.EXISTS != 'true'
10551126
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2
10561127
with:
10571128
tag_name: ${{ steps.version.outputs.TAG }}

0 commit comments

Comments
 (0)