Skip to content

Commit 4d66aea

Browse files
authored
Merge pull request #242 from taptap/fix/release-app-token-recovery
ci: use release app token for generated PRs
2 parents 1ba5ca8 + 6b7ba02 commit 4d66aea

3 files changed

Lines changed: 404 additions & 41 deletions

File tree

.github/workflows/release.yml

Lines changed: 198 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)