Skip to content

Commit 1f670a1

Browse files
Merge branch 'main' into feat/review-emit-workflow-v2
2 parents 891b6ac + 8beaa8d commit 1f670a1

374 files changed

Lines changed: 42969 additions & 2588 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/scripts/ci-runner-routing.test.mjs

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
// Runner-routing regression guards for ci.yml and serve-ab.yml.
1+
// Runner-routing regression guards for ci.yml, serve-ab.yml, and the
2+
// qwen-autofix.yml scan lane.
23
//
34
// classify_pr carries the routing logic TWICE — the `runs-on` expression
45
// (which selects the classify job's own runner) and the `pick_runner` shell
@@ -68,6 +69,13 @@ function evalRunsOn(expression, { ecsDisabled, eventName, sameRepo, assoc }) {
6869
/github\.event_name == 'merge_group'/,
6970
String(eventName === 'merge_group'),
7071
],
72+
// Longest term first as a convention; both patterns are quote-anchored
73+
// (the closing quote is part of each regex), so neither can match
74+
// inside the other and the substitution order is behaviorally inert.
75+
[
76+
/github\.event_name != 'pull_request_review'/,
77+
String(eventName !== 'pull_request_review'),
78+
],
7179
[
7280
/github\.event_name != 'pull_request'/,
7381
String(eventName !== 'pull_request'),
@@ -80,6 +88,7 @@ function evalRunsOn(expression, { ecsDisabled, eventName, sameRepo, assoc }) {
8088
/contains\(fromJSON\('\["OWNER","MEMBER","COLLABORATOR"\]'\), github\.event\.pull_request\.author_association\)/,
8189
String(TRUSTED.includes(assoc)),
8290
],
91+
[/github\.repository == 'QwenLM\/qwen-code'/, 'true'],
8392
];
8493
let expr = expression.replace(/^\$\{\{\s*/, '').replace(/\s*\}\}$/, '');
8594
for (const [term, value] of substitutions) {
@@ -421,3 +430,92 @@ describe('serve-ab.yml runner routing', () => {
421430
);
422431
});
423432
});
433+
434+
describe('qwen-autofix.yml scan-lane runner routing', () => {
435+
// route and review-scan gate the WHOLE fan-out: while they sit queued no
436+
// review-address leg starts. A hosted-runner backlog queued them past the
437+
// cron period, and the cron supersede rule then starved every scan round
438+
// (2026-08-25) — so pin the lane on the persistent pool, with the
439+
// fork-trust clause and the kill-switch intact.
440+
const autofixDoc = parse(
441+
readFileSync(join(workflowsDir, 'qwen-autofix.yml'), 'utf8'),
442+
);
443+
// evalRunsOn unwraps the winning fromJSON label to the array it names, so
444+
// compare against arrays, not the ECS/HOSTED string constants above.
445+
const ECS_LABELS = ['self-hosted', 'linux', 'x64', 'ecs-qwen'];
446+
const HOSTED_LABELS = ['ubuntu-latest'];
447+
448+
for (const jobName of ['route', 'review-scan']) {
449+
const runsOn = String(autofixDoc.jobs[jobName]['runs-on']);
450+
451+
it(`${jobName} reaches the persistent pool on schedule, dispatch, issue_comment, and issues`, () => {
452+
// issue_comment is route's /takeover and /retry lane, issues its
453+
// label/assign trigger lane for issue-autofix — pin both beside the
454+
// cron and dispatch triggers so a later event-allowlist narrowing of
455+
// the pool clause cannot silently demote either back to hosted.
456+
for (const eventName of [
457+
'schedule',
458+
'workflow_dispatch',
459+
'issue_comment',
460+
'issues',
461+
]) {
462+
assert.deepEqual(
463+
evalRunsOn(runsOn, {
464+
ecsDisabled: false,
465+
eventName,
466+
sameRepo: false,
467+
assoc: '',
468+
}),
469+
ECS_LABELS,
470+
`${jobName} must scan from the pool on ${eventName}`,
471+
);
472+
}
473+
});
474+
475+
it(`${jobName} keeps untrusted fork PR lanes hosted`, () => {
476+
for (const eventName of ['pull_request', 'pull_request_review']) {
477+
assert.deepEqual(
478+
evalRunsOn(runsOn, {
479+
ecsDisabled: false,
480+
eventName,
481+
sameRepo: false,
482+
assoc: 'NONE',
483+
}),
484+
HOSTED_LABELS,
485+
`${jobName} fork lane (${eventName}) must stay hosted`,
486+
);
487+
assert.deepEqual(
488+
evalRunsOn(runsOn, {
489+
ecsDisabled: false,
490+
eventName,
491+
sameRepo: true,
492+
assoc: 'NONE',
493+
}),
494+
ECS_LABELS,
495+
`${jobName} same-repo lane (${eventName}) must reach the pool`,
496+
);
497+
}
498+
});
499+
500+
it(`${jobName} obeys the kill-switch on every event`, () => {
501+
for (const eventName of [
502+
'schedule',
503+
'workflow_dispatch',
504+
'issue_comment',
505+
'pull_request',
506+
'pull_request_review',
507+
]) {
508+
assert.deepEqual(
509+
evalRunsOn(runsOn, {
510+
ecsDisabled: true,
511+
eventName,
512+
sameRepo: true,
513+
assoc: 'OWNER',
514+
}),
515+
HOSTED_LABELS,
516+
`kill-switch must win on ${eventName}`,
517+
);
518+
}
519+
});
520+
}
521+
});

