diff --git a/.github/workflows/qwen-triage.yml b/.github/workflows/qwen-triage.yml index b867587bcba..3a15d8bcebf 100644 --- a/.github/workflows/qwen-triage.yml +++ b/.github/workflows/qwen-triage.yml @@ -449,7 +449,10 @@ jobs: concurrency: # GitHub evaluates concurrency before the job `if`, but after `needs`. # Keep non-runnable PR/comment triggers out of the shared per-number - # group so they cannot cancel or replace an authorized run. + # group so they cannot cancel or replace an authorized run — including + # bot-created issues runs (#9264): the job `if` skips them, but a run + # left in the shared group would still cancel an in-progress triage of + # the same issue before its own skip is evaluated. group: >- ${{ ( @@ -459,7 +462,10 @@ jobs: (github.event_name == 'issue_comment' && (github.event.issue.state != 'open' || needs.authorize.outputs.should_run != 'true' || - !startsWith(github.event.comment.body, '@qwen-code /triage'))) + !startsWith(github.event.comment.body, '@qwen-code /triage'))) || + (github.event_name == 'issues' && + github.event.issue.user.login == + (vars.AUTOFIX_BOT_LOGIN || 'qwen-code-dev-bot')) ) && format('{0}-run-{1}', github.workflow, github.run_id) || format('{0}-{1}', github.workflow, github.event.issue.number || github.event.pull_request.number || github.event.inputs.number) @@ -501,9 +507,17 @@ jobs: # mention the phrase in quoted text or mid-sentence descriptions. # always() so the job still evaluates when the upstream `authorize` job is # skipped (issues / workflow_dispatch paths, which need no permission gate). + # The issues clause is conditioned on the creator NOT being the autofix + # bot (#9264): every PR that defers findings for the first time opens a + # tracking issue upserted by that bot, and the open issues trigger triaged + # the bookkeeping issue with a full agent run per deferral. The identity + # is the same one qwen-autofix.yml upserts under (AUTOFIX_BOT), so the + # guard tracks a rename on either side via the shared variable. if: >- always() && ( - github.event_name == 'issues' || + (github.event_name == 'issues' && + github.event.issue.user.login != + (vars.AUTOFIX_BOT_LOGIN || 'qwen-code-dev-bot')) || (github.event_name == 'workflow_dispatch' && github.event.inputs.number != '' && github.event.inputs.tmux_pr == '') || diff --git a/scripts/tests/qwen-triage-workflow.test.js b/scripts/tests/qwen-triage-workflow.test.js index 9f121ad0191..fc273f8e872 100644 --- a/scripts/tests/qwen-triage-workflow.test.js +++ b/scripts/tests/qwen-triage-workflow.test.js @@ -5982,3 +5982,46 @@ describe('triage job budget', () => { } }); }); + +describe('triage skips the autofix bot’s own bookkeeping issues (#9264)', () => { + // Every PR that defers findings for the first time opens a tracking issue + // upserted by the autofix bot, and `issues: [opened, edited, reopened]` + // triaged that bookkeeping issue with a full agent run per deferral. The + // guard keys on the same identity qwen-autofix.yml upserts under, so a + // rename on one side without the other silently re-opens the waste. + const botIdentityCore = "vars.AUTOFIX_BOT_LOGIN || 'qwen-code-dev-bot'"; + const botIdentity = `(${botIdentityCore})`; + + // The parsed expressions keep their YAML line breaks, so whitespace is + // normalized before matching — the pin must survive a re-wrap, not test it. + const flat = (value) => String(value).replace(/\s+/g, ' '); + + it('conditions the triage job’s issues clause on the creator not being the bot', () => { + // Parsed, not raw-text containment: a commented-out guard would still + // match a substring pin. + const doc = parse(workflow); + expect(flat(doc.jobs.triage.if)).toContain( + `(github.event_name == 'issues' && github.event.issue.user.login != ${botIdentity}) || (github.event_name == 'workflow_dispatch'`, + ); + }); + + it('routes bot-created issues runs to a per-run concurrency group', () => { + // GitHub evaluates concurrency BEFORE the job `if`: a bot bookkeeping run + // inside the shared per-number group cancels an in-progress triage of the + // same issue even though its own job skips. + const doc = parse(workflow); + expect(flat(doc.jobs.triage.concurrency.group)).toContain( + `!startsWith(github.event.comment.body, '@qwen-code /triage'))) || (github.event_name == 'issues' && github.event.issue.user.login == ${botIdentity}) ) && format('{0}-run-{1}', github.workflow, github.run_id)`, + ); + }); + + it('keeps the guard identity in sync with the autofix workflow', () => { + // qwen-autofix.yml defines AUTOFIX_BOT as the same variable-with-fallback + // (inside a bare `${{ }}`, so without the expression's parentheses). + expect(botIdentity).toContain(botIdentityCore); + const autofixDoc = parse( + readFileSync('.github/workflows/qwen-autofix.yml', 'utf8'), + ); + expect(autofixDoc.env.AUTOFIX_BOT).toBe(`\${{ ${botIdentityCore} }}`); + }); +});