From bf2c244cd4a035db26dc40b36eaa0cca7dc4d111 Mon Sep 17 00:00:00 2001 From: Shaojin Wen Date: Tue, 30 Jun 2026 10:52:02 +0800 Subject: [PATCH 1/7] ci(workflows): remind authors not to force-push active PRs Add a workflow that detects force-pushes (rebase/amend/reset) to open PRs via the pull_request_target synchronize event and posts a one-time, bilingual reminder that force-pushing invalidates existing review comments and that the integration bots squash all changes into a single commit automatically. A normal push (compare status "ahead") is ignored; the reminder is posted at most once per PR, bot-initiated pushes are skipped, and a failed compare is treated conservatively (no comment). --- .github/workflows/pr-force-push-reminder.yml | 118 +++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 .github/workflows/pr-force-push-reminder.yml diff --git a/.github/workflows/pr-force-push-reminder.yml b/.github/workflows/pr-force-push-reminder.yml new file mode 100644 index 00000000000..ae3601002b6 --- /dev/null +++ b/.github/workflows/pr-force-push-reminder.yml @@ -0,0 +1,118 @@ +name: 'PR Force-Push Reminder' + +on: + pull_request_target: + types: + - 'synchronize' + +permissions: + contents: 'read' + pull-requests: 'write' + +# One in-flight check per PR; a newer push supersedes the previous one so we +# never post the reminder twice for a rapid burst of force-pushes. +concurrency: + group: 'pr-force-push-reminder-${{ github.event.pull_request.number }}' + cancel-in-progress: true + +jobs: + remind-on-force-push: + name: 'Remind on force-push' + timeout-minutes: 5 + if: |- + ${{ github.repository == 'QwenLM/qwen-code' }} + runs-on: 'ubuntu-latest' + steps: + - name: 'Detect force-push and post reminder' + uses: 'actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3' # v9.0.0 + with: + github-token: '${{ secrets.GITHUB_TOKEN }}' + script: | + const pr = context.payload.pull_request; + const before = context.payload.before; + const after = context.payload.after; + + // A `synchronize` event should always carry both SHAs; bail out if + // either is missing or `before` is the all-zero (no parent) SHA. + if (!before || !after || /^0+$/.test(before)) { + console.log('No usable before/after SHA; nothing to do.'); + return; + } + + // Skip automation-driven updates (e.g. integration/autofix bots). + // Only humans rebasing/force-pushing should get the reminder. + if (context.payload.sender?.type === 'Bot') { + console.log(`Push made by bot "${context.payload.sender.login}"; skipping.`); + return; + } + + // Compare the old tip (`before`) with the new tip (`after`): + // ahead -> new commits added on top of the old tip (normal push) + // identical -> no change + // behind -> reset to an older commit (force-push) + // diverged -> history rewritten, e.g. rebase/amend (force-push) + let status; + try { + const cmp = await github.rest.repos.compareCommitsWithBasehead({ + owner: context.repo.owner, + repo: context.repo.repo, + basehead: `${before}...${after}`, + }); + status = cmp.data.status; + } catch (err) { + // The compare can 404 if the old tip is no longer reachable. + // Stay conservative and skip rather than risk a false accusation. + const reason = err.status || err.message; + console.log(`Could not compare ${before}...${after}: ${reason}. Skipping.`); + return; + } + + console.log(`Compare ${before.slice(0, 7)}...${after.slice(0, 7)} => ${status}`); + if (status === 'ahead' || status === 'identical') { + console.log('Fast-forward push (not a force-push); nothing to do.'); + return; + } + + // Force-push confirmed. Post the reminder at most once per PR; the + // hidden marker lets us detect a reminder we already left. + const MARKER = ''; + const comments = await github.paginate(github.rest.issues.listComments, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + per_page: 100, + }); + if (comments.some((c) => c.body && c.body.includes(MARKER))) { + console.log('Reminder already posted on this PR; skipping.'); + return; + } + + const english = + 'Please do not rebase or force-push to an active PR as it invalidates ' + + 'existing review comments. Note for future reference, the bots always ' + + 'squash all changes into a single commit automatically as part of the ' + + 'integration.'; + const chinese = + '请勿对活跃的 PR 执行 rebase 或 force-push,因为这会使已有的评审评论失效。' + + '另外,供日后参考:作为集成流程的一部分,机器人始终会自动将所有改动' + + '压缩(squash)为单个提交。'; + const body = [ + MARKER, + '', + english, + '', + '
', + '中文', + '', + chinese, + '', + '
', + ].join('\n'); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + body, + }); + console.log(`Posted force-push reminder on PR #${pr.number}.`); From 126674307917f84173aa65ba93dbfa2bffd11c41 Mon Sep 17 00:00:00 2001 From: Shaojin Wen Date: Tue, 30 Jun 2026 13:30:16 +0800 Subject: [PATCH 2/7] =?UTF-8?q?ci(workflows):=20address=20review=20?= =?UTF-8?q?=E2=80=94=20add=20issues:write,=20serialize=20without=20cancel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add `issues: write`: the listComments/createComment calls go through the Issues API; declaring it matches the repo's other PR-commenting workflows and avoids any risk of a 403 making the workflow inert. - Set `cancel-in-progress: false`: an in-flight run that already detected a force-push must finish and post. The concurrency group still serializes runs per PR, and the once-per-PR marker prevents duplicates, so later pushes queue and then no-op instead of cancelling (and silently dropping) a pending reminder. --- .github/workflows/pr-force-push-reminder.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr-force-push-reminder.yml b/.github/workflows/pr-force-push-reminder.yml index ae3601002b6..93c75c9a479 100644 --- a/.github/workflows/pr-force-push-reminder.yml +++ b/.github/workflows/pr-force-push-reminder.yml @@ -7,13 +7,17 @@ on: permissions: contents: 'read' + issues: 'write' pull-requests: 'write' -# One in-flight check per PR; a newer push supersedes the previous one so we -# never post the reminder twice for a rapid burst of force-pushes. +# Serialize runs per PR without cancelling: a run that already detected a +# force-push must be allowed to finish and post, while later pushes queue +# behind it and then no-op via the once-per-PR marker. Cancelling in-progress +# runs could silently drop a reminder when a force-push is immediately +# followed by a normal push (the cancelling run sees 'ahead' and exits). concurrency: group: 'pr-force-push-reminder-${{ github.event.pull_request.number }}' - cancel-in-progress: true + cancel-in-progress: false jobs: remind-on-force-push: From b7aa71fc0852cb48f4ab0a6e1119e1b8cec46a45 Mon Sep 17 00:00:00 2001 From: Shaojin Wen Date: Tue, 30 Jun 2026 14:38:35 +0800 Subject: [PATCH 3/7] ci(workflows): harden force-push detection per review - Marker dedup now requires the comment to be from github-actions[bot], so a user pasting the marker string into a comment can't suppress reminders. - Skip known automation logins (qwen-code-dev-bot et al.) that push via PAT as sender.type 'User', not just GitHub App bots (mirrors qwen-autofix KNOWN_BOTS). - Narrow the compare catch to 404 (orphaned old tip -> skip); rethrow other errors so auth/rate failures go red instead of silently no-op'ing. - Wrap createComment with structured error logging + rethrow. Kept 3-dot compare and base-repo owner: verified that 3-dot returns diverged/behind for force-pushes and that the base repo resolves fork-PR commits, while the suggested 2-dot syntax 404s in the REST API. --- .github/workflows/pr-force-push-reminder.yml | 70 +++++++++++++++----- 1 file changed, 52 insertions(+), 18 deletions(-) diff --git a/.github/workflows/pr-force-push-reminder.yml b/.github/workflows/pr-force-push-reminder.yml index 93c75c9a479..cfc2e16e6fb 100644 --- a/.github/workflows/pr-force-push-reminder.yml +++ b/.github/workflows/pr-force-push-reminder.yml @@ -43,10 +43,22 @@ jobs: return; } - // Skip automation-driven updates (e.g. integration/autofix bots). - // Only humans rebasing/force-pushing should get the reminder. - if (context.payload.sender?.type === 'Bot') { - console.log(`Push made by bot "${context.payload.sender.login}"; skipping.`); + // Skip automation-driven updates. A GitHub App push has + // sender.type === 'Bot', but the repo's own autofix bot pushes + // (including force-pushes) via a PAT as the user account + // qwen-code-dev-bot, which arrives with sender.type === 'User' — so + // also skip known automation logins (mirrors qwen-autofix.yml's + // KNOWN_BOTS). Only human contributors should be reminded. + const sender = context.payload.sender; + const KNOWN_AUTOMATION = new Set([ + 'qwen-code-ci-bot', + 'qwen-code-dev-bot', + 'github-actions', + 'github-actions[bot]', + 'gemini-cli-robot', + ]); + if (sender?.type === 'Bot' || KNOWN_AUTOMATION.has(sender?.login)) { + console.log(`Push made by automation "${sender?.login}" (${sender?.type}); skipping.`); return; } @@ -64,11 +76,16 @@ jobs: }); status = cmp.data.status; } catch (err) { - // The compare can 404 if the old tip is no longer reachable. - // Stay conservative and skip rather than risk a false accusation. - const reason = err.status || err.message; - console.log(`Could not compare ${before}...${after}: ${reason}. Skipping.`); - return; + // A 404 means the old tip (`before`) is no longer reachable — it + // was orphaned by the force-push and already GC'd. Skip + // conservatively rather than risk a false accusation. Any other + // error (403/429/5xx) is a real failure: rethrow so the run goes + // red and the outage is visible instead of a silent no-op. + if (err.status === 404) { + console.log(`Old tip ${before} no longer reachable (404); skipping.`); + return; + } + throw err; } console.log(`Compare ${before.slice(0, 7)}...${after.slice(0, 7)} => ${status}`); @@ -86,8 +103,18 @@ jobs: issue_number: pr.number, per_page: 100, }); - if (comments.some((c) => c.body && c.body.includes(MARKER))) { - console.log('Reminder already posted on this PR; skipping.'); + // Only trust the marker on our own bot's comment — otherwise anyone + // could permanently suppress reminders by pasting the marker string. + if ( + comments.some( + (c) => + c.user?.type === 'Bot' && + c.user?.login === 'github-actions[bot]' && + c.body && + c.body.includes(MARKER), + ) + ) { + console.log('Reminder already posted by the bot on this PR; skipping.'); return; } @@ -113,10 +140,17 @@ jobs: '', ].join('\n'); - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number, - body, - }); - console.log(`Posted force-push reminder on PR #${pr.number}.`); + try { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + body, + }); + console.log(`Posted force-push reminder on PR #${pr.number}.`); + } catch (err) { + // Surface auth/rate-limit/transient failures with context and let + // the run go red instead of failing silently. + core.error(`Failed to comment on PR #${pr.number}: ${err.status} ${err.message}`); + throw err; + } From 48b6c465f8ef3b681d1224bf0282a5344ef682ae Mon Sep 17 00:00:00 2001 From: Shaojin Wen Date: Tue, 30 Jun 2026 20:30:40 +0800 Subject: [PATCH 4/7] test(ci): add structural test for the force-push reminder workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add scripts/tests/pr-force-push-reminder-workflow.test.js (runs under test:scripts, which CI chains into test:ci). It asserts the trigger, repo guard, permissions, serialized concurrency, KNOWN_AUTOMATION sync with qwen-autofix, the 3-dot compare on the base repo, 404-vs-rethrow, the marker author check, and the bilingual body — locking in the reviewed behaviors. - Wrap the listComments paginate call in the same core.error + rethrow the other two API calls already use. - Note that KNOWN_AUTOMATION must stay in sync with qwen-autofix.yml KNOWN_BOTS. --- .github/workflows/pr-force-push-reminder.yml | 19 ++- .../pr-force-push-reminder-workflow.test.js | 127 ++++++++++++++++++ 2 files changed, 140 insertions(+), 6 deletions(-) create mode 100644 scripts/tests/pr-force-push-reminder-workflow.test.js diff --git a/.github/workflows/pr-force-push-reminder.yml b/.github/workflows/pr-force-push-reminder.yml index cfc2e16e6fb..98d1fddad4a 100644 --- a/.github/workflows/pr-force-push-reminder.yml +++ b/.github/workflows/pr-force-push-reminder.yml @@ -50,6 +50,7 @@ jobs: // also skip known automation logins (mirrors qwen-autofix.yml's // KNOWN_BOTS). Only human contributors should be reminded. const sender = context.payload.sender; + // KEEP IN SYNC with KNOWN_BOTS in .github/workflows/qwen-autofix.yml. const KNOWN_AUTOMATION = new Set([ 'qwen-code-ci-bot', 'qwen-code-dev-bot', @@ -97,12 +98,18 @@ jobs: // Force-push confirmed. Post the reminder at most once per PR; the // hidden marker lets us detect a reminder we already left. const MARKER = ''; - const comments = await github.paginate(github.rest.issues.listComments, { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number, - per_page: 100, - }); + let comments; + try { + comments = await github.paginate(github.rest.issues.listComments, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + per_page: 100, + }); + } catch (err) { + core.error(`Failed to list comments on PR #${pr.number}: ${err.status} ${err.message}`); + throw err; + } // Only trust the marker on our own bot's comment — otherwise anyone // could permanently suppress reminders by pasting the marker string. if ( diff --git a/scripts/tests/pr-force-push-reminder-workflow.test.js b/scripts/tests/pr-force-push-reminder-workflow.test.js new file mode 100644 index 00000000000..47013a87b92 --- /dev/null +++ b/scripts/tests/pr-force-push-reminder-workflow.test.js @@ -0,0 +1,127 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { readFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; + +const repoRoot = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '../..', +); + +describe('pr force-push reminder workflow', () => { + const workflow = readFileSync( + path.join(repoRoot, '.github/workflows/pr-force-push-reminder.yml'), + 'utf8', + ); + + it('triggers only on pull_request_target synchronize', () => { + expect(workflow).toContain('pull_request_target:'); + expect(workflow).toContain("- 'synchronize'"); + // Must not check out or run PR code: a github-script-only job needs no + // checkout, which is what keeps pull_request_target safe from pwn-requests. + expect(workflow).not.toContain('actions/checkout'); + }); + + it('only runs on the upstream repo', () => { + expect(workflow).toContain("github.repository == 'QwenLM/qwen-code'"); + }); + + it('grants the permissions the comment endpoints need', () => { + expect(workflow).toContain("contents: 'read'"); + expect(workflow).toContain("issues: 'write'"); + expect(workflow).toContain("pull-requests: 'write'"); + }); + + it('serializes per-PR runs without cancelling in-flight reminders', () => { + expect(workflow).toContain( + "group: 'pr-force-push-reminder-${{ github.event.pull_request.number }}'", + ); + // cancel-in-progress: true would let a normal push cancel an in-flight run + // that already detected a force-push, silently dropping the reminder. + expect(workflow).toContain('cancel-in-progress: false'); + expect(workflow).not.toContain('cancel-in-progress: true'); + }); + + it('bounds the job and pins the github-script action by SHA', () => { + expect(workflow).toContain('timeout-minutes: 5'); + expect(workflow).toContain( + 'actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3', + ); + }); + + it('guards against missing or zero before/after SHAs', () => { + expect(workflow).toContain('!before || !after || /^0+$/.test(before)'); + }); + + it('skips bot and known-automation pushes', () => { + // GitHub Apps arrive as sender.type Bot; the autofix bot force-pushes via a + // PAT as a User account, so its login must be skipped explicitly. + expect(workflow).toContain( + "sender?.type === 'Bot' || KNOWN_AUTOMATION.has(sender?.login)", + ); + for (const login of [ + 'qwen-code-ci-bot', + 'qwen-code-dev-bot', + 'github-actions', + 'github-actions[bot]', + 'gemini-cli-robot', + ]) { + expect(workflow).toContain(`'${login}'`); + } + // The KNOWN_AUTOMATION list mirrors qwen-autofix.yml — drift would let an + // automation account get reminded, so the sync is asserted here. + expect(workflow).toContain( + 'KEEP IN SYNC with KNOWN_BOTS in .github/workflows/qwen-autofix.yml', + ); + }); + + it('detects force-pushes with a 3-dot compare on the base repo', () => { + // Verified against the live REST API: 3-dot returns diverged/behind for + // force-pushes and resolves fork-PR commits, while 2-dot 404s. Do not + // "simplify" this to two dots. + expect(workflow).toContain('basehead: `${before}...${after}`'); + expect(workflow).not.toContain('basehead: `${before}..${after}`'); + // The base repo resolves fork-PR commits via refs/pull/N/head, so the + // compare targets context.repo, not the (possibly deleted) head repo. + expect(workflow).not.toContain('pr.head.repo'); + // Only ahead/identical is a normal push; behind/diverged is a force-push. + expect(workflow).toContain("status === 'ahead' || status === 'identical'"); + }); + + it('skips on a 404 compare but surfaces other errors', () => { + // A 404 means the old tip was orphaned by the force-push; anything else + // (403/429/5xx) must fail the run loudly instead of a silent green no-op. + expect(workflow).toContain('if (err.status === 404)'); + expect(workflow).toContain('throw err;'); + expect(workflow).not.toContain('Could not compare'); + }); + + it('only trusts the dedup marker on its own bot comment', () => { + // Otherwise any user could suppress all future reminders by pasting the + // marker string into a comment. + expect(workflow).toContain(''); + expect(workflow).toContain("c.user?.type === 'Bot'"); + expect(workflow).toContain("c.user?.login === 'github-actions[bot]'"); + }); + + it('wraps every GitHub write/read in error logging that rethrows', () => { + // listComments, compare (via the 404 branch), and createComment must all + // surface failures rather than swallowing them. + expect(workflow).toContain('Failed to list comments on PR #${pr.number}'); + expect(workflow).toContain('Failed to comment on PR #${pr.number}'); + expect(workflow).toContain('core.error('); + }); + + it('posts a bilingual reminder', () => { + expect(workflow).toContain('Please do not rebase or force-push'); + expect(workflow).toContain('squash all changes into a single commit'); + expect(workflow).toContain('中文'); + expect(workflow).toContain('请勿对活跃的 PR 执行 rebase 或 force-push'); + }); +}); From 6868e972cb82303eb89589099de250d35dfdc8de Mon Sep 17 00:00:00 2001 From: Shaojin Wen Date: Tue, 30 Jun 2026 20:59:03 +0800 Subject: [PATCH 5/7] ci(workflows): drop concurrency group, rely on marker for idempotency A concurrency group keeps at most one pending run per group, so a burst of pushes can cancel a still-pending force-push run before it reaches the script, dropping the reminder this workflow exists to post. Remove the group entirely: every synchronize event now runs independently and is always evaluated, and the once-per-PR marker provides idempotency. A rare double-post on two near-simultaneous first force-pushes is the acceptable cost of never silently missing one. Update the structural test to assert there is no concurrency block. The reviewer's suggested `queue: max` is not a valid GitHub Actions concurrency key (only `group`/`cancel-in-progress` are allowed) and fails actionlint. --- .github/workflows/pr-force-push-reminder.yml | 15 +++++++-------- .../tests/pr-force-push-reminder-workflow.test.js | 14 ++++++-------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/.github/workflows/pr-force-push-reminder.yml b/.github/workflows/pr-force-push-reminder.yml index 98d1fddad4a..48f3b72a694 100644 --- a/.github/workflows/pr-force-push-reminder.yml +++ b/.github/workflows/pr-force-push-reminder.yml @@ -10,14 +10,13 @@ permissions: issues: 'write' pull-requests: 'write' -# Serialize runs per PR without cancelling: a run that already detected a -# force-push must be allowed to finish and post, while later pushes queue -# behind it and then no-op via the once-per-PR marker. Cancelling in-progress -# runs could silently drop a reminder when a force-push is immediately -# followed by a normal push (the cancelling run sees 'ahead' and exits). -concurrency: - group: 'pr-force-push-reminder-${{ github.event.pull_request.number }}' - cancel-in-progress: false +# No concurrency group on purpose. GitHub keeps at most one pending run per +# group, so a burst of pushes can cancel a still-pending run that was about to +# post — dropping the very reminder this workflow exists to deliver. Letting +# every synchronize event run independently guarantees each force-push is +# evaluated; idempotency comes from the once-per-PR marker checked in the script +# (not from serializing runs). A rare double-post on two near-simultaneous +# first force-pushes is the acceptable cost of never silently missing one. jobs: remind-on-force-push: diff --git a/scripts/tests/pr-force-push-reminder-workflow.test.js b/scripts/tests/pr-force-push-reminder-workflow.test.js index 47013a87b92..29d2763aa67 100644 --- a/scripts/tests/pr-force-push-reminder-workflow.test.js +++ b/scripts/tests/pr-force-push-reminder-workflow.test.js @@ -38,14 +38,12 @@ describe('pr force-push reminder workflow', () => { expect(workflow).toContain("pull-requests: 'write'"); }); - it('serializes per-PR runs without cancelling in-flight reminders', () => { - expect(workflow).toContain( - "group: 'pr-force-push-reminder-${{ github.event.pull_request.number }}'", - ); - // cancel-in-progress: true would let a normal push cancel an in-flight run - // that already detected a force-push, silently dropping the reminder. - expect(workflow).toContain('cancel-in-progress: false'); - expect(workflow).not.toContain('cancel-in-progress: true'); + it('uses no concurrency group so no push event is ever dropped', () => { + // GitHub keeps at most one pending run per concurrency group, so a group + // could cancel a still-pending force-push run before it posts. Idempotency + // comes from the marker instead, so there must be no concurrency block. + expect(workflow).not.toContain('concurrency:'); + expect(workflow).not.toContain('cancel-in-progress'); }); it('bounds the job and pins the github-script action by SHA', () => { From 3ba3169cd516e0ee06a4a6de8a4dd3831e98270e Mon Sep 17 00:00:00 2001 From: Shaojin Wen Date: Tue, 30 Jun 2026 21:38:15 +0800 Subject: [PATCH 6/7] test(ci): use Qwen Team header and assert the dedup skip path - Switch the copyright header to the prevailing `Qwen Team` (14 of 17 sibling test files use it; this file had copied an older Google LLC header). - Assert the idempotency skip log line so removing the marker guard fails a test. --- scripts/tests/pr-force-push-reminder-workflow.test.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/tests/pr-force-push-reminder-workflow.test.js b/scripts/tests/pr-force-push-reminder-workflow.test.js index 29d2763aa67..2231a93c1e6 100644 --- a/scripts/tests/pr-force-push-reminder-workflow.test.js +++ b/scripts/tests/pr-force-push-reminder-workflow.test.js @@ -1,6 +1,6 @@ /** * @license - * Copyright 2025 Google LLC + * Copyright 2025 Qwen Team * SPDX-License-Identifier: Apache-2.0 */ @@ -106,6 +106,10 @@ describe('pr force-push reminder workflow', () => { expect(workflow).toContain(''); expect(workflow).toContain("c.user?.type === 'Bot'"); expect(workflow).toContain("c.user?.login === 'github-actions[bot]'"); + // Assert the skip path itself, so deleting the guard fails a test. + expect(workflow).toContain( + 'Reminder already posted by the bot on this PR; skipping.', + ); }); it('wraps every GitHub write/read in error logging that rethrows', () => { From be9e2db4effdde63d1d8afb6b58f7b540350abbf Mon Sep 17 00:00:00 2001 From: Shaojin Wen Date: Tue, 30 Jun 2026 21:43:59 +0800 Subject: [PATCH 7/7] test(ci): mechanically enforce KNOWN_AUTOMATION sync with qwen-autofix Read qwen-autofix.yml's KNOWN_BOTS and assert each login is also skipped here, so adding a bot there without updating this workflow fails the test instead of silently drifting. Replaces the hardcoded login list whose comment overclaimed that the sync was verified. --- .../pr-force-push-reminder-workflow.test.js | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/scripts/tests/pr-force-push-reminder-workflow.test.js b/scripts/tests/pr-force-push-reminder-workflow.test.js index 2231a93c1e6..e18db65ede1 100644 --- a/scripts/tests/pr-force-push-reminder-workflow.test.js +++ b/scripts/tests/pr-force-push-reminder-workflow.test.js @@ -63,20 +63,21 @@ describe('pr force-push reminder workflow', () => { expect(workflow).toContain( "sender?.type === 'Bot' || KNOWN_AUTOMATION.has(sender?.login)", ); - for (const login of [ - 'qwen-code-ci-bot', - 'qwen-code-dev-bot', - 'github-actions', - 'github-actions[bot]', - 'gemini-cli-robot', - ]) { - expect(workflow).toContain(`'${login}'`); - } - // The KNOWN_AUTOMATION list mirrors qwen-autofix.yml — drift would let an - // automation account get reminded, so the sync is asserted here. expect(workflow).toContain( 'KEEP IN SYNC with KNOWN_BOTS in .github/workflows/qwen-autofix.yml', ); + // Mechanically enforce the sync: read qwen-autofix.yml's KNOWN_BOTS and + // assert every login is also skipped here, so adding a bot there without + // updating this list fails the test rather than silently drifting. + const autofix = readFileSync( + path.join(repoRoot, '.github/workflows/qwen-autofix.yml'), + 'utf8', + ); + const match = autofix.match(/KNOWN_BOTS:\s*'(\[.*\])'/); + expect(match, 'KNOWN_BOTS not found in qwen-autofix.yml').not.toBeNull(); + for (const login of JSON.parse(match[1])) { + expect(workflow).toContain(`'${login}'`); + } }); it('detects force-pushes with a 3-dot compare on the base repo', () => {