.github/workflows/.size-baseline

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
6495 pr-self-report-label.yml
3535
9646 qwen-autofix-fork-bridge.yml
3636
5942 qwen-autofix-fork-signal.yml
37-
397656 qwen-autofix.yml
37+
404284 qwen-autofix.yml
3838
7061 qwen-ci-flaky-rerun.yml
3939
158010 qwen-code-pr-review.yml
4040
79041 qwen-fleet-shepherd.yml

.github/workflows/cd-cua-driver.yml

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ jobs:
8080
exit 1
8181
fi
8282
83+
DRIVER_VERSION=$(tr -d '[:space:]' < packages/cua-driver/rust/VERSION)
84+
if [[ "$DRIVER_VERSION" != "$VERSION" ]]; then
85+
echo "::error::Release version $VERSION does not match driver version $DRIVER_VERSION"
86+
exit 1
87+
fi
88+
8389
SDK_NAME=$(node -p "require('./packages/cua-driver/typescript/package.json').name")
8490
SDK_VERSION=$(node -p "require('./packages/cua-driver/typescript/package.json').version")
8591
if [[ "$SDK_NAME" != "@qwen-code/cua-sdk" ]]; then
@@ -803,14 +809,6 @@ jobs:
803809
with:
804810
name: 'cua-sdk-npm-${{ needs.validate-version.outputs.version }}'
805811
path: '${{ runner.temp }}/cua-sdk-package'
806-
- name: 'Require npm publishing token'
807-
env:
808-
NODE_AUTH_TOKEN: '${{ secrets.NPM_TOKEN }}'
809-
run: |
810-
if [[ -z "$NODE_AUTH_TOKEN" ]]; then
811-
echo '::error::production-release NPM_TOKEN is required for the first @qwen-code/cua-sdk publication'
812-
exit 1
813-
fi
814812
- name: 'Clean-install against the published GitHub Release'
815813
env:
816814
QWEN_CUA_SDK_CACHE_DIR: '${{ runner.temp }}/public-release-cache'
@@ -864,7 +862,6 @@ jobs:
864862
'
865863
- name: 'Publish immutable SDK tarball'
866864
env:
867-
NODE_AUTH_TOKEN: '${{ secrets.NPM_TOKEN }}'
868865
VERSION: '${{ needs.validate-version.outputs.version }}'
869866
run: |
870867
set -euo pipefail
@@ -934,17 +931,8 @@ jobs:
934931
name: 'node-repl-mcp-npm-${{
935932
needs.validate-version.outputs.node_repl_version }}'
936933
path: '${{ runner.temp }}/node-repl-package'
937-
- name: 'Require npm publishing token'
938-
env:
939-
NODE_AUTH_TOKEN: '${{ secrets.NPM_TOKEN }}'
940-
run: |
941-
if [[ -z "$NODE_AUTH_TOKEN" ]]; then
942-
echo '::error::production-release NPM_TOKEN is required for the first @qwen-code/node-repl-mcp publication'
943-
exit 1
944-
fi
945934
- name: 'Publish immutable Node REPL tarball'
946935
env:
947-
NODE_AUTH_TOKEN: '${{ secrets.NPM_TOKEN }}'
948936
VERSION: '${{ needs.validate-version.outputs.node_repl_version }}'
949937
run: |
950938
set -euo pipefail

.github/workflows/ci.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,6 +1029,18 @@ jobs:
10291029
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && runner.environment == 'self-hosted' }}"
10301030
uses: './.github/actions/configure-windows-runner'
10311031

1032+
# Hosted runners expose TEMP through an 8.3 short alias. Self-hosted
1033+
# runners keep their configured, alias-free RUNNER_TEMP path.
1034+
- name: 'Point temp at a short-alias-free directory'
1035+
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && runner.environment != 'self-hosted' }}"
1036+
shell: 'powershell'
1037+
run: |-
1038+
$temp = Join-Path $env:RUNNER_WORKSPACE 'qwen-code-temp'
1039+
New-Item -ItemType Directory -Force -Path $temp | Out-Null
1040+
"TEMP=$temp" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
1041+
"TMP=$temp" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
1042+
Write-Host "TEMP set to $temp"
1043+
10321044
# Same stale-checkout guard as the Ubuntu gate: this job now runs on ECS,
10331045
# so fail loud if the checkout lacks the merge-queue head rather than
10341046
# silently testing the wrong tree into a merge.
@@ -1059,6 +1071,19 @@ jobs:
10591071
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && runner.environment == 'self-hosted' }}"
10601072
uses: './.github/actions/self-hosted-node'
10611073

