@@ -422,11 +422,18 @@ jobs:
422422 (needs.build-native.result == 'success' || needs.download-native.result == 'success')
423423
424424 steps :
425+ - name : Generate release app token
426+ id : app-token
427+ uses : actions/create-github-app-token@v2
428+ with :
429+ app-id : ${{ secrets.RELEASE_APP_ID }}
430+ private-key : ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
431+
425432 - name : Checkout
426433 uses : actions/checkout@v4
427434 with :
428435 fetch-depth : 0
429- token : ${{ secrets.GITHUB_TOKEN }}
436+ token : ${{ steps.app-token.outputs.token }}
430437
431438 - name : Setup Node.js
432439 uses : actions/setup-node@v4
@@ -475,24 +482,105 @@ jobs:
475482 echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
476483 echo "Using manually resolved version: ${VERSION}"
477484
485+ - name : Generate release write app token
486+ id : write-app-token
487+ uses : actions/create-github-app-token@v2
488+ with :
489+ app-id : ${{ secrets.RELEASE_APP_ID }}
490+ private-key : ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
491+
492+ - name : Configure release write app token
493+ env :
494+ GH_TOKEN : ${{ steps.write-app-token.outputs.token }}
495+ run : |
496+ git config --local http.https://github.com/.extraheader "AUTHORIZATION: bearer ${GH_TOKEN}"
497+
498+ - name : Resolve existing release PR
499+ id : release_pr
500+ env :
501+ GH_TOKEN : ${{ steps.write-app-token.outputs.token }}
502+ run : |
503+ VERSION=${{ steps.release_version.outputs.version }}
504+ BRANCH="release/v${VERSION}"
505+
506+ PR_FIELDS=$(gh pr list \
507+ --state all \
508+ --base main \
509+ --head "$BRANCH" \
510+ --json url,number,state \
511+ --jq '[(.[0].url // ""), (.[0].number // ""), (.[0].state // "")] | @tsv')
512+ IFS=$'\t' read -r PR_URL PR_NUMBER PR_STATE <<< "$PR_FIELDS"
513+
514+ echo "url=${PR_URL}" >> "$GITHUB_OUTPUT"
515+ echo "number=${PR_NUMBER}" >> "$GITHUB_OUTPUT"
516+ echo "state=${PR_STATE}" >> "$GITHUB_OUTPUT"
517+ echo "branch=${BRANCH}" >> "$GITHUB_OUTPUT"
518+
519+ if [ -n "$PR_URL" ]; then
520+ echo "Found existing release PR: ${PR_URL} (state=${PR_STATE})"
521+ else
522+ echo "No existing release PR found for ${BRANCH}"
523+ fi
524+
525+ - name : Fail closed release PR
526+ if : steps.release_pr.outputs.state == 'CLOSED'
527+ run : |
528+ echo "Existing release PR was closed without merging"
529+ echo "${{ steps.release_pr.outputs.url }}"
530+ exit 1
531+
478532 # 创建 Release 分支
479533 - name : Create release branch
480- id : branch
534+ if : success() && steps.release_pr.outputs.state != 'MERGED'
481535 run : |
482536 VERSION=${{ steps.release_version.outputs.version }}
483537 BRANCH="release/v${VERSION}"
484538
485- echo "branch=$BRANCH" >> $GITHUB_OUTPUT
486-
487539 git config user.name "github-actions[bot]"
488540 git config user.email "github-actions[bot]@users.noreply.github.com"
489541
490- git checkout -b "$BRANCH"
542+ if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then
543+ echo "Release branch ${BRANCH} already exists, reusing it"
544+ git fetch origin "$BRANCH"
545+ git checkout -B "$BRANCH" "origin/$BRANCH"
546+ else
547+ git checkout -B "$BRANCH"
548+ fi
549+
550+ - name : Checkout merged release commit
551+ if : success() && steps.release_pr.outputs.state == 'MERGED'
552+ env :
553+ GH_TOKEN : ${{ steps.write-app-token.outputs.token }}
554+ run : |
555+ VERSION=${{ steps.release_version.outputs.version }}
556+ PR_NUMBER=${{ steps.release_pr.outputs.number }}
557+
558+ MERGE_COMMIT=$(gh pr view "$PR_NUMBER" --json mergeCommit --jq '.mergeCommit.oid // ""')
559+ if [ -z "$MERGE_COMMIT" ]; then
560+ echo "Release PR #${PR_NUMBER} is merged but merge commit is unavailable"
561+ exit 1
562+ fi
563+
564+ git fetch origin main
565+ git checkout -B main "$MERGE_COMMIT"
566+
567+ PACKAGE_VERSION=$(node -p "require('./package.json').version")
568+ if [ "$PACKAGE_VERSION" != "$VERSION" ]; then
569+ echo "Merged main package version ${PACKAGE_VERSION} does not match ${VERSION}"
570+ exit 1
571+ fi
491572
492573 - name : Set release package version
574+ if : success() && steps.release_pr.outputs.state != 'MERGED'
493575 run : |
494576 VERSION=${{ steps.release_version.outputs.version }}
495577
578+ PACKAGE_VERSION=$(node -p "require('./package.json').version")
579+ if [ "$PACKAGE_VERSION" = "$VERSION" ]; then
580+ echo "Release branch package.json already has version ${VERSION}"
581+ exit 0
582+ fi
583+
496584 npm version $VERSION --no-git-tag-version
497585
498586 # 生成主包 CHANGELOG 和 GitHub Release notes,过滤 Maker-only commits
@@ -512,18 +600,27 @@ jobs:
512600
513601 # 发布到 npm (包含 native binaries)
514602 # 优先使用 OIDC provenance;fallback 到 NPM_TOKEN(首次发布新包名时需要)
515- - name : Publish to npm
603+ - name : Verify or publish npm package
516604 env :
517605 NODE_AUTH_TOKEN : ${{ secrets.NPM_TOKEN }}
518606 run : |
607+ VERSION=${{ steps.release_version.outputs.version }}
608+
609+ if npm view "@taptap/instant-games-open-mcp@${VERSION}" version >/dev/null 2>&1; then
610+ echo "@taptap/instant-games-open-mcp@${VERSION} already exists on npm"
611+ npm dist-tag add "@taptap/instant-games-open-mcp@${VERSION}" latest
612+ exit 0
613+ fi
614+
519615 npm publish --access public --tag latest --provenance 2>/dev/null \
520616 || npm publish --access public --tag latest
521617
522618 # 提交并推送
523619 - name : Commit and push release branch
620+ if : success() && steps.release_pr.outputs.state != 'MERGED'
524621 run : |
525622 VERSION=${{ steps.release_version.outputs.version }}
526- BRANCH=${{ steps.branch .outputs.branch }}
623+ BRANCH=${{ steps.release_pr .outputs.branch }}
527624 NATIVE_CHANGED=${{ needs.analyze.outputs.native_changed }}
528625
529626 NATIVE_NOTE=""
@@ -534,6 +631,11 @@ jobs:
534631 fi
535632
536633 git add package.json package-lock.json CHANGELOG.md
634+ if git diff --cached --quiet; then
635+ echo "Release files already committed on ${BRANCH}"
636+ exit 0
637+ fi
638+
537639 git commit -m "chore(release): ${VERSION}
538640
539641 ${NATIVE_NOTE}
@@ -547,11 +649,12 @@ jobs:
547649 # 创建 PR
548650 - name : Create Pull Request
549651 id : pr
652+ if : success() && steps.release_pr.outputs.url == ''
550653 env :
551- GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
654+ GH_TOKEN : ${{ steps.write-app-token.outputs.token }}
552655 run : |
553656 VERSION=${{ steps.release_version.outputs.version }}
554- BRANCH=${{ steps.branch .outputs.branch }}
657+ BRANCH=${{ steps.release_pr .outputs.branch }}
555658 NATIVE_CHANGED=${{ needs.analyze.outputs.native_changed }}
556659
557660 if [ "$NATIVE_CHANGED" == "true" ]; then
@@ -592,12 +695,21 @@ jobs:
592695
593696 # 自动合并
594697 - name : Auto-merge PR
698+ if : success() && steps.release_pr.outputs.state != 'MERGED'
595699 env :
596- GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
700+ GH_TOKEN : ${{ steps.write-app-token.outputs.token }}
597701 run : |
598- PR_URL=${{ steps.pr.outputs.pr_url }}
702+ PR_URL=" ${{ steps.pr.outputs.pr_url || steps.release_pr.outputs.url }}"
599703 PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$')
600704
705+ AUTO_MERGE_ENABLED=$(gh pr view "$PR_NUMBER" \
706+ --json autoMergeRequest \
707+ --jq '.autoMergeRequest != null')
708+ if [ "$AUTO_MERGE_ENABLED" = "true" ]; then
709+ echo "Auto-merge is already enabled for PR #$PR_NUMBER"
710+ exit 0
711+ fi
712+
601713 echo "Enabling auto-merge for PR #$PR_NUMBER..."
602714
603715 gh pr merge "$PR_NUMBER" \
@@ -607,46 +719,97 @@ jobs:
607719
608720 echo "Auto-merge enabled for PR #$PR_NUMBER, will merge when all checks pass."
609721
610- # 等待 release PR 合并后,再创建 tag 和 GitHub Release
611- - name : Create GitHub Release
722+ # 等待 release PR 合并;等待期间不消耗后续写操作使用的 App token。
723+ - name : Wait for release PR merge
724+ id : wait_for_merge
612725 env :
613- GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
726+ GH_TOKEN : ${{ github.token }}
614727 run : |
615- VERSION=${{ steps.release_version.outputs.version }}
616- PR_URL=${{ steps.pr.outputs.pr_url }}
728+ PR_URL="${{ steps.pr.outputs.pr_url || steps.release_pr.outputs.url }}"
617729 PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$')
730+ STATE="${{ steps.release_pr.outputs.state }}"
618731
619732 # 等待 PR 合并(auto-merge 是异步的)
620- echo "Waiting for PR #$PR_NUMBER to be merged..."
621- for i in $(seq 1 60); do
622- STATE=$(gh pr view "$PR_NUMBER" --json state --jq '.state')
623- if [ "$STATE" = "MERGED" ]; then
624- echo "PR #$PR_NUMBER merged after ${i}0s"
625- break
626- fi
627- if [ "$STATE" = "CLOSED" ]; then
628- echo "PR #$PR_NUMBER was closed without merging"
629- exit 1
630- fi
631- sleep 10
632- done
733+ if [ "$STATE" != "MERGED" ]; then
734+ echo "Waiting for PR #$PR_NUMBER to be merged..."
735+ for i in $(seq 1 360); do
736+ STATE=$(gh pr view "$PR_NUMBER" --json state --jq '.state')
737+ if [ "$STATE" = "MERGED" ]; then
738+ echo "PR #$PR_NUMBER merged after ${i}0s"
739+ break
740+ fi
741+ if [ "$STATE" = "CLOSED" ]; then
742+ echo "PR #$PR_NUMBER was closed without merging"
743+ exit 1
744+ fi
745+ sleep 10
746+ done
747+ fi
633748
634749 if [ "$STATE" != "MERGED" ]; then
635- echo "Timeout waiting for PR merge, skipping release creation"
750+ echo "Timeout waiting for PR merge."
751+ echo "After the release PR is merged, rerun failed jobs to continue."
752+ exit 1
753+ fi
754+
755+ echo "state=${STATE}" >> "$GITHUB_OUTPUT"
756+ MERGE_COMMIT=$(gh pr view "$PR_NUMBER" --json mergeCommit --jq '.mergeCommit.oid // ""')
757+ if [ -z "$MERGE_COMMIT" ]; then
758+ echo "Release PR #${PR_NUMBER} is merged but merge commit is unavailable"
636759 exit 1
637760 fi
761+ echo "merge_commit=${MERGE_COMMIT}" >> "$GITHUB_OUTPUT"
762+
763+ - name : Generate final release app token
764+ id : final-app-token
765+ if : success() && steps.wait_for_merge.outputs.state == 'MERGED'
766+ uses : actions/create-github-app-token@v2
767+ with :
768+ app-id : ${{ secrets.RELEASE_APP_ID }}
769+ private-key : ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
770+
771+ # release PR 合并后,用新生成的 App token 创建 tag 和 GitHub Release。
772+ - name : Create GitHub Release
773+ if : success() && steps.wait_for_merge.outputs.state == 'MERGED'
774+ env :
775+ GH_TOKEN : ${{ steps.final-app-token.outputs.token }}
776+ run : |
777+ VERSION=${{ steps.release_version.outputs.version }}
778+ MERGE_COMMIT=${{ steps.wait_for_merge.outputs.merge_commit }}
638779
639- # PR 已合并,获取最新 main
640- git fetch origin
641- git checkout -B main origin/main
780+ # PR 已合并,检出该 release PR 的 merge commit,避免 main 后续推进影响 tag 目标。
781+ git config --local http.https://github.com/.extraheader "AUTHORIZATION: bearer ${GH_TOKEN}"
782+ git fetch origin main
783+ git checkout -B main "$MERGE_COMMIT"
784+ git config user.name "github-actions[bot]"
785+ git config user.email "github-actions[bot]@users.noreply.github.com"
642786
643787 test -s "$RUNNER_TEMP/main-release-notes.md"
644788
645789 # 在 main 最新 commit 上创建 tag(即 squash merge 后的 release commit)
646- git tag -a "v${VERSION}" -m "Release v${VERSION}"
647- git push origin "v${VERSION}"
790+ if git ls-remote --exit-code --tags origin "v${VERSION}" >/dev/null 2>&1; then
791+ git fetch --force origin "refs/tags/v${VERSION}:refs/tags/v${VERSION}"
792+ TAG_TARGET=$(git rev-list -n 1 "v${VERSION}")
793+ MAIN_TARGET=$(git rev-parse HEAD)
794+
795+ if [ "$TAG_TARGET" != "$MAIN_TARGET" ]; then
796+ echo "Existing tag v${VERSION} points to ${TAG_TARGET}, expected ${MAIN_TARGET}"
797+ exit 1
798+ fi
799+
800+ echo "Existing tag v${VERSION} points to ${TAG_TARGET}"
801+ else
802+ git tag -a "v${VERSION}" -m "Release v${VERSION}"
803+ git push origin "v${VERSION}"
804+ fi
648805
649806 # 创建 GitHub Release,上传 native binaries 作为 assets
807+ if gh release view "v${VERSION}" >/dev/null 2>&1; then
808+ echo "GitHub Release v${VERSION} already exists"
809+ gh release upload "v${VERSION}" native/*.node --clobber
810+ exit 0
811+ fi
812+
650813 gh release create "v${VERSION}" \
651814 --title "v${VERSION}" \
652815 --notes-file "$RUNNER_TEMP/main-release-notes.md" \
0 commit comments