Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/scripts/ci-runner-routing.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,46 @@ describe('ci.yml classify_pr runner routing', () => {
);
});

it('a push to main reaches ECS, and the kill-switch still wins', () => {
// The post-merge fast signal (Classify PR + Test on `main`) runs the same
// ten-plus-minute Test job a pull request does, in the most trusted
// context there is: the code is already merged and the YAML is main's
// own. Route it to the pool instead of spending a scarce hosted Linux
// runner on every merge. A push carries no author association, so this
// arm is necessarily event-based.
assert.equal(
runPickRunner({
ecsDisabled: false,
sameRepo: false,
assoc: '',
eventName: 'push',
}),
ECS,
);
assert.equal(
runPickRunner({
ecsDisabled: true,
sameRepo: false,
assoc: '',
eventName: 'push',
}),
HOSTED,
'kill-switch must revert post-merge runs to hosted',
);
});

it('keeps the push arm out of the classify runs-on expression', () => {
// classify_pr routes twice, and on a push the two halves deliberately
// disagree: the shell step sends downstream Linux jobs to ECS while this
// expression leaves classify_pr itself hosted. Pin the asymmetry so a
// future "drift fix" has to read why first — this expression is the
// canonical association-routing text that sdk-java.yml and serve-ab.yml
// mirror, a push has no association to route on, and classify_pr is a
// seconds-long job whose pool costs nothing either way. The drift guard
// above evaluates both halves on pull_request, where they must agree.
assert.doesNotMatch(classifyRunsOn, /github\.event_name == 'push'/);
});