1074+
# Compare case-insensitively: Windows paths are case-insensitive and
1075+
# realpathSync returns the on-disk casing, so a drive-letter or
1076+
# directory-case difference is the SAME directory under one spelling,
1077+
# not the 8.3 alias this guards. A strict !== there would fail the
1078+
# self-hosted lane too — it runs this step against the runner's
1079+
# pre-existing RUNNER_TEMP — for a reason unrelated to the alias. The
1080+
# alias case (RUNNER~1 -> runneradmin) differs by more than casing and
1081+
# still fails; a casing-only difference warns so it stays visible.
1082+
- name: 'Verify temp paths carry no short alias'
1083+
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}"
1084+
run: |-
1085+
node -e "const fs = require('node:fs'); for (const key of ['TEMP', 'TMP']) { const value = process.env[key]; if (!value) { console.error(key + ' is not set'); process.exitCode = 1; continue; } const real = fs.realpathSync(value); if (real === value) continue; if (real.toLowerCase() === value.toLowerCase()) { console.log('::warning::' + key + ' differs from its real path only by casing: ' + value + ' -> ' + real); continue; } console.error(key + ' carries a short alias: ' + value + ' -> ' + real); process.exitCode = 1; }"
1086+
10621087
- name: 'Configure persistent npm cache (self-hosted)'
10631088
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && runner.environment == 'self-hosted' }}"
10641089
run: |-

.github/workflows/qwen-autofix.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ task-oriented guides — what a maintainer types and what happens next — see:
249249
- [145. review-address · Report dry-run / failure — CUMULATIVE timeout breaker — the sibling of the consecutive one above, for the…](#af-145)
250250
- [146. review-address · Report dry-run / failure — The agent committed (verify recorded committed=true before any gate could fail),…](#af-146)
251251
- [147. review-address · Report dry-run / failure — Same byte-budget hygiene as the English excerpt above. 3000 bytes ≈ 1000 CJK…](#af-147)
252+
- [148. route — Persistent pool, not hosted: a hosted backlog queued route past the cron period, and af-005's…](#af-148)
252253

253254
---
254255

@@ -3727,3 +3728,43 @@ forbids HTML in failure.zh.md), but must not be able to open
37273728
or close a <details>/<summary> that swallows the closing tag
37283729
the workflow emits below.
37293730
```
3731+
3732+
<a id="af-148"></a>
3733+
3734+
### 148. route — Persistent pool, not hosted: a hosted backlog queued route past the cron period, and af-005's supersede then starved every scan round.
3735+
3736+
In `route` and `review-scan`.
3737+
3738+
```text
3739+
route and review-scan run on the persistent pool, not the
3740+
hosted one. They are short trusted base-repo jobs, but they
3741+
gate the WHOLE fan-out: while they sit queued, no
3742+
review-address leg starts. On 2026-08-25 a hosted-runner
3743+
backlog queued route for over 20 minutes — longer than the
3744+
cron period — and af-005's newer-tick-supersedes-older rule
3745+
then cancelled every still-queued round: nine consecutive
3746+
schedule runs died without scanning while the ecs-qwen pool
3747+
stood mostly idle. The supersede rule stays — it is right
3748+
once route gets a runner in seconds; the fix is taking the
3749+
hosted queue out of the critical path. review-scan moves
3750+
with it because it shares the gate, and its own hosted waits
3751+
delayed every fan-out by the same backlog. Both keep the
3752+
fork-trust clause of the sibling lanes: pull_request and
3753+
pull_request_review resolve this file from the PR's own
3754+
merge commit, so only same-repo heads and write-access
3755+
authors may reach the persistent pool; everything else stays
3756+
hosted, and the kill-switch wins everywhere. Neither job
3757+
checks code out — route only decides phases, and review-scan
3758+
only calls the API — so the shared workspace needs no
3759+
restore or wipe step here. The pool still leaves two marks
3760+
on the lane, both closed in the same change. One: gh reads
3761+
its config from the shared, attacker-writable $HOME, so both
3762+
steps carry the heavy jobs' gh hardening preamble — a planted
3763+
~/.config/gh/config.yml could reroute their gh calls into a
3764+
local socket, taking CI_DEV_BOT_PAT with the scan and forged
3765+
collaborator-permission answers with route. Two: review-scan
3766+
fills a per-run WORKDIR with API dumps that no VM teardown
3767+
removes on the pool, so it gets a fixed autofix* per-run path
3768+
(the age sweep can reclaim it after a hard kill), an EXIT
3769+
trap, and an always() cleanup step mirroring issue-autofix.
3770+
```

0 commit comments

Comments
 (0)