From fa71c78facefb35bc2076b0c0e35944fbecdd391 Mon Sep 17 00:00:00 2001 From: boomzero Date: Tue, 6 Oct 2026 10:03:07 +0800 Subject: [PATCH 1/7] Open release version PRs with the GitHub App token PRs created with GITHUB_TOKEN need a maintainer to approve their workflow runs, so the auto-merge on the actions/temp version PR never completes on its own. Create it with the app token like the other workflows. UpdateVersion still skips it because the actor ends in [bot]. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E5dr7XTsCeoamqLSiT9rLp (cherry picked from commit ce63eafcee736b5e71e7a67664de89e3431285ed) --- .github/workflows/UpdateToRelease.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/UpdateToRelease.yml b/.github/workflows/UpdateToRelease.yml index 5f82aa0fb..782e5d0e3 100644 --- a/.github/workflows/UpdateToRelease.yml +++ b/.github/workflows/UpdateToRelease.yml @@ -17,9 +17,17 @@ jobs: pull-requests: write contents: write steps: + # 用 GitHub App 的 token 创建版本 PR,这样它的工作流无需手动批准就能运行并自动合并 + - name: Generate a token + id: generate_token + uses: actions/create-github-app-token@v3 + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} - uses: actions/checkout@v6 with: fetch-depth: 0 + token: ${{ steps.generate_token.outputs.token }} - name: Update to release run: | if [[ ${{ github.event.pull_request.head.ref }} != dev && $(git diff --name-only HEAD HEAD~1) == *XMOJ.user.js* ]]; then @@ -27,8 +35,8 @@ jobs: gh pr comment ${{ github.event.pull_request.number }} --body "请向\`dev\`分支提交pull request, 本pull request将被自动关闭" gh pr close ${{ github.event.pull_request.number }} else - node ./Update/UpdateToRelease.js ${{ secrets.GITHUB_TOKEN }} ${{ github.event.pull_request.number }} "$PR_BODY" + node ./Update/UpdateToRelease.js ${{ steps.generate_token.outputs.token }} ${{ github.event.pull_request.number }} "$PR_BODY" fi env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} PR_BODY: ${{ github.event.pull_request.body }} From 9bf6842f272bdb2656dfd702e4f17470e24addc6 Mon Sep 17 00:00:00 2001 From: boomzero Date: Tue, 6 Oct 2026 10:16:35 +0800 Subject: [PATCH 2/7] Skip fork PRs in UpdateToRelease and scope its app token (workflow part of cherry picked commit f117fe9) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E5dr7XTsCeoamqLSiT9rLp --- .github/workflows/UpdateToRelease.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/UpdateToRelease.yml b/.github/workflows/UpdateToRelease.yml index 782e5d0e3..aeed3d394 100644 --- a/.github/workflows/UpdateToRelease.yml +++ b/.github/workflows/UpdateToRelease.yml @@ -11,8 +11,8 @@ on: jobs: UpdateToRelease: runs-on: ubuntu-latest - # 添加条件:如果是 bot 触发的则跳过,避免无限循环 - if: github.event.pull_request.user.login != 'github-actions[bot]' + # 添加条件:如果是 bot 触发的则跳过,避免无限循环;fork 的 PR 拿不到 secrets,也跳过 + if: github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.user.login != 'github-actions[bot]' permissions: pull-requests: write contents: write @@ -24,6 +24,8 @@ jobs: with: app-id: ${{ secrets.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} + permission-contents: write + permission-pull-requests: write - uses: actions/checkout@v6 with: fetch-depth: 0 From 1d9bc7c76c741af59edf7076ce818fa9897aeca9 Mon Sep 17 00:00:00 2001 From: boomzero Date: Tue, 6 Oct 2026 10:16:18 +0800 Subject: [PATCH 3/7] Retire extern-contrib workflows and harden workflows for fork PRs - Remove the dev -> extern-contrib sync and the merge.yml job that could never run (its only job required ref extern-contrib). - UpdateVersion: pass the PR title through env instead of pasting it into the shell command. - Prerelease: skip when a release for the current version exists, so a merge without a version bump cannot overwrite a published release. - Qodana: skip fork PRs, which do not receive QODANA_TOKEN. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E5dr7XTsCeoamqLSiT9rLp (cherry picked from commit eab4141de4fa6ae7270f9b8c19ddb4b64a1344b1) --- .github/workflows/Prerelease.yml | 12 +++++ .github/workflows/UpdateVersion.yml | 7 ++- .github/workflows/main.yml | 2 + .github/workflows/merge.yml | 51 -------------------- .github/workflows/sync-to-extern-contrib.yml | 24 --------- 5 files changed, 20 insertions(+), 76 deletions(-) delete mode 100644 .github/workflows/merge.yml delete mode 100644 .github/workflows/sync-to-extern-contrib.yml diff --git a/.github/workflows/Prerelease.yml b/.github/workflows/Prerelease.yml index ec92742ce..38fca7359 100644 --- a/.github/workflows/Prerelease.yml +++ b/.github/workflows/Prerelease.yml @@ -33,7 +33,19 @@ jobs: GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} id: get_version run: node ./Update/GetVersion.js + # 版本号没变(例如合并了没有更新版本号的 PR)时,不能覆盖已发布的同版本 release + - name: Check for an existing release + id: existing_release + env: + GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} + VERSION: ${{ steps.get_version.outputs.version }} + run: | + if gh release view "$VERSION" > /dev/null 2>&1; then + echo "Release $VERSION already exists, skipping." + echo exists=true >> $GITHUB_OUTPUT + fi - id: create_release + if: steps.existing_release.outputs.exists != 'true' uses: softprops/action-gh-release@v2 env: GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} diff --git a/.github/workflows/UpdateVersion.yml b/.github/workflows/UpdateVersion.yml index e43ba0c82..dbf982f73 100644 --- a/.github/workflows/UpdateVersion.yml +++ b/.github/workflows/UpdateVersion.yml @@ -24,6 +24,11 @@ jobs: private-key: ${{ secrets.APP_PRIVATE_KEY }} - uses: actions/checkout@v6 - name: Update version + # PR 标题不可信,通过环境变量传入,不直接拼进 shell 命令 env: + GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} + PR_NUMBER: ${{ github.event.number }} + PR_TITLE: ${{ github.event.pull_request.title }} PR_BODY: ${{ github.event.pull_request.body }} - run: node ./Update/UpdateVersion.js ${{ steps.generate_token.outputs.token }} ${{ github.event.number }} "${{ github.event.pull_request.title }}" "$PR_BODY" "${{ github.event.action }}" + PR_ACTION: ${{ github.event.action }} + run: node ./Update/UpdateVersion.js "$GITHUB_TOKEN" "$PR_NUMBER" "$PR_TITLE" "$PR_BODY" "$PR_ACTION" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fdd59cbdb..4042a1bcc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,6 +10,8 @@ on: jobs: qodana: runs-on: ubuntu-latest + # fork 的 PR 拿不到 QODANA_TOKEN + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository permissions: contents: write pull-requests: write diff --git a/.github/workflows/merge.yml b/.github/workflows/merge.yml deleted file mode 100644 index d127affd3..000000000 --- a/.github/workflows/merge.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Merge changes to extern-contrib -on: - push: - branches: - - dev - workflow_dispatch: -jobs: - create_pull_request: - name: 'Create PR' - if: github.ref == 'refs/heads/extern-contrib' - runs-on: ubuntu-latest - permissions: - pull-requests: write - steps: - - name: 'Create PR' - uses: actions/github-script@v7 - with: - script: | - const { repo, owner } = context.repo; - const pulls = await github.rest.pulls.list({ - owner: owner, - repo: repo, - head: context.ref, - base: 'dev', - state: 'open', - }); - - if (pulls.data.length < 1) { - await github.rest.pulls.create({ - title: 'Merge dev into extern-contrib', - owner: owner, - repo: repo, - head: context.ref, - base: 'dev', - body: [ - 'This PR is auto-generated by', - '[actions/github-script](https://github.com/actions/github-script)', - ].join('\n'), - }); - } else { - const existingPR = pulls.data[0]; - await github.rest.pulls.update({ - owner: owner, - repo: repo, - pull_number: existingPR.number, - body: [ - existingPR.body, - `Updated by Job ${context.job}`, - ].join('\n'), - }); - } diff --git a/.github/workflows/sync-to-extern-contrib.yml b/.github/workflows/sync-to-extern-contrib.yml deleted file mode 100644 index 2bb08cb94..000000000 --- a/.github/workflows/sync-to-extern-contrib.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Sync -on: - push: - branches: - - dev - -jobs: - sync-branches: - runs-on: ubuntu-latest - name: Syncing branches - steps: - - name: Checkout - uses: actions/checkout@v6 - - name: Set up Node - uses: actions/setup-node@v4 - with: - node-version: 12 - - name: Opening pull request - id: pull - uses: tretuna/sync-branches@1.4.0 - with: - GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} - FROM_BRANCH: "dev" - TO_BRANCH: "extern-contrib" From 909f9000185e15c99807f49a9c88665740fdbece Mon Sep 17 00:00:00 2001 From: boomzero Date: Tue, 6 Oct 2026 10:24:00 +0800 Subject: [PATCH 4/7] Bump the version for merged fork PRs from the push to dev Fork PRs get no secrets and the bot cannot push to forks, so their version cannot be bumped before merge. On a push to dev that touches XMOJ.user.js, look up the PR whose merge commit it is; if it came from a fork, run UpdateVersion in fork-merged mode: bump on actions/version- from dev and open an auto-merging PR as the GitHub App. Runs only on the trusted push, never on fork events, and never checks out fork code. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E5dr7XTsCeoamqLSiT9rLp (cherry picked from commit 581153a017573e96c5d94c11ccb7128aee53ab38) --- .github/workflows/UpdateVersionFork.yml | 57 +++++++++++++++++++++++++ Update/UpdateVersion.js | 48 +++++++++++++++------ 2 files changed, 92 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/UpdateVersionFork.yml diff --git a/.github/workflows/UpdateVersionFork.yml b/.github/workflows/UpdateVersionFork.yml new file mode 100644 index 000000000..8072aa068 --- /dev/null +++ b/.github/workflows/UpdateVersionFork.yml @@ -0,0 +1,57 @@ +name: UpdateVersionFork +# fork 的 PR 拿不到 secrets,也不能推送到 fork,所以合并前没法更新版本号。 +# 维护者合并后会推送到 dev:在这次(可信的)推送上找到对应的 PR,如果来自 fork, +# 就基于 dev 更新版本号,并以 GitHub App 身份开一个自动合并的 PR。 +on: + push: + branches: + - dev + paths: + - XMOJ.user.js +concurrency: + group: update-version-fork + cancel-in-progress: false +jobs: + UpdateVersionFork: + runs-on: ubuntu-latest + if: ${{ !endsWith(github.actor, '[bot]') }} + permissions: + contents: read + steps: + - name: Generate a token + id: generate_token + uses: actions/create-github-app-token@v3 + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + permission-contents: write + permission-pull-requests: write + - name: Find the merged fork PR + id: fork_pr + env: + GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} + SHA: ${{ github.sha }} + REPOSITORY: ${{ github.repository }} + run: | + PR=$(gh api "repos/$REPOSITORY/commits/$SHA/pulls" --jq "map(select(.merge_commit_sha == \"$SHA\" and .base.ref == \"dev\" and .head.repo.full_name != \"$REPOSITORY\")) | .[0].number // empty") + if [ -z "$PR" ]; then + echo "This push is not a merged fork PR." + else + echo "Merged fork PR: #$PR" + echo number=$PR >> $GITHUB_OUTPUT + fi + - uses: actions/checkout@v6 + if: steps.fork_pr.outputs.number + with: + ref: dev + token: ${{ steps.generate_token.outputs.token }} + - name: Update version + if: steps.fork_pr.outputs.number + env: + GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} + PR_NUMBER: ${{ steps.fork_pr.outputs.number }} + REPOSITORY: ${{ github.repository }} + run: | + PR_TITLE=$(gh api "repos/$REPOSITORY/pulls/$PR_NUMBER" --jq .title) + PR_BODY=$(gh api "repos/$REPOSITORY/pulls/$PR_NUMBER" --jq '.body // ""') + node ./Update/UpdateVersion.js "$GITHUB_TOKEN" "$PR_NUMBER" "$PR_TITLE" "$PR_BODY" fork-merged diff --git a/Update/UpdateVersion.js b/Update/UpdateVersion.js index 891d2c46f..b5ea89286 100644 --- a/Update/UpdateVersion.js +++ b/Update/UpdateVersion.js @@ -4,20 +4,29 @@ import {execSync} from "child_process"; var GithubToken = process.argv[2]; var PRNumber = process.argv[3]; process.env.GITHUB_TOKEN = GithubToken; -execSync("gh pr checkout " + PRNumber); -console.info("PR #" + PRNumber + " has been checked out."); - -// Check if the last commit was made by github-actions[bot] -// Only skip for synchronize events (push-triggered) to prevent infinite loops. -// For edited events (PR title/body changes), allow metadata updates even when -// the branch tip is a bot commit. const eventAction = String(process.argv[6] || ""); -const lastCommitAuthor = execSync("git log -1 --pretty=format:'%an'").toString().trim(); -console.log("Last commit author: " + lastCommitAuthor); +// A merged PR from a fork: we cannot push to the fork, so bump the version on +// a branch of this repository (checked out at dev) and open an auto-merging PR. +const ForkMerged = eventAction === "fork-merged"; +const ForkBranch = "actions/version-" + Number(PRNumber); console.log("Event action : " + eventAction); -if (lastCommitAuthor === "github-actions[bot]" && eventAction !== "edited") { - console.log("Last commit was made by github-actions[bot]. Skipping to prevent infinite loop."); - process.exit(0); +if (ForkMerged) { + execSync("git checkout -B " + ForkBranch); + console.info("Bumping the version for merged fork PR #" + PRNumber + " on " + ForkBranch + "."); +} else { + execSync("gh pr checkout " + PRNumber); + console.info("PR #" + PRNumber + " has been checked out."); + + // Check if the last commit was made by github-actions[bot] + // Only skip for synchronize events (push-triggered) to prevent infinite loops. + // For edited events (PR title/body changes), allow metadata updates even when + // the branch tip is a bot commit. + const lastCommitAuthor = execSync("git log -1 --pretty=format:'%an'").toString().trim(); + console.log("Last commit author: " + lastCommitAuthor); + if (lastCommitAuthor === "github-actions[bot]" && eventAction !== "edited") { + console.log("Last commit was made by github-actions[bot]. Skipping to prevent infinite loop."); + process.exit(0); + } } const JSONFileName = "./Update.json"; @@ -26,7 +35,10 @@ var JSONFileContent = readFileSync(JSONFileName, "utf8"); var JSFileContent = readFileSync(JSFileName, "utf8"); execSync("git config --global user.email \"github-actions[bot]@users.noreply.github.com\""); execSync("git config --global user.name \"github-actions[bot]\""); -if (JSONFileContent.includes('//!ci-no-touch')) { +if (ForkMerged && JSONFileContent.includes('//!ci-no-touch')) { + // The marker only means something on an open PR branch. + JSONFileContent = JSONFileContent.replace('//!ci-no-touch', ''); +} else if (JSONFileContent.includes('//!ci-no-touch')) { var updatedContent = JSONFileContent.replace('//!ci-no-touch', ''); writeFileSync(JSONFileName, updatedContent, "utf8"); execSync("git config pull.rebase false"); @@ -115,6 +127,16 @@ writeFileSync(JSONFileName, JSON.stringify(JSONObject, null, 4), "utf8"); console.warn("Update.json has been updated."); +if (ForkMerged) { + execSync("git commit -a -m \"" + CommitMessage + "\""); + execSync("git push -u origin " + ForkBranch + " -f"); + console.log("Pushed to " + ForkBranch + "."); + var VersionPR = execSync("gh pr create --base dev --head " + ForkBranch + " --title \"" + CommitMessage + " (#" + CurrentPR + ")\" --body \"Version bump for fork PR #" + CurrentPR + ".\"").toString().trim(); + console.log("Created " + VersionPR + "."); + execSync("gh pr merge " + VersionPR + " --merge --auto"); + console.log("Enabled auto merge."); + process.exit(0); +} execSync("git config pull.rebase false"); execSync("git pull"); execSync("git commit -a -m \"" + CommitMessage + "\""); From 997b83cfdc7d60763cbee450bf6ab8b91a54b7a4 Mon Sep 17 00:00:00 2001 From: boomzero Date: Tue, 6 Oct 2026 12:30:41 +0800 Subject: [PATCH 5/7] Serialize fork version bumps and only guard stable releases - UpdateVersionFork processes every merged fork PR to dev that has no Update.json entry, oldest first, each from the latest dev. A run dropped by the concurrency group loses nothing: the next run picks its PR up. - fork-merged mode reuses an open version PR on a rerun and waits for it to merge, so the next bump starts from a dev with this version. - Prerelease only skips when the version is already a stable release; an existing prerelease of the same version is still refreshed. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E5dr7XTsCeoamqLSiT9rLp (cherry picked from commit 461e19463859c12c35c881c7eb2d2a633b4f3868) --- .github/workflows/Prerelease.yml | 6 ++-- .github/workflows/UpdateVersionFork.yml | 43 ++++++++++++------------- Update/UpdateVersion.js | 21 ++++++++++-- 3 files changed, 42 insertions(+), 28 deletions(-) diff --git a/.github/workflows/Prerelease.yml b/.github/workflows/Prerelease.yml index 38fca7359..02734607c 100644 --- a/.github/workflows/Prerelease.yml +++ b/.github/workflows/Prerelease.yml @@ -33,15 +33,15 @@ jobs: GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} id: get_version run: node ./Update/GetVersion.js - # 版本号没变(例如合并了没有更新版本号的 PR)时,不能覆盖已发布的同版本 release + # 版本号没变(例如合并了没有更新版本号的 PR)时,不能把已发布的正式版覆盖成预发布版;同版本的预发布版照常更新 - name: Check for an existing release id: existing_release env: GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} VERSION: ${{ steps.get_version.outputs.version }} run: | - if gh release view "$VERSION" > /dev/null 2>&1; then - echo "Release $VERSION already exists, skipping." + if [ "$(gh release view "$VERSION" --json isPrerelease --jq .isPrerelease 2>/dev/null)" = "false" ]; then + echo "Release $VERSION is already published as a stable release, skipping the prerelease." echo exists=true >> $GITHUB_OUTPUT fi - id: create_release diff --git a/.github/workflows/UpdateVersionFork.yml b/.github/workflows/UpdateVersionFork.yml index 8072aa068..a75fb2397 100644 --- a/.github/workflows/UpdateVersionFork.yml +++ b/.github/workflows/UpdateVersionFork.yml @@ -1,13 +1,15 @@ name: UpdateVersionFork # fork 的 PR 拿不到 secrets,也不能推送到 fork,所以合并前没法更新版本号。 -# 维护者合并后会推送到 dev:在这次(可信的)推送上找到对应的 PR,如果来自 fork, -# 就基于 dev 更新版本号,并以 GitHub App 身份开一个自动合并的 PR。 +# 维护者合并后会推送到 dev:在这次(可信的)推送上找出所有已合并、但 Update.json 里还没有记录的 fork PR, +# 逐个基于最新的 dev 更新版本号,并以 GitHub App 身份开自动合并的 PR,等它合并后再处理下一个。 +# 并发组最多保留一个排队的运行,被替换掉的运行要处理的 PR 也会被后面的运行补上。 on: push: branches: - dev paths: - XMOJ.user.js + workflow_dispatch: concurrency: group: update-version-fork cancel-in-progress: false @@ -26,32 +28,27 @@ jobs: private-key: ${{ secrets.APP_PRIVATE_KEY }} permission-contents: write permission-pull-requests: write - - name: Find the merged fork PR - id: fork_pr - env: - GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} - SHA: ${{ github.sha }} - REPOSITORY: ${{ github.repository }} - run: | - PR=$(gh api "repos/$REPOSITORY/commits/$SHA/pulls" --jq "map(select(.merge_commit_sha == \"$SHA\" and .base.ref == \"dev\" and .head.repo.full_name != \"$REPOSITORY\")) | .[0].number // empty") - if [ -z "$PR" ]; then - echo "This push is not a merged fork PR." - else - echo "Merged fork PR: #$PR" - echo number=$PR >> $GITHUB_OUTPUT - fi - uses: actions/checkout@v6 - if: steps.fork_pr.outputs.number with: ref: dev token: ${{ steps.generate_token.outputs.token }} - - name: Update version - if: steps.fork_pr.outputs.number + - name: Update versions of merged fork PRs env: GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} - PR_NUMBER: ${{ steps.fork_pr.outputs.number }} REPOSITORY: ${{ github.repository }} + # 在此之前合并的 fork PR 不再补记版本号 + SINCE: "2026-10-06T00:00:00Z" run: | - PR_TITLE=$(gh api "repos/$REPOSITORY/pulls/$PR_NUMBER" --jq .title) - PR_BODY=$(gh api "repos/$REPOSITORY/pulls/$PR_NUMBER" --jq '.body // ""') - node ./Update/UpdateVersion.js "$GITHUB_TOKEN" "$PR_NUMBER" "$PR_TITLE" "$PR_BODY" fork-merged + PRS=$(gh pr list --repo "$REPOSITORY" --base dev --state merged --limit 50 --json number,isCrossRepository,mergedAt \ + --jq "map(select(.isCrossRepository and .mergedAt >= \"$SINCE\")) | sort_by(.mergedAt) | .[].number") + for PR_NUMBER in $PRS; do + git fetch -q origin dev + git checkout -q -B dev origin/dev + if jq -e --argjson pr "$PR_NUMBER" '[.UpdateHistory[].UpdateContents[].PR] | index($pr)' Update.json > /dev/null; then + echo "#$PR_NUMBER already has a version." + continue + fi + PR_TITLE=$(gh api "repos/$REPOSITORY/pulls/$PR_NUMBER" --jq .title) + PR_BODY=$(gh api "repos/$REPOSITORY/pulls/$PR_NUMBER" --jq '.body // ""') + node ./Update/UpdateVersion.js "$GITHUB_TOKEN" "$PR_NUMBER" "$PR_TITLE" "$PR_BODY" fork-merged + done diff --git a/Update/UpdateVersion.js b/Update/UpdateVersion.js index b5ea89286..1b5ff2e32 100644 --- a/Update/UpdateVersion.js +++ b/Update/UpdateVersion.js @@ -131,10 +131,27 @@ if (ForkMerged) { execSync("git commit -a -m \"" + CommitMessage + "\""); execSync("git push -u origin " + ForkBranch + " -f"); console.log("Pushed to " + ForkBranch + "."); - var VersionPR = execSync("gh pr create --base dev --head " + ForkBranch + " --title \"" + CommitMessage + " (#" + CurrentPR + ")\" --body \"Version bump for fork PR #" + CurrentPR + ".\"").toString().trim(); - console.log("Created " + VersionPR + "."); + // A rerun may find the PR from an earlier attempt; reuse it instead of failing to create a duplicate. + var VersionPR = execSync("gh pr list --base dev --head " + ForkBranch + " --state open --json url --jq '.[0].url // empty'").toString().trim(); + if (VersionPR) { + console.log("Reusing " + VersionPR + "."); + } else { + VersionPR = execSync("gh pr create --base dev --head " + ForkBranch + " --title \"" + CommitMessage + " (#" + CurrentPR + ")\" --body \"Version bump for fork PR #" + CurrentPR + ".\"").toString().trim(); + console.log("Created " + VersionPR + "."); + } execSync("gh pr merge " + VersionPR + " --merge --auto"); console.log("Enabled auto merge."); + // Wait for the merge, so the next fork PR is bumped from a dev that already has this version. + for (var Attempt = 0; ; Attempt++) { + var State = execSync("gh pr view " + VersionPR + " --json state --jq .state").toString().trim(); + if (State == "MERGED") break; + if (State == "CLOSED" || Attempt >= 120) { + console.error(VersionPR + " was not merged (state: " + State + ")."); + process.exit(1); + } + execSync("sleep 10"); + } + console.log(VersionPR + " has been merged."); process.exit(0); } execSync("git config pull.rebase false"); From fc5bc5bca3e01fd9fbf30c9f410cab554ca828c4 Mon Sep 17 00:00:00 2001 From: boomzero Date: Tue, 6 Oct 2026 13:00:51 +0800 Subject: [PATCH 6/7] Remove the retired Qodana workflow Qodana is no longer used and the workflow is disabled on GitHub; this also drops the fork guard added to it earlier in this PR. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E5dr7XTsCeoamqLSiT9rLp (cherry picked from commit 0f68da7a8b99d293a275656acf1d2f40df1c59ba) --- .github/workflows/main.yml | 27 --------------------------- 1 file changed, 27 deletions(-) delete mode 100644 .github/workflows/main.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index 4042a1bcc..000000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Qodana -on: - workflow_dispatch: - pull_request: - push: - branches: - - master - - dev - -jobs: - qodana: - runs-on: ubuntu-latest - # fork 的 PR 拿不到 QODANA_TOKEN - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository - permissions: - contents: write - pull-requests: write - checks: write - steps: - - uses: actions/checkout@v6 - with: - ref: ${{ github.event.pull_request.head.sha }} # to check out the actual pull request commit, not the merge commit - fetch-depth: 0 # a full history is required for pull request analysis - - name: 'Qodana Scan' - uses: JetBrains/qodana-action@v2025.2 - env: - QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }} From d5c319fdc61c003d89c55773d6c997731d3a2a24 Mon Sep 17 00:00:00 2001 From: Zhu Chenrui Date: Tue, 6 Oct 2026 19:15:00 +0800 Subject: [PATCH 7/7] Update ServerURL for DebugMode configuration Signed-off-by: Zhu Chenrui --- XMOJ.user.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/XMOJ.user.js b/XMOJ.user.js index a1bfdc4dd..736e03764 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -3844,7 +3844,7 @@ if (UtilityEnabled("AutoLogin") && document.querySelector("body > a:nth-child(1) } SearchParams = new URLSearchParams(location.search); -let ServerURL = (UtilityEnabled("DebugMode") ? "https://ghpages.xmoj-script.uk/" : "https://www.xmoj-script.uk") +let ServerURL = (UtilityEnabled("DebugMode") ? "https://dev.xmoj-script.uk/" : "https://www.xmoj-script.uk") const profileElement = document.querySelector("#profile"); if (profileElement === null) { RevealPage();