it('the runs-on expression keeps the trusted clause and kill-switch', () => {
// Structural pins for the expression half of the drift guard — the
// simulation above re-implements it, so pin the real text too.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/.size-baseline
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
9256 build-and-publish-image.yml
49610 cd-cua-driver.yml
2076 cd-mobile-mcp.yml
91370 ci.yml
95501 ci.yml
1482 codeql.yml
9389 comment-attachment-guard.yml
31677 desktop-release.yml
Expand Down
88 changes: 72 additions & 16 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,21 @@
name: 'Qwen Code CI'

on:
# No `push` trigger: every job here is gated to pull_request / merge_group, so
# a push to `main` ran nothing (CodeQL was the last push job and moved to its
# own scheduled codeql.yml). The merge queue validates the merged tree before
# it lands, so there is nothing left to run on the post-merge push.
# This trigger was removed on the premise that the merge queue validates the
# merged tree before it lands. No queue is enabled (none since 2026-07-02),
# so nothing validated that tree on either side of a merge: `main` had no
# gate before one and nothing faster than the ~40-minute E2E after one.
# Only `test` and the `classify_pr` it depends on accept it (see their
# `if`s); every other job excludes push, so a post-merge run is exactly
# Classify PR + Test. The no-AK integration gate is its own
# `integration_no_ak` job on pull_request / merge_group, so this lane is
# lint, static analysis and unit tests. Removing it once the queue is
# enabled means dropping the trigger AND the push arm from BOTH those
# `if`s: a half-removal leaves every merge running Classify PR alone,
# publishing outputs nothing consumes.
push:
branches:
- 'main'
Comment thread
wenshao marked this conversation as resolved.
Resolved
Comment thread
wenshao marked this conversation as resolved.
Resolved
Comment thread
wenshao marked this conversation as resolved.
Resolved
Comment thread
wenshao marked this conversation as resolved.
Resolved
Comment thread
wenshao marked this conversation as resolved.
Resolved
pull_request:
branches:
- 'main'
Expand Down Expand Up @@ -40,7 +51,14 @@ on:
- 'hosted'

concurrency:
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
# Event + source repo + ref, not ref alone. Without the event the group
# collapses onto `Qwen Code CI-refs/heads/main`, shared with the nightly
# (60-minute lanes, neither cancelling on `main`). Without the source repo,
# same-named branches across forks collide — they evaluate in the base
# repo's namespace and PR runs DO cancel in progress, so a fork whose
# default branch is `main` cancels a victim PR's run on every push.
# Supersede within a PR is kept: one branch backs one open PR per fork.
group: '${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.head_ref || github.ref }}'
cancel-in-progress: |-
${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/heads/release/') }}

Expand All @@ -67,12 +85,20 @@ env:
# baseline went stale on main" (#9904). Wired once here so every lane
# inherits it — the gate step and every `npm run test:ci` step, whatever
# it is named — instead of each step hand-wiring a copy.
WORKFLOW_SIZE_BASE_SHA: '${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha }}'
# `github.event.before` is the push lane's base: on a squash-merge push it is
# main's previous tip, restoring the PR lane's leniency for a baseline that
# went stale on main. Without it both enforcers see an empty base and fail
# closed, so the next stale baseline reddens every merge while every PR stays
# green — telling you to "update .size-baseline in this PR" when no PR
# exists. `github.sha` is deliberately not used: on workflow_dispatch it
# resolves to the checked-out commit and would compare the file against
# itself, failing open. An all-zeros `before` still fails closed.
WORKFLOW_SIZE_BASE_SHA: '${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha || github.event.before }}'

jobs:
classify_pr:
name: 'Classify PR'
if: "${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' || github.event_name == 'workflow_dispatch' }}"
if: "${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' || github.event_name == 'workflow_dispatch' || github.event_name == 'push' }}"
Comment thread
wenshao marked this conversation as resolved.
Resolved
# Gate runs on ECS for in-repo PRs, fork PRs whose author has write access
# (OWNER/MEMBER/COLLABORATOR association — a write-access author is as
# trusted as an in-repo branch), and the merge queue (base-repo context),
Expand Down Expand Up @@ -127,9 +153,17 @@ jobs:
echo "skip_ci=${skip_ci}"

# In-repo PRs, fork PRs whose author has write access
# (OWNER/MEMBER/COLLABORATOR association), and the merge queue
# (base-repo context) run the Linux jobs on ECS; other fork PRs stay
# hosted. Disable via repo var MAINTAINER_ECS_RUNNER_DISABLED=true.
# (OWNER/MEMBER/COLLABORATOR association), the merge queue (base-repo
# context) and a push to `main` run the Linux jobs on ECS; other fork
# PRs stay hosted. Disable via repo var
# MAINTAINER_ECS_RUNNER_DISABLED=true.
#
# push is deliberately absent from the `runs-on` expression above, so a
# post-merge run leaves classify_pr hosted while the Test job it feeds
# goes to ECS. That is intentional: the expression is the canonical
# association-routing text sdk-java.yml and serve-ab.yml mirror, a push
# has no author association to route on, and classify_pr costs seconds —
# while Test should not spend a scarce hosted runner on every merge.
- name: 'Select Linux runner'
id: 'pick_runner'
env:
Expand All @@ -148,7 +182,7 @@ jobs:
if [[ "${ECS_DISABLED}" != "true" && "${DISPATCH_LINUX_RUNNER}" == "self-hosted" ]]; then
ubuntu_runner='["self-hosted", "linux", "x64", "ecs-qwen"]'
fi
elif [[ "${ECS_DISABLED}" != "true" && ( "${SAME_REPO}" == "true" || "${trusted_author}" == "true" || "${EVENT_NAME}" == "merge_group" ) ]]; then
elif [[ "${ECS_DISABLED}" != "true" && ( "${SAME_REPO}" == "true" || "${trusted_author}" == "true" || "${EVENT_NAME}" == "merge_group" || "${EVENT_NAME}" == "push" ) ]]; then
ubuntu_runner='["self-hosted", "linux", "x64", "ecs-qwen"]'
fi
echo "ubuntu_runner=${ubuntu_runner}" >> "${GITHUB_OUTPUT}"
Expand All @@ -162,9 +196,18 @@ jobs:
needs: 'classify_pr'
# Stay running on release-sync PRs so the required Test contexts still
# report; the per-step skip_ci guards below make them no-op (pass) there.
# Not on push: the merge queue already tested the merged tree, so a
# post-merge re-run on `main` would be redundant.
if: "${{ !cancelled() && github.event_name != 'push' && github.event_name != 'schedule' }}"
# Accepts push on `main`: this is the only post-merge check faster than the
# ~40-minute E2E, and with no merge queue enabled nothing validates the
# merged tree before it lands either. The expensive and PR-shaped steps
# stay off it on their own gates, each by its own mechanism — the
# checkout-head verifier is restricted to pull_request / merge_group,
# while the test reporter carries no event gate at all: its same-repo
# clause (head.repo.full_name == github.repository) is empty on push. Do
# not "normalise" that clause into the event allowlist — it also keeps
# fork PRs out, and dorny/test-reporter needs `checks: write`, which
# fork-event runs do not have. So a push run is lint, static analysis and
# unit tests.
if: "${{ !cancelled() && github.event_name != 'schedule' }}"
Comment thread
wenshao marked this conversation as resolved.
Resolved
Comment thread
wenshao marked this conversation as resolved.
Resolved
runs-on: '${{ fromJSON(needs.classify_pr.outputs.ubuntu_runner || ''["ubuntu-latest"]'') }}'
timeout-minutes: 60
outputs:
Expand Down Expand Up @@ -281,7 +324,17 @@ jobs:
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}"
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
with:
ref: "${{ github.event.inputs.branch_ref || (github.event_name == 'pull_request' && format('refs/pull/{0}/head', github.event.pull_request.number)) || (github.event_name == 'merge_group' && github.event.merge_group.head_sha) || github.ref }}"
# Pin push runs to the event SHA: the `github.ref` fallback resolves
# `refs/heads/main` at fetch time — a moving tip — while the check run
# attaches to the triggering commit, so the lane would validate a tree
# it does not report on. Nothing detects that (the checkout-head
# verifier is gated to pull_request / merge_group, and its ancestor
# check passes for a newer tip regardless), and merges land close
# enough together to hit it routinely. Regressions are still caught —
# the tested tree is a descendant — but attribution breaks: red on a
# clean commit, autofix filed against the wrong merge. `e2e.yml` pins
# the event SHA by taking actions/checkout's default.
ref: "${{ github.event.inputs.branch_ref || (github.event_name == 'pull_request' && format('refs/pull/{0}/head', github.event.pull_request.number)) || (github.event_name == 'merge_group' && github.event.merge_group.head_sha) || (github.event_name == 'push' && github.sha) || github.ref }}"
# Shallow: nothing here walks git history except on demand (the
# verify guard below checks head.sha == HEAD; the size gate and its
# vitest mirror fetch the PR's base commit at depth 1 when the
Expand Down Expand Up @@ -619,8 +672,11 @@ jobs:
name: 'test-results-fork-22.x-ubuntu-latest'
path: 'packages/*/junit.xml'

# Excludes push: the only consumer, `post_coverage_comment`, is gated to
# pull_request and nothing else reads `coverage-reports-*`, so uploading
# on every merge spends egress and retention on a lane with no reader.
- name: 'Upload coverage reports'
if: "${{ always() && needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"
if: "${{ always() && needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' && github.event_name != 'push' }}"
uses: 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' # v7.0.1
with:
name: 'coverage-reports-22.x-ubuntu-latest'
Expand Down
13 changes: 9 additions & 4 deletions .github/workflows/main-ci-failure-issue.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@ name: 'Main CI Failure Issue'

on:
workflow_run:
# 'Qwen Code CI' is watched for its SCHEDULED run only (see the event
# filter below): that nightly is the macOS and Windows lanes' only
# trigger outside a pull request, and a lane nobody is told about is a
# lane nobody looks at — which is how those two came to stop running.
# 'Qwen Code CI' is watched for its SCHEDULED run and its post-merge PUSH
# run (see the event filter below — the push arm is not name-scoped, so it
# covers this workflow too). The nightly is the macOS and Windows lanes'
# only trigger outside a pull request, and a lane nobody is told about is a
# lane nobody looks at — which is how those two came to stop running. The
# push arm covers ci.yml's post-merge lane on `main`, whose whole point is
# to surface a regression as it lands; a failure there files an issue with
# `type/bug`, `status/ready-for-agent` and `autofix/approved`, so expect
# one per red merge, not per red nightly only.
workflows: ['E2E Tests', 'SDK Python', 'Qwen Code CI']
types: ['completed']
# Trigger-level branch filter, not only the `if` below: 'Qwen Code CI'
Expand Down
8 changes: 6 additions & 2 deletions .github/workflows/qwen-autofix.md
Original file line number Diff line number Diff line change
Expand Up @@ -975,8 +975,12 @@ signal, NOT proof that main is healthy.

MAIN_GREEN_CHECKS is sourced from the last-merged PR's PRE-MERGE
check-runs, which ran against that PR merged with main-as-of-then —
never the tree now on main (ci.yml has no push trigger, so main's
squash commits carry no check-runs to read). main breaks here by
never the tree now on main. (ci.yml's post-merge push lane DOES put
check-runs on main's squash commits now, but that lane is lint, static
analysis and unit tests only — the no-AK integration gate and every
platform lane stay off the push trigger — so it is a strictly narrower
signal than a PR's full matrix, and this deliberately stays on the
pre-merge runs.) main breaks here by
SEMANTIC CONFLICT: two PRs green apart but broken together. In exactly
that state the last-merged PR is green, this signal reads green, and
the update would merge a currently-broken main into a healthy PR. The
Expand Down
Loading
Loading