@@ -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