-
Notifications
You must be signed in to change notification settings - Fork 3k
fix(ci): cap E2E lanes and auto-retry transient main failures once #10229
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| # Re-runs the failed lanes of a post-merge E2E run once before leaving main | ||
| # red. The recurring 'Main CI failed: E2E Tests' issues are overwhelmingly | ||
| # transient model-endpoint connectivity flakes, not regressions: run 32966197395 | ||
| # (2026-08-26) failed a Linux shard with `connect ETIMEDOUT` to the endpoint | ||
| # and passed the identical tree on attempt 2. One automatic re-run turns those | ||
| # green again; a failure that survives both attempts stays red and still | ||
| # raises the Main CI Failure issue. | ||
|
|
||
| name: 'E2E Flaky Rerun' | ||
|
|
||
| on: | ||
| workflow_run: | ||
| workflows: ['E2E Tests'] | ||
| types: ['completed'] | ||
| branches: ['main'] | ||
|
|
||
| permissions: | ||
| contents: 'read' | ||
|
|
||
| jobs: | ||
| rerun-failed-lanes: | ||
| name: 'Re-run failed E2E lanes (once)' | ||
| # First-attempt push runs only: the attempt-2 completion event carries | ||
| # run_attempt == 2, so the re-run cannot re-trigger itself, and a failure | ||
| # that survives both attempts reaches the issue bot untouched. Scheduled | ||
| # and dispatched runs stay manual: they exist to surface flakiness, not to | ||
| # gate main. | ||
| if: |- | ||
| ${{ github.repository == 'QwenLM/qwen-code' && github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.event == 'push' && github.event.workflow_run.head_branch == 'main' && github.event.workflow_run.run_attempt == 1 }} | ||
| # Deliberately hosted, NOT the ECS pool — same reasoning as | ||
| # main-ci-failure-issue.yml#file_issue: a recovery job must not queue | ||
| # behind the infrastructure it is recovering from. | ||
|
Comment on lines
+30
to
+32
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The deliberate expect(job['runs-on']).toBe('ubuntu-latest');Acceptance check: with that assertion in place, change — qwen3.8-max via Qwen Code /review (v0.22.2) |
||
| runs-on: 'ubuntu-latest' | ||
| timeout-minutes: 5 | ||
| steps: | ||
| # Checks out nothing and runs no repository code: the job holding the | ||
| # bot PAT only calls the Actions API. | ||
| - name: 'Re-run the failed jobs' | ||
| env: | ||
| # GitHub writes use CI_BOT_PAT; keep the generated GITHUB_TOKEN | ||
| # read-only. | ||
| GH_TOKEN: '${{ secrets.CI_BOT_PAT }}' | ||
| REPO: '${{ github.repository }}' | ||
| RUN_ID: '${{ github.event.workflow_run.id }}' | ||
| run: |- | ||
| gh run rerun --failed "${RUN_ID}" --repo "${REPO}" | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,53 @@ | ||||||
| /** | ||||||
| * @license | ||||||
| * Copyright 2026 Qwen Team | ||||||
| * SPDX-License-Identifier: Apache-2.0 | ||||||
| */ | ||||||
|
|
||||||
| import { readFileSync } from 'node:fs'; | ||||||
| import { describe, expect, it } from 'vitest'; | ||||||
| import { parse } from 'yaml'; | ||||||
|
|
||||||
| describe('e2e flaky rerun workflow', () => { | ||||||
| const workflow = readFileSync( | ||||||
| '.github/workflows/e2e-flaky-rerun.yml', | ||||||
| 'utf8', | ||||||
| ); | ||||||
| const yml = parse(workflow); | ||||||
| const job = yml.jobs['rerun-failed-lanes']; | ||||||
|
|
||||||
| it('watches completed E2E Tests runs on main only', () => { | ||||||
| expect(workflow).toContain('workflow_run:'); | ||||||
| expect(yml.on.workflow_run.workflows).toEqual(['E2E Tests']); | ||||||
| expect(yml.on.workflow_run.types).toEqual(['completed']); | ||||||
| expect(yml.on.workflow_run.branches).toEqual(['main']); | ||||||
| }); | ||||||
|
|
||||||
| it('retries only first-attempt failed push runs, exactly once', () => { | ||||||
| // The attempt-2 completion event carries run_attempt == 2: without that | ||||||
| // guard the re-run could re-trigger itself forever, and with it a failure | ||||||
| // that survives both attempts reaches the issue bot untouched. Scheduled | ||||||
| // and dispatched runs exist to surface flakiness, not to gate main, so | ||||||
| // push is the only event that earns a retry. Pin the whole clause so a | ||||||
| // connective mutation fails here. | ||||||
|
Comment on lines
+31
to
+32
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] This comment promises that a connective mutation fails here, but the five expect(job.if).toBe(
"${{ github.repository == 'QwenLM/qwen-code' && github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.event == 'push' && github.event.workflow_run.head_branch == 'main' && github.event.workflow_run.run_attempt == 1 }}",
);Acceptance check: with the exact match in place, flip any — qwen3.8-max via Qwen Code /review (v0.22.2) |
||||||
| expect(job.if).toContain("github.repository == 'QwenLM/qwen-code'"); | ||||||
| expect(job.if).toContain( | ||||||
| "github.event.workflow_run.conclusion == 'failure'", | ||||||
| ); | ||||||
| expect(job.if).toContain("github.event.workflow_run.event == 'push'"); | ||||||
| expect(job.if).toContain("github.event.workflow_run.head_branch == 'main'"); | ||||||
| expect(job.if).toContain('github.event.workflow_run.run_attempt == 1'); | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] This pin is a prefix substring, so a value mutation to
Suggested change
Acceptance check: with the tightened pin, mutate the workflow guard to — qwen3.8-max via Qwen Code /review (v0.22.2) |
||||||
| }); | ||||||
|
|
||||||
| it('re-runs only the failed jobs with the bot PAT, not repository code', () => { | ||||||
| // The job holding the PAT must stay a pure Actions-API call: no checkout, | ||||||
| // no repository code. `--failed` keeps the green lanes' results so the | ||||||
| // re-run covers exactly the lanes that flaked. | ||||||
| expect(workflow).not.toContain('actions/checkout'); | ||||||
| expect(workflow).toContain("GH_TOKEN: '${{ secrets.CI_BOT_PAT }}'"); | ||||||
| expect(workflow).toContain( | ||||||
| 'gh run rerun --failed "${RUN_ID}" --repo "${REPO}"', | ||||||
| ); | ||||||
| expect(workflow).toContain("RUN_ID: '${{ github.event.workflow_run.id }}'"); | ||||||
| }); | ||||||
| }); | ||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -32,4 +32,20 @@ describe('e2e workflow', () => { | |||||
| expect(group).toContain('github.event_name'); | ||||||
| expect(group).toContain('github.head_ref || github.ref_name'); | ||||||
| }); | ||||||
|
|
||||||
| it('caps every lane so a hung model endpoint cannot stall the run', () => { | ||||||
| // A lane that loses connectivity to the model endpoint burns every test's | ||||||
| // full 300s timeout before reporting — the 2026-08-26 run took over | ||||||
| // 4 hours to fail while healthy lanes finish in ~17 minutes. Dropping a | ||||||
| // cap would silently reintroduce that, so every lane's timeout is pinned. | ||||||
| const lanes = [ | ||||||
| 'e2e-test-linux', | ||||||
| 'e2e-test-macos', | ||||||
| 'isolated-nightly', | ||||||
| 'web-shell-browser-regression', | ||||||
| ]; | ||||||
|
Comment on lines
+41
to
+46
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The pin hand-enumerates today's four job ids, so it silently stops covering any lane added later. Verified by mutation at this commit: appending a fifth job to for (const [lane, job] of Object.entries(yml.jobs)) {
expect(job['timeout-minutes'], lane).toBe(60);
}(allow named exceptions explicitly if a future job legitimately needs no cap). Acceptance check: with the quantified loop, delete — qwen3.8-max via Qwen Code /review (v0.22.2) |
||||||
| for (const lane of lanes) { | ||||||
| expect(yml.jobs[lane]['timeout-minutes'], lane).toBeGreaterThan(0); | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion]
Suggested change
Acceptance check: with the pin tightened, change any lane's — qwen3.8-max via Qwen Code /review (v0.22.2) |
||||||
| } | ||||||
| }); | ||||||
| }); | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Critical] The Main CI Failure issue bot races this watcher, so a first-attempt flake still files and autofix-routes a "Main CI failed: E2E Tests" issue even though the re-run heals it.
main-ci-failure-issue.ymlsubscribes to the sameworkflow_runcompletion, and itsanalyzeguard checks onlyconclusion == 'failure' && head_branch == 'main' && event == 'push'—run_attemptappears nowhere in that file (verified at this commit). On a push to main where one shard flakes withconnect ETIMEDOUT— the exact incident this header cites (run 32966197395) — the single attempt-1 completion event fires both watchers at once: this one starts attempt 2, while the issue bot downloads the attempt-1 logs, files the issue, appliestype/bug+status/ready-for-agent+autofix/approved, and assigns the autofix bot. When attempt 2 turns green ~20-40 minutes later, nothing closes or de-routes the issue (the bot only acts onconclusion == 'failure', and no workflow in.github/closes issues), so an autofix agent is dispatched at a healthy tree — on every absorbed flake. This comment says only "a failure that survives both attempts" raises the issue; that is false of the wiring as it stands, and before this change a filed issue at least coincided with a red main.Witness (live state observed at this commit):
Coordinate the two responders: gate the issue bot's
analyzepath on the retried attempt for E2E push runs — e.g. append&& (github.event.workflow_run.name != 'E2E Tests' || github.event.workflow_run.run_attempt > 1)inmain-ci-failure-issue.yml, leaving SDK Python and scheduled "Qwen Code CI" untouched since neither has a rerun watcher — and decide the fallback explicitly for the case where this watcher itself never fires (expired PAT, API outage), so an attempt-1 failure is not silently unfiled. If first-attempt filing is intended to stay, correct this comment and the "reaches the issue bot untouched" note instead.main-ci-failure-issue.ymlis outside this diff, so a linked follow-up is fine. If the gate is added, extendscripts/tests/main-ci-failure-issue-workflow.test.jsto pin the new attempt-exclusion clause — removing the clause must turn that test red.— qwen3.8-max via Qwen Code /review (v0.22.2)