mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
Lint & Static red onfa9e3ca963was the lint gate freshness check, not this PR: it named .github/workflows/ci.yml as changed on main by #16513 (708ba347d3) after this branch last incorporated it, and this branch does not touch that file. Merging main is the fix; no code change. The merge auto-resolved both overlapping files (the size manifest and the shared runner-routing gate) with this branch's entries intact. Re-verified on the merged tree: 503 passed | 1 skipped across seven suites, 134/134 shared node gates, size ratchet green at 46,266 bytes.
1764 lines
65 KiB
JavaScript
1764 lines
65 KiB
JavaScript
// Runner-routing regression guards for ci.yml, serve-ab.yml, e2e.yml, the
|
|
// qwen-autofix.yml scan lane, the trusted-PR lanes of #13245, and the
|
|
// control-plane jobs that run no PR code.
|
|
//
|
|
// classify_pr's own runner is unconditional ECS (kill-switch excepted):
|
|
// the gate executes only trusted base-SHA code with a read-only token, and
|
|
// keeping it on hosted for forks queued the whole fork CI chain behind a
|
|
// scarce pool (2026-10-31 decision, same accepted risk class as lint).
|
|
// The association routing for DOWNSTREAM lanes lives in the `pick_runner`
|
|
// shell step, which publishes `ubuntu_runner` (association-gated) and
|
|
// `lint_runner` (untrusted forks default to hosted). The matrix below
|
|
// pins the gate's unconditional routing AND that ubuntu_runner keeps the
|
|
// association gate for the test/integration lanes.
|
|
//
|
|
// test_windows carries a deliberately different policy. A pull_request run
|
|
// executes the workflow YAML from the PR's own merge commit, so any trust
|
|
// clause a PR can read it can also rewrite. The matrix evaluates the real
|
|
// expression text and asserts the only enforceable shape: every pull request
|
|
// stays hosted, and only the merge queue, schedule and dispatch reach the
|
|
// persistent pool.
|
|
import assert from 'node:assert/strict';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { dirname, join } from 'node:path';
|
|
import { describe, it } from 'node:test';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { parse } from 'yaml';
|
|
|
|
const workflowsDir = join(
|
|
dirname(fileURLToPath(import.meta.url)),
|
|
'..',
|
|
'workflows',
|
|
);
|
|
const ciDoc = parse(readFileSync(join(workflowsDir, 'ci.yml'), 'utf8'));
|
|
const serveAbDoc = parse(
|
|
readFileSync(join(workflowsDir, 'serve-ab.yml'), 'utf8'),
|
|
);
|
|
|
|
const TRUSTED = ['OWNER', 'MEMBER', 'COLLABORATOR'];
|
|
const ECS = '["self-hosted", "linux", "x64", "ecs-qwen"]';
|
|
const HOSTED = '["ubuntu-latest"]';
|
|
const WIN_ECS = ['self-hosted', 'Windows', 'X64', 'ecs-win'];
|
|
const WIN_HOSTED = ['windows-2022'];
|
|
|
|
const classifyRunsOn = String(ciDoc.jobs.classify_pr['runs-on']);
|
|
const windowsRunsOn = String(ciDoc.jobs.test_windows['runs-on']);
|
|
const pickRunner = ciDoc.jobs.classify_pr.steps.find(
|
|
(s) => s.id === 'pick_runner',
|
|
);
|
|
|
|
// GitHub expression semantics for the runs-on expressions, restricted to
|
|
// the routing-relevant inputs: contains(list, '') is false, a missing
|
|
// pull_request (merge_group / dispatch) yields '' for both head.repo and
|
|
// author_association.
|
|
|
|
// Evaluates a real `runs-on` expression text with the routing inputs
|
|
// substituted, leaving only the &&/||/parenthesis skeleton — which matches
|
|
// GitHub's operator semantics closely enough for this fixed shape: both
|
|
// return the winning operand, and the winning operand is a fromJSON runner
|
|
// label, unwrapped here to the array it names. Any term the substitutions
|
|
// do not recognise fails loud, so an edited expression is re-read here
|
|
// instead of silently outgrowing the matrix.
|
|
function evalRunsOn(expression, { ecsDisabled, eventName, sameRepo, assoc }) {
|
|
const substitutions = [
|
|
[/vars\.MAINTAINER_ECS_RUNNER_DISABLED != 'true'/, String(!ecsDisabled)],
|
|
[
|
|
/github\.event_name == 'merge_group'/,
|
|
String(eventName === 'merge_group'),
|
|
],
|
|
[/github\.event_name == 'push'/, String(eventName === 'push')],
|
|
// Longest term first as a convention; both patterns are quote-anchored
|
|
// (the closing quote is part of each regex), so neither can match
|
|
// inside the other and the substitution order is behaviorally inert.
|
|
[
|
|
/github\.event_name != 'pull_request_review'/,
|
|
String(eventName !== 'pull_request_review'),
|
|
],
|
|
[
|
|
/github\.event_name != 'pull_request_target'/,
|
|
String(eventName !== 'pull_request_target'),
|
|
],
|
|
[
|
|
/github\.event_name != 'pull_request'/,
|
|
String(eventName !== 'pull_request'),
|
|
],
|
|
[
|
|
/github\.event_name == 'pull_request'/,
|
|
String(eventName === 'pull_request'),
|
|
],
|
|
[/github\.repository == 'QwenLM\/qwen-code'/, 'true'],
|
|
[
|
|
/github\.event\.pull_request\.head\.repo\.full_name == github\.repository/,
|
|
String(sameRepo),
|
|
],
|
|
[
|
|
/contains\(fromJSON\('\["OWNER","MEMBER","COLLABORATOR"\]'\), github\.event\.pull_request\.author_association\)/,
|
|
String(TRUSTED.includes(assoc)),
|
|
],
|
|
[/github\.repository == 'QwenLM\/qwen-code'/, 'true'],
|
|
];
|
|
let expr = expression.replace(/^\$\{\{\s*/, '').replace(/\s*\}\}$/, '');
|
|
for (const [term, value] of substitutions) {
|
|
expr = expr.replace(term, value);
|
|
}
|
|
expr = expr.replace(/fromJSON\('(\[[^\]]*\])'\)/g, '$1');
|
|
assert.doesNotMatch(
|
|
expr,
|
|
/github\.|vars\.|contains\(|fromJSON\(/,
|
|
`routing expression carries a term the matrix does not model: ${expr}`,
|
|
);
|
|
const selected = new Function(`return (${expr});`)();
|
|
assert.ok(Array.isArray(selected), `no runner label selected: ${expr}`);
|
|
return selected;
|
|
}
|
|
|
|
// Executes the real pick_runner shell with the same inputs and returns the
|
|
// selected runners exactly as CI would publish them.
|
|
function runPickRunnerOutputs({
|
|
ecsDisabled,
|
|
sameRepo,
|
|
assoc,
|
|
eventName,
|
|
dispatch,
|
|
fleet,
|
|
ephemeralEnabled,
|
|
ephemeralArch,
|
|
}) {
|
|
const tmp = mkdtempSync(join(tmpdir(), 'pick-runner-'));
|
|
const outputFile = join(tmp, 'github_output');
|
|
const result = spawnSync('bash', ['-c', pickRunner.run], {
|
|
env: {
|
|
SAME_REPO: sameRepo ? 'true' : 'false',
|
|
AUTHOR_ASSOCIATION: assoc,
|
|
ECS_DISABLED: ecsDisabled ? 'true' : '',
|
|
EVENT_NAME: eventName,
|
|
LINUX_RUNNER_FLEET: fleet ?? '',
|
|
EPHEMERAL_ENABLED: ephemeralEnabled ?? '',
|
|
EPHEMERAL_ARCH: ephemeralArch ?? '',
|
|
DISPATCH_LINUX_RUNNER: dispatch ?? '',
|
|
GITHUB_OUTPUT: outputFile,
|
|
},
|
|
encoding: 'utf8',
|
|
});
|
|
const outputs = existsSync(outputFile)
|
|
? readFileSync(outputFile, 'utf8')
|
|
: '';
|
|
rmSync(tmp, { recursive: true, force: true });
|
|
assert.equal(result.status, 0, `pick_runner failed: ${result.stderr}`);
|
|
const select = (prefix) => {
|
|
const line = result.stdout.split('\n').find((l) => l.startsWith(prefix));
|
|
assert.ok(line, `no selection in pick_runner output: ${result.stdout}`);
|
|
return line.slice(prefix.length);
|
|
};
|
|
const ubuntuRunner = select('Selected Linux runner: ');
|
|
const lintRunner = select('Selected lint runner: ');
|
|
// The stdout echo and the published output must agree — a rename of the
|
|
// GITHUB_OUTPUT key would otherwise pass the echo parse and break every
|
|
// downstream runs-on at runtime.
|
|
assert.ok(
|
|
outputs.split('\n').includes(`ubuntu_runner=${ubuntuRunner}`),
|
|
`ubuntu_runner not published: ${outputs}`,
|
|
);
|
|
assert.ok(
|
|
outputs.split('\n').includes(`lint_runner=${lintRunner}`),
|
|
`lint_runner not published: ${outputs}`,
|
|
);
|
|
const prTest = outputs
|
|
.split('\n')
|
|
.find((line) => line.startsWith('pr_test_runner='));
|
|
assert.ok(prTest, 'pr_test_runner not published');
|
|
return {
|
|
ubuntuRunner,
|
|
lintRunner,
|
|
prTestRunner: prTest.slice('pr_test_runner='.length),
|
|
};
|
|
}
|
|
|
|
function runPickRunner(inputs) {
|
|
return runPickRunnerOutputs(inputs).ubuntuRunner;
|
|
}
|
|
|
|
const ASSOCIATIONS = [
|
|
...TRUSTED,
|
|
'CONTRIBUTOR',
|
|
'FIRST_TIME_CONTRIBUTOR',
|
|
'FIRST_TIMER',
|
|
'NONE',
|
|
'',
|
|
];
|
|
|
|
describe('ci.yml classify_pr runner routing', () => {
|
|
it('the gate routes every association to ECS; the kill-switch wins', () => {
|
|
// classify executes trusted base-SHA code only, so its own runner needs
|
|
// no association gate — a hosted gate queued the entire fork CI chain.
|
|
// The downstream ubuntu_runner keeps the gate (next test).
|
|
for (const sameRepo of [true, false]) {
|
|
for (const assoc of ASSOCIATIONS) {
|
|
assert.deepEqual(
|
|
evalRunsOn(classifyRunsOn, {
|
|
ecsDisabled: false,
|
|
eventName: 'pull_request',
|
|
sameRepo,
|
|
assoc,
|
|
}),
|
|
JSON.parse(ECS),
|
|
`gate sameRepo=${sameRepo} assoc='${assoc}' must reach ECS`,
|
|
);
|
|
assert.deepEqual(
|
|
evalRunsOn(classifyRunsOn, {
|
|
ecsDisabled: true,
|
|
eventName: 'pull_request',
|
|
sameRepo,
|
|
assoc,
|
|
}),
|
|
JSON.parse(HOSTED),
|
|
`kill-switch must revert the gate (assoc='${assoc}')`,
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
it('only write-access associations leave the hosted pool', () => {
|
|
for (const assoc of ['CONTRIBUTOR', 'FIRST_TIME_CONTRIBUTOR', 'NONE', '']) {
|
|
assert.equal(
|
|
runPickRunner({
|
|
ecsDisabled: false,
|
|
sameRepo: false,
|
|
assoc,
|
|
eventName: 'pull_request',
|
|
}),
|
|
HOSTED,
|
|
`assoc '${assoc}' must stay hosted`,
|
|
);
|
|
}
|
|
for (const assoc of TRUSTED) {
|
|
assert.equal(
|
|
runPickRunner({
|
|
ecsDisabled: false,
|
|
sameRepo: false,
|
|
assoc,
|
|
eventName: 'pull_request',
|
|
}),
|
|
ECS,
|
|
`assoc '${assoc}' must route to ECS`,
|
|
);
|
|
}
|
|
});
|
|
|
|
it('merge queue and explicit dispatch still reach ECS; the kill-switch wins', () => {
|
|
assert.equal(
|
|
runPickRunner({
|
|
ecsDisabled: false,
|
|
sameRepo: false,
|
|
assoc: '',
|
|
eventName: 'merge_group',
|
|
}),
|
|
ECS,
|
|
);
|
|
assert.equal(
|
|
runPickRunner({
|
|
ecsDisabled: false,
|
|
sameRepo: false,
|
|
assoc: '',
|
|
eventName: 'workflow_dispatch',
|
|
dispatch: 'self-hosted',
|
|
}),
|
|
ECS,
|
|
);
|
|
assert.equal(
|
|
runPickRunner({
|
|
ecsDisabled: true,
|
|
sameRepo: true,
|
|
assoc: 'OWNER',
|
|
eventName: 'pull_request',
|
|
}),
|
|
HOSTED,
|
|
'kill-switch must revert even trusted runs to hosted',
|
|
);
|
|
});
|
|
|
|
it('routes the push classifier and downstream jobs to the same pool', () => {
|
|
for (const ecsDisabled of [false, true]) {
|
|
const inputs = {
|
|
ecsDisabled,
|
|
sameRepo: false,
|
|
assoc: '',
|
|
eventName: 'push',
|
|
};
|
|
const expected = ecsDisabled ? HOSTED : ECS;
|
|
assert.deepEqual(
|
|
evalRunsOn(classifyRunsOn, inputs),
|
|
JSON.parse(expected),
|
|
);
|
|
assert.equal(runPickRunner(inputs), expected);
|
|
}
|
|
});
|
|
|
|
it('the gate expression is unconditional except the kill-switch', () => {
|
|
// Structural pins: no association/head-repo clause may creep back into
|
|
// the gate (it would re-queue fork CI on hosted), and the kill-switch
|
|
// must stay the single escape hatch.
|
|
assert.doesNotMatch(classifyRunsOn, /author_association/);
|
|
assert.doesNotMatch(classifyRunsOn, /head\.repo\.full_name/);
|
|
assert.match(
|
|
classifyRunsOn,
|
|
/vars\.MAINTAINER_ECS_RUNNER_DISABLED != 'true'/,
|
|
);
|
|
assert.match(classifyRunsOn, /ecs-qwen/);
|
|
});
|
|
});
|
|
|
|
describe('ci.yml lint_runner routing', () => {
|
|
const ARM = '["self-hosted", "linux", "arm64"]';
|
|
const lintRunner = (inputs) => runPickRunnerOutputs(inputs).lintRunner;
|
|
|
|
it('keeps untrusted fork lint hosted when the disposable switch is unset', () => {
|
|
for (const sameRepo of [true, false]) {
|
|
for (const assoc of ASSOCIATIONS) {
|
|
assert.equal(
|
|
lintRunner({
|
|
ecsDisabled: false,
|
|
sameRepo,
|
|
assoc,
|
|
eventName: 'pull_request',
|
|
}),
|
|
sameRepo || TRUSTED.includes(assoc) ? ECS : HOSTED,
|
|
`lint sameRepo=${sameRepo} assoc='${assoc}'`,
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
it('keeps the kill-switch and hosted-fleet overrides', () => {
|
|
for (const assoc of ASSOCIATIONS) {
|
|
assert.equal(
|
|
lintRunner({
|
|
ecsDisabled: true,
|
|
sameRepo: true,
|
|
assoc,
|
|
eventName: 'pull_request',
|
|
}),
|
|
HOSTED,
|
|
'kill-switch must revert even lint to hosted',
|
|
);
|
|
assert.equal(
|
|
lintRunner({
|
|
ecsDisabled: false,
|
|
sameRepo: false,
|
|
assoc,
|
|
eventName: 'pull_request',
|
|
fleet: 'hosted',
|
|
}),
|
|
HOSTED,
|
|
'LINUX_RUNNER_FLEET=hosted must keep lint hosted',
|
|
);
|
|
}
|
|
});
|
|
|
|
it('follows the arm64 fleet selector', () => {
|
|
assert.equal(
|
|
lintRunner({
|
|
ecsDisabled: false,
|
|
sameRepo: false,
|
|
assoc: 'MEMBER',
|
|
eventName: 'pull_request',
|
|
fleet: 'arm64',
|
|
}),
|
|
ARM,
|
|
);
|
|
});
|
|
|
|
it('inherits ubuntu_runner on non-PR events and dispatch inputs', () => {
|
|
for (const eventName of ['merge_group', 'push']) {
|
|
assert.equal(
|
|
lintRunner({
|
|
ecsDisabled: false,
|
|
sameRepo: false,
|
|
assoc: '',
|
|
eventName,
|
|
}),
|
|
ECS,
|
|
);
|
|
}
|
|
assert.equal(
|
|
lintRunner({
|
|
ecsDisabled: false,
|
|
sameRepo: false,
|
|
assoc: '',
|
|
eventName: 'workflow_dispatch',
|
|
dispatch: 'self-hosted',
|
|
}),
|
|
ECS,
|
|
);
|
|
assert.equal(
|
|
lintRunner({
|
|
ecsDisabled: false,
|
|
sameRepo: false,
|
|
assoc: '',
|
|
eventName: 'workflow_dispatch',
|
|
dispatch: 'hosted',
|
|
}),
|
|
HOSTED,
|
|
);
|
|
assert.equal(
|
|
lintRunner({
|
|
ecsDisabled: true,
|
|
sameRepo: false,
|
|
assoc: '',
|
|
eventName: 'workflow_dispatch',
|
|
dispatch: 'self-hosted',
|
|
}),
|
|
HOSTED,
|
|
);
|
|
});
|
|
|
|
it('classify_pr publishes lint_runner from pick_runner', () => {
|
|
assert.equal(
|
|
ciDoc.jobs.classify_pr.outputs.lint_runner,
|
|
'${{ steps.pick_runner.outputs.lint_runner }}',
|
|
);
|
|
});
|
|
|
|
it('lint_and_static consumes lint_runner for runs-on and timeout', () => {
|
|
assert.equal(
|
|
String(ciDoc.jobs.lint_and_static['runs-on']),
|
|
'${{ fromJSON(needs.classify_pr.outputs.lint_runner || \'["ubuntu-latest"]\') }}',
|
|
);
|
|
assert.match(
|
|
String(ciDoc.jobs.lint_and_static['timeout-minutes']),
|
|
/needs\.classify_pr\.outputs\.lint_runner/,
|
|
);
|
|
});
|
|
|
|
it('keeps both outputs on one fleet ladder', () => {
|
|
// The lint arm assigns from the same resolved fleet labels as
|
|
// ubuntu_runner; a fleet value neither models must not split them when
|
|
// the trust clause is satisfied (a second literal ladder drifts silently
|
|
// because the timeout reads lint_runner and still reports a pool ceiling).
|
|
for (const fleet of ['', 'x64', 'arm64', 'future-fleet']) {
|
|
const out = runPickRunnerOutputs({
|
|
ecsDisabled: false,
|
|
sameRepo: true,
|
|
assoc: 'NONE',
|
|
eventName: 'pull_request',
|
|
fleet,
|
|
});
|
|
assert.equal(out.lintRunner, out.ubuntuRunner, `fleet='${fleet}'`);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('ci.yml disposable runner routing', () => {
|
|
it('keeps all selected fork jobs hosted unless the switch is exactly true', () => {
|
|
for (const ephemeralEnabled of [undefined, '', 'false', 'invalid']) {
|
|
const out = runPickRunnerOutputs({
|
|
eventName: 'pull_request',
|
|
sameRepo: false,
|
|
assoc: 'NONE',
|
|
ephemeralEnabled,
|
|
});
|
|
assert.equal(out.prTestRunner, HOSTED);
|
|
assert.equal(out.lintRunner, HOSTED);
|
|
}
|
|
});
|
|
it('selects ARM64 only when explicitly configured', () => {
|
|
const inputs = {
|
|
eventName: 'pull_request',
|
|
sameRepo: false,
|
|
assoc: 'NONE',
|
|
ephemeralEnabled: 'true',
|
|
};
|
|
const arm = '["self-hosted", "linux", "arm64", "ecs-qwen-ephemeral"]';
|
|
assert.equal(
|
|
runPickRunnerOutputs({ ...inputs, ephemeralArch: 'arm64' }).prTestRunner,
|
|
arm,
|
|
);
|
|
assert.equal(
|
|
runPickRunnerOutputs({ ...inputs, ephemeralArch: 'arm64' }).lintRunner,
|
|
arm,
|
|
);
|
|
assert.equal(
|
|
runPickRunnerOutputs({ ...inputs, ephemeralArch: 'invalid' })
|
|
.prTestRunner,
|
|
'["self-hosted", "linux", "x64", "ecs-qwen-ephemeral"]',
|
|
);
|
|
});
|
|
it('routes only untrusted fork jobs to the disposable pool when enabled', () => {
|
|
const ephemeral = '["self-hosted", "linux", "x64", "ecs-qwen-ephemeral"]';
|
|
for (const eventName of [
|
|
'pull_request',
|
|
'push',
|
|
'merge_group',
|
|
'workflow_dispatch',
|
|
]) {
|
|
for (const sameRepo of [true, false]) {
|
|
for (const assoc of ASSOCIATIONS) {
|
|
for (const ecsDisabled of [true, false]) {
|
|
for (const fleet of ['', 'hosted', 'arm64']) {
|
|
const inputs = { eventName, sameRepo, assoc, ecsDisabled, fleet };
|
|
const baseline = runPickRunnerOutputs(inputs);
|
|
const enabled = runPickRunnerOutputs({
|
|
...inputs,
|
|
ephemeralEnabled: 'true',
|
|
});
|
|
const selected =
|
|
eventName === 'pull_request' &&
|
|
!sameRepo &&
|
|
!TRUSTED.includes(assoc) &&
|
|
!ecsDisabled &&
|
|
fleet !== 'hosted';
|
|
assert.equal(
|
|
enabled.prTestRunner,
|
|
selected ? ephemeral : baseline.ubuntuRunner,
|
|
JSON.stringify(inputs),
|
|
);
|
|
assert.equal(
|
|
enabled.lintRunner,
|
|
selected ? ephemeral : baseline.lintRunner,
|
|
JSON.stringify(inputs),
|
|
);
|
|
assert.equal(enabled.ubuntuRunner, baseline.ubuntuRunner);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
it('routes only the intended jobs through the new output', () => {
|
|
assert.equal(
|
|
ciDoc.jobs.classify_pr.outputs.pr_test_runner,
|
|
'${{ steps.pick_runner.outputs.pr_test_runner }}',
|
|
);
|
|
for (const name of ['test_build', 'test'])
|
|
assert.match(ciDoc.jobs[name]['runs-on'], /pr_test_runner/);
|
|
for (const name of ['lint_and_static', 'integration_no_ak'])
|
|
assert.match(ciDoc.jobs[name]['runs-on'], /lint_runner/);
|
|
for (const [name, job] of Object.entries(ciDoc.jobs)) {
|
|
if (!['classify_pr', 'test_build', 'test'].includes(name))
|
|
assert.doesNotMatch(String(job['runs-on']), /pr_test_runner/, name);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('ci.yml lint HOME isolation', () => {
|
|
// This guard lives in the routing battery (HELPER_TESTS) because it gates
|
|
// every full-profile PR; the scripts/tests vitest suites only run after the
|
|
// merge queue. lint_runner admits untrusted forks to a pool where
|
|
// RUNNER_TEMP survives across jobs, so the isolated HOME must be run-unique
|
|
// and cleared before use — a fixed path hands a fork's ~/.npmrc to the next
|
|
// job on the registration. The ubuntu_runner lanes' association clause is
|
|
// throughput policy, not an enforceable boundary (a pull_request run
|
|
// executes the PR's own YAML); their pre-existing HOME properties are
|
|
// separately owned.
|
|
const ungated = Object.entries(ciDoc.jobs).filter(([, job]) =>
|
|
/outputs\.lint_runner/.test(String(job['runs-on'] ?? '')),
|
|
);
|
|
|
|
// The one run-unique path per lane. create, the gate's QWEN_HOME and the
|
|
// cleanup must all name it: a `home=` that drifts at one site leaves the
|
|
// others pointing at a directory this run never cleared.
|
|
const HOME_TEMPLATE = {
|
|
lint_and_static: 'qwen-lint-home-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}',
|
|
integration_no_ak: 'qwen-no-ak-home-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}',
|
|
};
|
|
|
|
// The `$GITHUB_ENV` writers, by value. It is last-write-wins, so an added
|
|
// HOME/USERPROFILE writer (or a dropped `HOME=%s`) must break equality;
|
|
// COREPACK_HOME stays inside the run-unique home (corepack does not
|
|
// re-verify entries on cache hit — a shared home is a poisoning channel)
|
|
// and XDG_CACHE_HOME rides the SHA-256-verified linter-archive cache.
|
|
const ENV_WRITES = [
|
|
'printf \'HOME=%s\\nUSERPROFILE=%s\\n\' "$home" "$home" >> "${GITHUB_ENV}"',
|
|
'printf \'XDG_CACHE_HOME=%s\\n\' "${RUNNER_TOOL_CACHE}/qwen-ci-cache" >> "${GITHUB_ENV}"',
|
|
'printf \'COREPACK_HOME=%s\\n\' "${home}/.cache/node/corepack" >> "${GITHUB_ENV}"',
|
|
];
|
|
|
|
it('scopes the ungated routing to the recorded lanes', () => {
|
|
// The decision record scopes the boundary change to Lint & Static and
|
|
// (2026-11-02, same trust class — PR code on the pool, zero secrets by
|
|
// construction) the no-AK integration lane; a superset must force a
|
|
// deliberate record update, not pass silently.
|
|
assert.deepEqual(
|
|
ungated.map(([id]) => id),
|
|
['lint_and_static', 'integration_no_ak'],
|
|
);
|
|
});
|
|
|
|
it('integration_no_ak isolates from its first step, zero-secrets at the gate', () => {
|
|
// no_ak's fork code starts at the checkout action and the install
|
|
// (lifecycle scripts AND scripts/prepare.js — build+bundle), not at the
|
|
// gate step, so the whole job needs lint's first-step isolation:
|
|
// GITHUB_ENV-published run-unique HOME, COREPACK_HOME inside it (corepack
|
|
// does not re-verify entries on cache hit — a shared home is a
|
|
// cross-job poisoning channel), XDG_CACHE_HOME on the shared verified
|
|
// tool cache. The gate keeps the model-key scrub.
|
|
// The routing decision's premise is that this job carries no secrets
|
|
// because its only grant is the read-only checkout. Nothing else pins it:
|
|
// adding any other scope would leave every assertion below green while
|
|
// the accepted-risk argument went stale.
|
|
assert.deepEqual(
|
|
ciDoc.jobs.integration_no_ak.permissions,
|
|
{ contents: 'read' },
|
|
'no_ak must keep only the read-only checkout grant',
|
|
);
|
|
|
|
const steps = ciDoc.jobs.integration_no_ak.steps ?? [];
|
|
assert.equal(
|
|
steps[0]?.name,
|
|
'Create isolated HOME',
|
|
'no_ak must isolate HOME from its first step',
|
|
);
|
|
// The create/cleanup step text is pinned by VALUE in the two shared tests
|
|
// below, which iterate every ungated lane; looped substring checks used to
|
|
// survive a `-v2` path drift, a dropped `HOME=%s` and an added HOME writer.
|
|
// Job env must stay empty of HOME (the runner context is illegal there).
|
|
assert.equal(ciDoc.jobs.integration_no_ak.env?.HOME, undefined);
|
|
|
|
const gate = steps.find(
|
|
(s) => s.name === 'Run required no-AK integration gate',
|
|
);
|
|
assert.ok(gate, 'integration gate step missing');
|
|
// By value, not by prefix: a drifted create path or a `-v2` suffix would
|
|
// satisfy `startsWith` while QWEN_HOME pointed at a directory this run
|
|
// never cleared.
|
|
assert.equal(
|
|
gate.env?.QWEN_HOME,
|
|
'${{ runner.temp }}/qwen-no-ak-home-${{ github.run_id }}-${{ github.run_attempt }}/.qwen',
|
|
'QWEN_HOME must be the run-unique home the create step built',
|
|
);
|
|
// Zero-secrets contract, enforced by VALUE not by name: every env key
|
|
// outside QWEN_HOME must be the empty string, and the step must
|
|
// reference no secrets.* anywhere. A name-based regex inherits the
|
|
// implementation's own premise about which names are credentials, so a
|
|
// secret under any other name (CI_BOT_PAT, *_DSN, *_BEARER) shipped
|
|
// green — falsifying the contract that justifies routing untrusted-fork
|
|
// PR code onto the shared pool.
|
|
const PATH_KEYS = new Set(['QWEN_HOME']);
|
|
for (const [key, value] of Object.entries(gate.env ?? {})) {
|
|
if (!PATH_KEYS.has(key)) {
|
|
assert.equal(
|
|
value,
|
|
'',
|
|
`${key} must be empty in the no-AK PR-code step`,
|
|
);
|
|
}
|
|
}
|
|
assert.ok(
|
|
!JSON.stringify(gate).includes('secrets.'),
|
|
'the no-AK PR-code step must not reference any secrets.* context',
|
|
);
|
|
// The value whitelist above only iterates PRESENT keys, so a deleted
|
|
// scrub key escapes it (verified by mutation: dropping
|
|
// ANTHROPIC_AUTH_TOKEN left both suites green). Pin a floor on the
|
|
// known-dangerous set — the SDK back-fills this one from env.
|
|
assert.equal(
|
|
gate.env?.ANTHROPIC_AUTH_TOKEN,
|
|
'',
|
|
'ANTHROPIC_AUTH_TOKEN must be present and empty (SDK auth-resolver ' +
|
|
'back-fill; deleting the scrub key escapes the value whitelist)',
|
|
);
|
|
});
|
|
|
|
it('publishes a run-unique HOME from the first step, not job env', () => {
|
|
// Both ungated lanes now share lint's shape: fork code runs from early
|
|
// steps, so the first step publishes HOME/USERPROFILE via GITHUB_ENV for
|
|
// every later step. no_ak's specifics (gate key scrub, reclamation) are
|
|
// covered by its own test above.
|
|
for (const [id, job] of ungated) {
|
|
// jobs.<job_id>.env cannot read the runner context — GitHub rejects the
|
|
// whole workflow file (actionlint 1.7.12 witnesses it).
|
|
assert.equal(job.env?.HOME, undefined, `${id} job-level HOME`);
|
|
assert.equal(
|
|
job.env?.USERPROFILE,
|
|
undefined,
|
|
`${id} job-level USERPROFILE`,
|
|
);
|
|
const steps = job.steps ?? [];
|
|
assert.equal(steps[0]?.name, 'Create isolated HOME', id);
|
|
const run = String(steps[0]?.run ?? '');
|
|
const lines = run.split('\n').map((line) => line.trim());
|
|
// The declared `home=` by value: a `-v2` suffix here leaves the gate's
|
|
// QWEN_HOME and the cleanup aimed at a directory this run never built.
|
|
assert.ok(
|
|
lines.includes(`home="\${RUNNER_TEMP}/${HOME_TEMPLATE[id]}"`),
|
|
`${id} must build its run-unique HOME from the lane template`,
|
|
);
|
|
assert.ok(
|
|
run.includes('rm -rf') && run.indexOf('rm -rf') < run.indexOf('mkdir'),
|
|
`${id} must clear the path before creating it`,
|
|
);
|
|
assert.deepEqual(
|
|
lines.filter((line) => line.includes('${GITHUB_ENV}')),
|
|
ENV_WRITES,
|
|
`${id} must publish exactly HOME/USERPROFILE, XDG_CACHE_HOME and COREPACK_HOME via GITHUB_ENV`,
|
|
);
|
|
// GITHUB_ENV is last-write-wins, so the guard cannot stop at step 0: a
|
|
// later step that republishes an isolated key silently reverts the home
|
|
// for the steps that run PR code while step 0 still matches. The npm
|
|
// cache step is a legitimate later writer, so only the isolated keys
|
|
// are banned — not every later `$GITHUB_ENV` append.
|
|
// `[^\w]`, not `\s`: the realistic form is `printf 'HOME=%s\n'` inside
|
|
// quotes, which a whitespace-only prefix let through.
|
|
const ISOLATED_KEY =
|
|
/(^|[^\w])(HOME|USERPROFILE|COREPACK_HOME|XDG_CACHE_HOME)=/;
|
|
for (const [index, step] of steps.entries()) {
|
|
if (index === 0) continue;
|
|
for (const line of String(step.run ?? '')
|
|
.split('\n')
|
|
.map((l) => l.trim())) {
|
|
assert.ok(
|
|
!(line.includes('${GITHUB_ENV}') && ISOLATED_KEY.test(line)),
|
|
`${id} step ${index} (${step.name}) must not republish an isolated HOME key via GITHUB_ENV`,
|
|
);
|
|
}
|
|
for (const key of Object.keys(step.env ?? {})) {
|
|
assert.ok(
|
|
!/^(HOME|USERPROFILE|COREPACK_HOME|XDG_CACHE_HOME)$/.test(key),
|
|
`${id} step ${index} (${step.name}) must not override ${key} for a single step`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
it('reclaims the isolated HOME before the failure collector', () => {
|
|
// Both ungated lanes reclaim: no_ak's cleanup is its last step (it has
|
|
// no collector); lint's sits before the failure collector, which must
|
|
// stay last.
|
|
for (const [id, job] of ungated) {
|
|
const steps = job.steps ?? [];
|
|
const names = steps.map((s) => s.name);
|
|
const clean = steps.find((s) => s.name === 'Clean isolated HOME');
|
|
assert.ok(clean, `${id} cleanup step missing`);
|
|
assert.equal(clean.if, '${{ always() }}', id);
|
|
const collectorIndex = names.indexOf('Upload disk-pressure samples');
|
|
if (collectorIndex !== -1) {
|
|
assert.ok(
|
|
names.indexOf('Clean isolated HOME') < collectorIndex,
|
|
`${id} collector must stay last`,
|
|
);
|
|
} else {
|
|
// No collector (no_ak): the cleanup itself is the last step.
|
|
assert.equal(
|
|
names[names.length - 1],
|
|
'Clean isolated HOME',
|
|
`${id} cleanup must be the final step`,
|
|
);
|
|
}
|
|
// The realized removal by value, never $HOME: any `qwen-*` path passes a
|
|
// substring check, so a retargeted `rm -rf` (or an `echo` stand-in)
|
|
// would leave the run-unique home to accumulate on this persistent pool.
|
|
assert.equal(
|
|
String(clean.run),
|
|
`rm -rf -- "\${RUNNER_TEMP}/${HOME_TEMPLATE[id]}"`,
|
|
`${id} must reclaim the exact run-unique HOME the create step built`,
|
|
);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('ci.yml web_shell_e2e_smoke runner routing', () => {
|
|
const expression = String(ciDoc.jobs.web_shell_e2e_smoke['runs-on'])
|
|
.replace(/^\$\{\{\s*/, '')
|
|
.replace(/\s*\}\}$/, '');
|
|
const selectSmokeRunner = new Function(
|
|
'needs',
|
|
'fromJSON',
|
|
'contains',
|
|
`return (${expression});`,
|
|
);
|
|
|
|
it('uses ARM64 for self-hosted runs while preserving every hosted fallback', () => {
|
|
for (const ecsDisabled of [false, true]) {
|
|
for (const fleet of ['', 'x64', 'arm64', 'hosted']) {
|
|
for (const sameRepo of [false, true]) {
|
|
for (const assoc of ASSOCIATIONS) {
|
|
for (const dispatch of [
|
|
undefined,
|
|
'self-hosted',
|
|
'self-hosted-arm64',
|
|
'hosted',
|
|
]) {
|
|
const runner = runPickRunner({
|
|
ecsDisabled,
|
|
fleet,
|
|
sameRepo,
|
|
assoc,
|
|
dispatch,
|
|
eventName: dispatch ? 'workflow_dispatch' : 'pull_request',
|
|
});
|
|
assert.deepEqual(
|
|
selectSmokeRunner(
|
|
{ classify_pr: { outputs: { ubuntu_runner: runner } } },
|
|
JSON.parse,
|
|
(labels, label) => labels.includes(label),
|
|
),
|
|
JSON.parse(runner).includes('self-hosted')
|
|
? ['self-hosted', 'linux', 'arm64']
|
|
: JSON.parse(HOSTED),
|
|
JSON.stringify({
|
|
ecsDisabled,
|
|
fleet,
|
|
sameRepo,
|
|
assoc,
|
|
dispatch,
|
|
runner,
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
it('stays hosted when the classifier output is missing', () => {
|
|
assert.deepEqual(
|
|
selectSmokeRunner(
|
|
{ classify_pr: { outputs: {} } },
|
|
JSON.parse,
|
|
(labels, label) => labels.includes(label),
|
|
),
|
|
JSON.parse(HOSTED),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('ci.yml test_windows runner routing', () => {
|
|
it('keeps every pull request hosted, whoever opens it', () => {
|
|
// A pull_request run executes the workflow YAML from the PR's own merge
|
|
// commit: any PR this lane admits could rewrite `runs-on` in the same
|
|
// diff (editing this file is what classifies it platform-sensitive), so
|
|
// no trust clause evaluated on that event is enforceable. The enforceable
|
|
// shape is unconditional — pull requests never reach the persistent pool.
|
|
for (const sameRepo of [true, false]) {
|
|
for (const assoc of ASSOCIATIONS) {
|
|
assert.deepEqual(
|
|
evalRunsOn(windowsRunsOn, {
|
|
ecsDisabled: false,
|
|
eventName: 'pull_request',
|
|
sameRepo,
|
|
assoc,
|
|
}),
|
|
WIN_HOSTED,
|
|
`pull_request sameRepo=${sameRepo} assoc='${assoc}' must stay hosted`,
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
it('keeps the pool for every non-pull-request trigger', () => {
|
|
// The denial form exists so the queue, the nightly and dispatch runs stay
|
|
// on the pool without a pull_request context to read; an && / || flip in
|
|
// the gate must not exile them to hosted runners.
|
|
for (const eventName of ['merge_group', 'schedule', 'workflow_dispatch']) {
|
|
assert.deepEqual(
|
|
evalRunsOn(windowsRunsOn, {
|
|
ecsDisabled: false,
|
|
eventName,
|
|
sameRepo: false,
|
|
assoc: '',
|
|
}),
|
|
WIN_ECS,
|
|
`${eventName} must keep the pool`,
|
|
);
|
|
}
|
|
});
|
|
|
|
it('the kill-switch wins on every event', () => {
|
|
for (const eventName of [
|
|
'pull_request',
|
|
'merge_group',
|
|
'schedule',
|
|
'workflow_dispatch',
|
|
]) {
|
|
assert.deepEqual(
|
|
evalRunsOn(windowsRunsOn, {
|
|
ecsDisabled: true,
|
|
eventName,
|
|
sameRepo: true,
|
|
assoc: 'OWNER',
|
|
}),
|
|
WIN_HOSTED,
|
|
`kill-switch must win on ${eventName}`,
|
|
);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('serve-ab.yml runner routing', () => {
|
|
const runsOn = String(serveAbDoc.jobs.ab['runs-on']);
|
|
|
|
it('admits same-repo and write-access fork PRs, guarded by the kill-switch', () => {
|
|
assert.match(runsOn, /head\.repo\.full_name == github\.repository/);
|
|
assert.match(
|
|
runsOn,
|
|
/contains\(fromJSON\('\["OWNER","MEMBER","COLLABORATOR"\]'\), github\.event\.pull_request\.author_association\)/,
|
|
);
|
|
assert.match(runsOn, /vars\.MAINTAINER_ECS_RUNNER_DISABLED != 'true'/);
|
|
assert.match(runsOn, /ecs-qwen/);
|
|
assert.match(runsOn, /ubuntu-latest/);
|
|
});
|
|
|
|
it('wipes the reused workspace except the shared root .git before checking out PR code', () => {
|
|
const steps = serveAbDoc.jobs.ab.steps;
|
|
const wipeIndex = steps.findIndex(
|
|
(s) =>
|
|
s.name ===
|
|
'Wipe stale workspace except the shared .git before checkout',
|
|
);
|
|
assert.ok(
|
|
wipeIndex !== -1,
|
|
'self-hosted reuse must not bleed one PR into the next',
|
|
);
|
|
const wipe = steps[wipeIndex];
|
|
assert.equal(wipe.if, "${{ runner.environment == 'self-hosted' }}");
|
|
// The script text alone does not decide whether the wipe runs: the
|
|
// shell wrapper, continue-on-error (step and job level), and env
|
|
// overrides (BASH_ENV, PATH, GITHUB_WORKSPACE) — at step, job, and
|
|
// workflow level — all control whether the pinned command executes
|
|
// and whether its failure fails the job.
|
|
const shell =
|
|
wipe.shell ??
|
|
serveAbDoc.jobs.ab.defaults?.run?.shell ??
|
|
serveAbDoc.defaults?.run?.shell;
|
|
assert.ok(
|
|
shell === undefined || shell === 'bash',
|
|
'the wipe must run under the default bash wrapper',
|
|
);
|
|
assert.ok(
|
|
!('continue-on-error' in wipe),
|
|
'a failed wipe must fail the job, not bleed into the next PR',
|
|
);
|
|
assert.ok(
|
|
!('continue-on-error' in serveAbDoc.jobs.ab),
|
|
'a job-level continue-on-error would mask a failed wipe',
|
|
);
|
|
for (const envMap of [wipe.env, serveAbDoc.jobs.ab.env, serveAbDoc.env]) {
|
|
assert.ok(
|
|
!envMap ||
|
|
(envMap.BASH_ENV === undefined &&
|
|
envMap.PATH === undefined &&
|
|
envMap.GITHUB_WORKSPACE === undefined),
|
|
'BASH_ENV, PATH, or GITHUB_WORKSPACE can shadow the pinned wipe command',
|
|
);
|
|
}
|
|
// The sudo-less wipe only works because ownership-restore ran first,
|
|
// and it must precede the checkouts or it deletes the freshly
|
|
// checked-out code instead of stale leftovers.
|
|
const stepIndex = (name) => steps.findIndex((s) => s.name === name);
|
|
const ownershipIndex = stepIndex('Restore workspace ownership');
|
|
assert.ok(
|
|
ownershipIndex !== -1,
|
|
'the wipe depends on the ownership-restore step existing',
|
|
);
|
|
assert.match(
|
|
steps[ownershipIndex].run,
|
|
/chown -R .* "\$GITHUB_WORKSPACE"/,
|
|
'ownership-restore must actually chown the workspace',
|
|
);
|
|
assert.ok(
|
|
ownershipIndex < wipeIndex,
|
|
'the wipe depends on ownership-restore running first',
|
|
);
|
|
const checkouts = steps.filter((s) =>
|
|
String(s.uses || '').startsWith('actions/checkout'),
|
|
);
|
|
for (const checkout of checkouts) {
|
|
assert.ok(
|
|
steps.indexOf(checkout) > wipeIndex,
|
|
'the wipe must run before every checkout it protects',
|
|
);
|
|
}
|
|
assert.ok(checkouts.length >= 2, 'expected at least two checkouts');
|
|
// Wiping the shared root .git forces the next job on this runner to
|
|
// re-fetch the full history from github.com — on the ECS pool's slow
|
|
// link that is the "hung runner" pathology. The checkout-heal path
|
|
// guard (#9220, #9265) ahead of it is pinned and exec-verified by
|
|
// scripts/tests/serve-ab-workflow.test.js; here pin that the guard is
|
|
// present and hands off to the kept-.git tail, line by line, so any
|
|
// change forces a deliberate test update.
|
|
assert.match(
|
|
wipe.run,
|
|
/refusing to wipe suspicious workspace path/,
|
|
'the wipe must keep the checkout-heal path guard',
|
|
);
|
|
const executed = wipe.run
|
|
.split('\n')
|
|
.map((l) => l.trim())
|
|
.filter((l) => l !== '' && !l.startsWith('#'));
|
|
assert.equal(executed[0], 'set -uo pipefail');
|
|
const tail = executed.slice(-5);
|
|
assert.equal(
|
|
tail[0],
|
|
'find "$WS" -mindepth 1 -maxdepth 1 ! \\( -name \'.git\' -type d \\) -exec rm -rf {} +',
|
|
'the wipe must keep only a REAL .git directory — a symlink or gitfile named .git can point outside the workspace — and only the guarded $WS may reach the rm',
|
|
);
|
|
assert.equal(
|
|
tail[1],
|
|
'rm -rf "$WS/.git/hooks" "$WS/.git/info/attributes"',
|
|
'the kept .git must lose its hooks and info/attributes exec vectors',
|
|
);
|
|
assert.equal(
|
|
tail[2],
|
|
'rm -f "$(git --git-dir="$WS/.git" rev-parse --git-path config.worktree 2>/dev/null || echo /nonexistent)" 2>/dev/null || true',
|
|
"extensions.worktreeConfig activates .git/config.worktree, a second local file that git config --local neither lists nor unsets — delete it like qwen-triage.yml's hardened config-sanitize",
|
|
);
|
|
assert.equal(
|
|
tail[3],
|
|
'git --git-dir="$WS/.git" config --local --unset-all extensions.worktreeConfig 2>/dev/null || true',
|
|
'drop the extension that re-activates the split config file',
|
|
);
|
|
assert.equal(
|
|
tail[4],
|
|
'{ git --git-dir="$WS/.git" config --local --name-only --list 2>/dev/null || true; } | { grep -ivE \'^(core\\.(repositoryformatversion|bare|filemode|symlinks|ignorecase|precomposeunicode|logallrefupdates|worktree|hidedotfiles|protecthfs|protectntfs)|remote\\.|branch\\.|extensions\\.|gc\\.|pack\\.|fetch\\.|index\\.|safe\\.|submodule\\.[^.]+\\.(url|active|branch))\' || true; } | while IFS= read -r key; do git --git-dir="$WS/.git" config --local --unset-all "$key" 2>/dev/null || true; done',
|
|
"the kept .git config must be scrubbed to the qwen-triage.yml config-sanitize allowlist, anchored to $WS/.git so a healed symlinked root never scrubs the link's target",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('e2e.yml e2e-test-linux runner routing', () => {
|
|
// Every trigger is a trusted context (push to in-repo branches, schedule,
|
|
// dispatch — no pull_request), so the lane routes to the persistent pool
|
|
// whenever routing is enabled, with the kill-switch as the only fallback.
|
|
const e2eDoc = parse(readFileSync(join(workflowsDir, 'e2e.yml'), 'utf8'));
|
|
// evalRunsOn unwraps the winning fromJSON label to the array it names.
|
|
const ECS_LABELS = ['self-hosted', 'linux', 'x64', 'ecs-qwen'];
|
|
const HOSTED_LABELS = ['ubuntu-latest'];
|
|
const job = e2eDoc.jobs['e2e-test-linux'];
|
|
const runsOn = String(job['runs-on']);
|
|
|
|
it('pins the repository guard clause the matrix substitutes away', () => {
|
|
// evalRunsOn substitutes the repo clause with a constant; deleting it
|
|
// from the workflow would leave every evaluation green while fork runs
|
|
// queue on a label no fork registers. Pin the real text.
|
|
assert.match(runsOn, /github\.repository == 'QwenLM\/qwen-code'/);
|
|
});
|
|
|
|
it('reaches the persistent pool on every trusted trigger', () => {
|
|
for (const eventName of ['push', 'schedule', 'workflow_dispatch']) {
|
|
assert.deepEqual(
|
|
evalRunsOn(runsOn, {
|
|
ecsDisabled: false,
|
|
eventName,
|
|
sameRepo: false,
|
|
assoc: '',
|
|
}),
|
|
ECS_LABELS,
|
|
`e2e-test-linux must run from the pool on ${eventName}`,
|
|
);
|
|
}
|
|
});
|
|
|
|
it('obeys the kill-switch', () => {
|
|
assert.deepEqual(
|
|
evalRunsOn(runsOn, {
|
|
ecsDisabled: true,
|
|
eventName: 'push',
|
|
sameRepo: true,
|
|
assoc: 'OWNER',
|
|
}),
|
|
HOSTED_LABELS,
|
|
'kill-switch must force the lane back to hosted',
|
|
);
|
|
});
|
|
|
|
it('keeps the workflow free of pull_request triggers', () => {
|
|
// The simple repo+kill-switch expression above is only safe because no
|
|
// lane of this workflow ever runs PR-authored workflow code. Adding a
|
|
// pull_request trigger must force a deliberate routing rework.
|
|
// `on:` is equally valid as a map, a sequence, or a scalar; normalize
|
|
// before enumerating, or a non-map form enumerates indices, not names.
|
|
const onMap = e2eDoc.on ?? e2eDoc[true] ?? {};
|
|
const triggers = Array.isArray(onMap)
|
|
? onMap.map(String)
|
|
: typeof onMap === 'string'
|
|
? [onMap]
|
|
: Object.keys(onMap);
|
|
assert.ok(triggers.length > 0, 'could not read the trigger map');
|
|
for (const trigger of triggers) {
|
|
assert.ok(
|
|
!trigger.startsWith('pull_request'),
|
|
`e2e.yml gained a ${trigger} trigger; the pool routing needs the fork-trust clause before this can land`,
|
|
);
|
|
}
|
|
});
|
|
|
|
it('carries the pool hygiene and capability steps in order', () => {
|
|
const names = job.steps.map((s) => s.name);
|
|
const preflight = names.indexOf('Check container runtime');
|
|
const heal = names.indexOf('Restore workspace ownership');
|
|
const checkout = job.steps.findIndex((s) =>
|
|
String(s.uses || '').startsWith('actions/checkout'),
|
|
);
|
|
const prune = names.indexOf('Prune dangling docker images');
|
|
// GitHub's default is 360 minutes; a wedged shard would hold a pool
|
|
// runner for all of it. Pin the ci.yml pool precedent.
|
|
assert.equal(job['timeout-minutes'], 60);
|
|
// Fail-fast daemon probe (#9556) before any expensive step, only on the
|
|
// docker leg.
|
|
assert.ok(preflight !== -1, 'the docker preflight must exist');
|
|
assert.match(job.steps[preflight].if, /sandbox:docker/);
|
|
assert.match(job.steps[preflight].run, /docker info/);
|
|
assert.match(job.steps[preflight].run, /exit 1/);
|
|
assert.ok(
|
|
preflight < names.indexOf('Install dependencies'),
|
|
'the docker preflight must fail fast, before any expensive step',
|
|
);
|
|
assert.ok(
|
|
!('continue-on-error' in job.steps[preflight]) &&
|
|
!('continue-on-error' in job),
|
|
'a failed docker probe must fail the job, not downgrade to a warning',
|
|
);
|
|
// Ownership heal before checkout, self-hosted only.
|
|
assert.ok(heal !== -1, 'the ownership heal must exist');
|
|
assert.equal(
|
|
job.steps[heal].if,
|
|
"${{ runner.environment == 'self-hosted' }}",
|
|
);
|
|
assert.match(job.steps[heal].run, /chown -R .* "\$GITHUB_WORKSPACE"/);
|
|
assert.match(job.steps[heal].run, /chmod -R u\+rwX/);
|
|
assert.ok(heal < checkout, 'the heal must precede the checkout');
|
|
// Cleanup at the end: always(), docker leg, pool only. Tagged cleanup is
|
|
// restricted to old workflow-owned images; the general cleanup remains
|
|
// dangling-only so it cannot remove images from unrelated jobs.
|
|
assert.ok(prune !== -1, 'the dangling prune must exist');
|
|
assert.match(job.steps[prune].if, /always\(\)/);
|
|
assert.match(job.steps[prune].if, /sandbox:docker/);
|
|
assert.match(job.steps[prune].if, /runner\.environment == 'self-hosted'/);
|
|
assert.match(
|
|
job.steps[prune].run,
|
|
/docker image prune --all --force --filter 'label=org\.qwen-code\.ci\.sandbox=true' --filter 'until=24h'/,
|
|
);
|
|
assert.match(job.steps[prune].run, /docker image prune --force/);
|
|
assert.match(job.steps[prune].run, /until=24h/);
|
|
// A failing prune must stay diagnosable: surface a warning instead of a
|
|
// silent `|| true`, and keep the daemon's error out of /dev/null.
|
|
assert.match(job.steps[prune].run, /\|\| echo "::warning::/);
|
|
assert.doesNotMatch(job.steps[prune].run, /\/dev\/null/);
|
|
assert.ok(
|
|
prune === job.steps.length - 1,
|
|
'the prune must be the final step so nothing dirties the pool after it',
|
|
);
|
|
});
|
|
|
|
it('keeps setup-node off the pool', () => {
|
|
// The action's post step uploads the npm cache to GitHub; on the
|
|
// pool's slow egress that save ran 14+ minutes and timed out
|
|
// security-checks' first pool run (2026-08-26). Hosted keeps the
|
|
// action; the pool reuses the machine's Node and its persistent npm
|
|
// cache, exactly as ci.yml does.
|
|
const setup = job.steps.find((s) =>
|
|
String(s.uses || '').startsWith('actions/setup-node'),
|
|
);
|
|
assert.ok(setup, 'the hosted setup-node step must exist');
|
|
assert.equal(setup.if, "${{ runner.environment == 'github-hosted' }}");
|
|
const preflight = job.steps.find(
|
|
(s) => s.uses === './.github/actions/self-hosted-node',
|
|
);
|
|
assert.ok(preflight, 'the pool lane must use the pre-installed Node');
|
|
assert.equal(preflight.if, "${{ runner.environment == 'self-hosted' }}");
|
|
});
|
|
});
|
|
|
|
describe('qwen-autofix.yml scan-lane runner routing', () => {
|
|
// route and review-scan gate the WHOLE fan-out: while they sit queued no
|
|
// review-address leg starts. A hosted-runner backlog queued them past the
|
|
// cron period, and the cron supersede rule then starved every scan round
|
|
// (2026-08-25) — so pin the lane on the persistent pool, with the
|
|
// fork-trust clause and the kill-switch intact.
|
|
const autofixDoc = parse(
|
|
readFileSync(join(workflowsDir, 'qwen-autofix.yml'), 'utf8'),
|
|
);
|
|
// evalRunsOn unwraps the winning fromJSON label to the array it names, so
|
|
// compare against arrays, not the ECS/HOSTED string constants above.
|
|
const ECS_LABELS = ['self-hosted', 'linux', 'x64', 'ecs-qwen'];
|
|
const HOSTED_LABELS = ['ubuntu-latest'];
|
|
|
|
for (const jobName of ['route', 'review-scan']) {
|
|
const runsOn = String(autofixDoc.jobs[jobName]['runs-on']);
|
|
|
|
it(`${jobName} reaches the persistent pool on schedule, dispatch, issue_comment, and issues`, () => {
|
|
// issue_comment is route's /takeover and /retry lane, issues its
|
|
// label/assign trigger lane for issue-autofix — pin both beside the
|
|
// cron and dispatch triggers so a later event-allowlist narrowing of
|
|
// the pool clause cannot silently demote either back to hosted.
|
|
for (const eventName of [
|
|
'schedule',
|
|
'workflow_dispatch',
|
|
'issue_comment',
|
|
'issues',
|
|
]) {
|
|
assert.deepEqual(
|
|
evalRunsOn(runsOn, {
|
|
ecsDisabled: false,
|
|
eventName,
|
|
sameRepo: false,
|
|
assoc: '',
|
|
}),
|
|
ECS_LABELS,
|
|
`${jobName} must scan from the pool on ${eventName}`,
|
|
);
|
|
}
|
|
});
|
|
|
|
it(`${jobName} keeps untrusted fork PR lanes hosted`, () => {
|
|
for (const eventName of ['pull_request', 'pull_request_review']) {
|
|
assert.deepEqual(
|
|
evalRunsOn(runsOn, {
|
|
ecsDisabled: false,
|
|
eventName,
|
|
sameRepo: false,
|
|
assoc: 'NONE',
|
|
}),
|
|
HOSTED_LABELS,
|
|
`${jobName} fork lane (${eventName}) must stay hosted`,
|
|
);
|
|
assert.deepEqual(
|
|
evalRunsOn(runsOn, {
|
|
ecsDisabled: false,
|
|
eventName,
|
|
sameRepo: true,
|
|
assoc: 'NONE',
|
|
}),
|
|
ECS_LABELS,
|
|
`${jobName} same-repo lane (${eventName}) must reach the pool`,
|
|
);
|
|
}
|
|
});
|
|
|
|
it(`${jobName} obeys the kill-switch on every event`, () => {
|
|
for (const eventName of [
|
|
'schedule',
|
|
'workflow_dispatch',
|
|
'issue_comment',
|
|
'pull_request',
|
|
'pull_request_review',
|
|
]) {
|
|
assert.deepEqual(
|
|
evalRunsOn(runsOn, {
|
|
ecsDisabled: true,
|
|
eventName,
|
|
sameRepo: true,
|
|
assoc: 'OWNER',
|
|
}),
|
|
HOSTED_LABELS,
|
|
`kill-switch must win on ${eventName}`,
|
|
);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
describe('trusted-PR lanes moved off the hosted queue (#13245)', () => {
|
|
// Each lane reaches the pool for in-repo heads, write-access authors and
|
|
// its non-PR triggers; an untrusted fork stays hosted (these either build
|
|
// PR code or hold a write token), and the kill-switch wins everywhere.
|
|
const ECS_LABELS = ['self-hosted', 'linux', 'x64', 'ecs-qwen'];
|
|
const HOSTED_LABELS = ['ubuntu-latest'];
|
|
const lanes = [
|
|
['tui-parity.yml', 'parity', 'pull_request', ['workflow_dispatch']],
|
|
['tui-parity.yml', 'noflicker', 'pull_request', ['workflow_dispatch']],
|
|
[
|
|
'sdk-java.yml',
|
|
'flyway-migrations',
|
|
'pull_request',
|
|
['push', 'workflow_dispatch'],
|
|
],
|
|
// managed-agent-gate.yml calls sdk-java.yml on an approval, whose event
|
|
// a fork PR can raise too: approving one must not put it on the pool.
|
|
[
|
|
'sdk-java.yml',
|
|
'mysql-integration',
|
|
'pull_request_review',
|
|
['push', 'merge_group', 'workflow_dispatch'],
|
|
],
|
|
// The gate's own jobs report the would-be required check, so a starved
|
|
// hosted queue must not hold them; fork PRs and fork approvals stay hosted.
|
|
[
|
|
'managed-agent-gate.yml',
|
|
'scope',
|
|
'pull_request',
|
|
['push', 'merge_group'],
|
|
],
|
|
[
|
|
'managed-agent-gate.yml',
|
|
'scope',
|
|
'pull_request_review',
|
|
['push', 'merge_group'],
|
|
],
|
|
[
|
|
'sdk-java.yml',
|
|
'event-transport-broker',
|
|
'pull_request',
|
|
['schedule', 'workflow_dispatch'],
|
|
],
|
|
[
|
|
'sdk-java.yml',
|
|
'event-transport-multi-node',
|
|
'pull_request',
|
|
['schedule', 'workflow_dispatch'],
|
|
],
|
|
// Both gate lanes also run in the gate's approval-triggered full tier.
|
|
[
|
|
'sdk-java.yml',
|
|
'event-transport-broker',
|
|
'pull_request_review',
|
|
['schedule', 'workflow_dispatch'],
|
|
],
|
|
[
|
|
'sdk-java.yml',
|
|
'event-transport-multi-node',
|
|
'pull_request_review',
|
|
['schedule', 'workflow_dispatch'],
|
|
],
|
|
[
|
|
'assign-pr-owner.yml',
|
|
'assign',
|
|
'pull_request_target',
|
|
['workflow_dispatch'],
|
|
],
|
|
];
|
|
|
|
for (const [file, jobName, prEvent, otherEvents] of lanes) {
|
|
const doc = parse(readFileSync(join(workflowsDir, file), 'utf8'));
|
|
const job = doc.jobs[jobName];
|
|
const runsOn = String(job['runs-on']);
|
|
const route = (eventName, sameRepo, assoc, ecsDisabled = false) =>
|
|
evalRunsOn(runsOn, { ecsDisabled, eventName, sameRepo, assoc });
|
|
|
|
it(`${file} ${jobName}: trusted PRs and non-PR triggers reach the pool`, () => {
|
|
assert.match(runsOn, /github\.repository == 'QwenLM\/qwen-code'/);
|
|
assert.deepEqual(route(prEvent, true, 'NONE'), ECS_LABELS);
|
|
for (const assoc of TRUSTED) {
|
|
assert.deepEqual(route(prEvent, false, assoc), ECS_LABELS, assoc);
|
|
}
|
|
for (const eventName of otherEvents) {
|
|
assert.deepEqual(route(eventName, false, ''), ECS_LABELS, eventName);
|
|
}
|
|
});
|
|
|
|
it(`${file} ${jobName}: untrusted forks stay hosted`, () => {
|
|
for (const assoc of ['CONTRIBUTOR', 'FIRST_TIME_CONTRIBUTOR', 'NONE']) {
|
|
assert.deepEqual(route(prEvent, false, assoc), HOSTED_LABELS, assoc);
|
|
}
|
|
});
|
|
|
|
it(`${file} ${jobName}: the kill-switch wins on every event`, () => {
|
|
for (const eventName of [prEvent, ...otherEvents]) {
|
|
assert.deepEqual(
|
|
route(eventName, true, 'OWNER', true),
|
|
HOSTED_LABELS,
|
|
eventName,
|
|
);
|
|
}
|
|
});
|
|
|
|
it(`${file} ${jobName}: restores pool workspace ownership before checkout`, () => {
|
|
const names = job.steps.map((s) => s.name);
|
|
const heal = names.indexOf('Restore workspace ownership');
|
|
const checkout = job.steps.findIndex((s) =>
|
|
String(s.uses || '').startsWith('actions/checkout'),
|
|
);
|
|
assert.ok(heal !== -1 && heal < checkout);
|
|
assert.equal(
|
|
job.steps[heal].if,
|
|
"${{ runner.environment == 'self-hosted' }}",
|
|
);
|
|
});
|
|
}
|
|
|
|
it('release-managed-agent-reference.yml build: only trusted PR dry runs reach the pool', () => {
|
|
const doc = parse(
|
|
readFileSync(
|
|
join(workflowsDir, 'release-managed-agent-reference.yml'),
|
|
'utf8',
|
|
),
|
|
);
|
|
const runsOn = String(doc.jobs.build['runs-on']);
|
|
const route = (eventName, sameRepo, assoc, ecsDisabled = false) =>
|
|
evalRunsOn(runsOn, { ecsDisabled, eventName, sameRepo, assoc });
|
|
assert.deepEqual(route('pull_request', true, 'NONE'), ECS_LABELS);
|
|
for (const assoc of TRUSTED) {
|
|
assert.deepEqual(route('pull_request', false, assoc), ECS_LABELS, assoc);
|
|
}
|
|
for (const assoc of ['CONTRIBUTOR', 'FIRST_TIME_CONTRIBUTOR', 'NONE']) {
|
|
assert.deepEqual(route('pull_request', false, assoc), HOSTED_LABELS);
|
|
}
|
|
// The dispatch build produces the published assets.
|
|
assert.deepEqual(route('workflow_dispatch', true, ''), HOSTED_LABELS);
|
|
assert.deepEqual(route('pull_request', true, 'OWNER', true), HOSTED_LABELS);
|
|
assert.equal(doc.jobs.publish['runs-on'], 'ubuntu-latest');
|
|
});
|
|
|
|
it('release-managed-agent-reference.yml build is safe on a shared pool host', () => {
|
|
const text = readFileSync(
|
|
join(workflowsDir, 'release-managed-agent-reference.yml'),
|
|
'utf8',
|
|
);
|
|
const build = parse(text).jobs.build;
|
|
// A service container cannot take a command, so it always reserves native
|
|
// AIO contexts and can exhaust the pool host's fs.aio-max-nr (#14416).
|
|
assert.equal(build.services, undefined);
|
|
const start = build.steps.find(
|
|
(s) => s.name === 'Start MySQL without native AIO',
|
|
);
|
|
assert.match(start.run, /--publish '127\.0\.0\.1::3306'/);
|
|
assert.match(start.run, /type=tmpfs,destination=\/var\/lib\/mysql/);
|
|
assert.match(start.run, /--innodb-use-native-aio=0/);
|
|
assert.match(start.run, /MYSQL_PORT=.*GITHUB_ENV/);
|
|
const remove = build.steps.find(
|
|
(s) => s.name === 'Remove the MySQL container',
|
|
);
|
|
assert.match(remove.if, /always\(\)/);
|
|
const run = (name) => build.steps.find((s) => s.name === name).run;
|
|
assert.match(run('Build Java components'), /-Dmaven\.repo\.local=/);
|
|
assert.match(
|
|
run('Build Java components'),
|
|
/rm -rf -- "\$\{repo\}\/com\/alibaba\/qwencode-sdk" "\$\{repo\}\/com\/alibaba\/"qwen-managed-agent-\*/,
|
|
);
|
|
assert.match(
|
|
run('Assemble reference distribution'),
|
|
/rm -rf -- "\$\{RUNNER_TEMP\}\/managed-agent-reference"\n\s*node scripts\/package-managed-agent-reference\.mjs/,
|
|
);
|
|
const boot = run('Verify and boot the exact archive');
|
|
assert.match(boot, /SERVER_PORT="\$\{server_port\}"/);
|
|
assert.doesNotMatch(boot, /:8080|:3306\//);
|
|
const setupNode = build.steps.find((s) =>
|
|
String(s.uses || '').startsWith('actions/setup-node'),
|
|
);
|
|
assert.equal(setupNode.if, "${{ runner.environment == 'github-hosted' }}");
|
|
const setupJava = build.steps.find((s) =>
|
|
String(s.uses || '').startsWith('actions/setup-java'),
|
|
);
|
|
const cache = setupJava.with.cache;
|
|
for (const [environment, expected] of [
|
|
['github-hosted', 'maven'],
|
|
['self-hosted', ''],
|
|
]) {
|
|
const actual = cache.startsWith('${{')
|
|
? new Function('runner', `return (${cache.slice(3, -2)});`)({
|
|
environment,
|
|
})
|
|
: cache;
|
|
assert.equal(actual, expected, environment);
|
|
}
|
|
});
|
|
|
|
it('tui-parity keeps setup-node and the pnpm cache off the pool', () => {
|
|
// nodejs.org may be unreachable through the pool's egress proxy, and the
|
|
// pool keeps its pnpm store on local disk — the ci.yml split.
|
|
const doc = parse(
|
|
readFileSync(join(workflowsDir, 'tui-parity.yml'), 'utf8'),
|
|
);
|
|
for (const jobName of ['parity', 'noflicker']) {
|
|
const steps = doc.jobs[jobName].steps;
|
|
const hostedOnly = steps.filter(
|
|
(s) =>
|
|
String(s.uses || '').startsWith('actions/setup-node') ||
|
|
s.uses === './.github/actions/pnpm-store-cache',
|
|
);
|
|
assert.equal(hostedOnly.length, 2, jobName);
|
|
for (const s of hostedOnly) {
|
|
assert.equal(s.if, "${{ runner.environment == 'github-hosted' }}");
|
|
}
|
|
const preflight = steps.find(
|
|
(s) => s.uses === './.github/actions/self-hosted-node',
|
|
);
|
|
assert.equal(preflight?.if, "${{ runner.environment == 'self-hosted' }}");
|
|
assert.ok(
|
|
steps.some((s) => String(s.run || '').includes('check-disk-floor.sh')),
|
|
`${jobName} must fail fast on a saturated host`,
|
|
);
|
|
}
|
|
// accept-noflicker.sh defaults its report to a fixed /tmp path; one ECS
|
|
// host runs many jobs at once, so the report must be runner-scoped.
|
|
const gate = doc.jobs.noflicker.steps.find((s) =>
|
|
String(s.run || '').includes('accept-noflicker.sh'),
|
|
);
|
|
assert.equal(gate.env?.OUT, '${{ runner.temp }}/opentui-noflicker-out');
|
|
});
|
|
|
|
it('tui-parity changes gate is routed like the lanes it guards', () => {
|
|
// The changes job is the door to parity/noflicker: a 40-minute hosted
|
|
// queue wait behind it starves two pool-routed lanes. Route it like them;
|
|
// it runs no checkout, so the workspace-restore pin does not apply.
|
|
const doc = parse(
|
|
readFileSync(join(workflowsDir, 'tui-parity.yml'), 'utf8'),
|
|
);
|
|
const job = doc.jobs.changes;
|
|
const runsOn = String(job['runs-on']);
|
|
const route = (eventName, sameRepo, assoc, ecsDisabled = false) =>
|
|
evalRunsOn(runsOn, { ecsDisabled, eventName, sameRepo, assoc });
|
|
assert.match(runsOn, /github\.repository == 'QwenLM\/qwen-code'/);
|
|
assert.deepEqual(route('pull_request', true, 'NONE'), ECS_LABELS);
|
|
for (const assoc of TRUSTED) {
|
|
assert.deepEqual(route('pull_request', false, assoc), ECS_LABELS, assoc);
|
|
}
|
|
assert.deepEqual(route('workflow_dispatch', false, ''), ECS_LABELS);
|
|
for (const assoc of ['CONTRIBUTOR', 'FIRST_TIME_CONTRIBUTOR', 'NONE']) {
|
|
assert.deepEqual(
|
|
route('pull_request', false, assoc),
|
|
HOSTED_LABELS,
|
|
assoc,
|
|
);
|
|
}
|
|
for (const eventName of ['pull_request', 'workflow_dispatch']) {
|
|
assert.deepEqual(
|
|
route(eventName, true, 'OWNER', true),
|
|
HOSTED_LABELS,
|
|
eventName,
|
|
);
|
|
}
|
|
// Byte-identity with the lanes it guards is this change's own acceptance
|
|
// criterion: the gate is their door, so drift here is what re-starves
|
|
// them behind a hosted queue.
|
|
for (const lane of ['parity', 'noflicker']) {
|
|
assert.equal(
|
|
runsOn,
|
|
String(doc.jobs[lane]['runs-on']),
|
|
`the gate must be routed by the same expression as ${lane}`,
|
|
);
|
|
}
|
|
// Any added step would run on the reused pool workspace with no hygiene
|
|
// step before it, so the whole step list is pinned, not just checkouts.
|
|
assert.deepEqual(
|
|
job.steps.map((s) => s.uses),
|
|
['actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3'],
|
|
'changes gate must stay checkout-free and shell-free; adding a step re-adds the pool-hygiene requirement',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('control-plane jobs moved off the hosted queue', () => {
|
|
// None of these checks out or runs PR code, so each reaches the pool on
|
|
// every trigger, fork-raised events included; the kill-switch wins.
|
|
const ECS_LABELS = ['self-hosted', 'linux', 'x64', 'ecs-qwen'];
|
|
const HOSTED_LABELS = ['ubuntu-latest'];
|
|
const jobs = [
|
|
[
|
|
'qwen-code-pr-review.yml',
|
|
'delay-automatic-review',
|
|
['pull_request_target'],
|
|
],
|
|
['assign-issue-owner.yml', 'assign', ['issues', 'workflow_dispatch']],
|
|
[
|
|
'auto-minimize-spam.yml',
|
|
'minimize',
|
|
[
|
|
'issue_comment',
|
|
'pull_request_review_comment',
|
|
'schedule',
|
|
'push',
|
|
'workflow_dispatch',
|
|
],
|
|
],
|
|
];
|
|
|
|
for (const [file, jobName, events] of jobs) {
|
|
const job = parse(readFileSync(join(workflowsDir, file), 'utf8')).jobs[
|
|
jobName
|
|
];
|
|
const runsOn = String(job['runs-on']);
|
|
const route = (eventName, assoc, ecsDisabled = false) =>
|
|
evalRunsOn(runsOn, { ecsDisabled, eventName, sameRepo: false, assoc });
|
|
const checkouts = job.steps.filter((s) =>
|
|
String(s.uses || '').startsWith('actions/checkout'),
|
|
);
|
|
|
|
it(`${file} ${jobName}: every trigger reaches the pool, forks included`, () => {
|
|
assert.match(runsOn, /github\.repository == 'QwenLM\/qwen-code'/);
|
|
for (const eventName of events) {
|
|
for (const assoc of ASSOCIATIONS) {
|
|
assert.deepEqual(route(eventName, assoc), ECS_LABELS, eventName);
|
|
}
|
|
}
|
|
});
|
|
|
|
it(`${file} ${jobName}: the kill-switch wins on every event`, () => {
|
|
for (const eventName of events) {
|
|
assert.deepEqual(route(eventName, 'OWNER', true), HOSTED_LABELS);
|
|
}
|
|
});
|
|
|
|
it(`${file} ${jobName}: checks out only the default branch`, () => {
|
|
// An omitted ref is the default branch on issues events and the
|
|
// write-access dispatcher's choice on workflow_dispatch.
|
|
for (const step of checkouts) {
|
|
assert.ok(
|
|
[undefined, '${{ github.event.repository.default_branch }}'].includes(
|
|
step.with?.ref,
|
|
),
|
|
String(step.with?.ref),
|
|
);
|
|
}
|
|
});
|
|
|
|
if (checkouts.length > 0) {
|
|
it(`${file} ${jobName}: checkout is safe on a reused pool workspace`, () => {
|
|
const heal = job.steps.findIndex(
|
|
(s) => s.name === 'Restore workspace ownership',
|
|
);
|
|
assert.ok(heal !== -1 && heal < job.steps.indexOf(checkouts[0]));
|
|
assert.equal(
|
|
job.steps[heal].if,
|
|
"${{ runner.environment == 'self-hosted' }}",
|
|
);
|
|
// Cone mode rejects a file entry the reused index holds as a file.
|
|
for (const step of checkouts) {
|
|
assert.equal(step.with['sparse-checkout'], '.github/scripts');
|
|
assert.equal(step.with['sparse-checkout-cone-mode'], undefined);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
describe('credential-free release fanout routing (#15465)', () => {
|
|
const reference = parse(
|
|
readFileSync(join(workflowsDir, 'repo-hygiene.yml'), 'utf8'),
|
|
).jobs.dedup['runs-on'];
|
|
it('keeps the Desktop reusable caller limited to schedule and dispatch', () => {
|
|
const caller = parse(
|
|
readFileSync(join(workflowsDir, 'desktop-packaging-check.yml'), 'utf8'),
|
|
);
|
|
assert.deepEqual(Object.keys(caller.on).sort(), [
|
|
'schedule',
|
|
'workflow_dispatch',
|
|
]);
|
|
assert.equal(
|
|
caller.jobs.package.uses,
|
|
'./.github/workflows/desktop-release.yml',
|
|
);
|
|
});
|
|
for (const [file, jobId, events] of [
|
|
['desktop-release.yml', 'prepare', ['release', 'workflow_dispatch']],
|
|
['live-host-release.yml', 'prepare', ['workflow_dispatch']],
|
|
[
|
|
'dsw-swe-verified-release.yml',
|
|
'release_gate',
|
|
['release', 'workflow_dispatch'],
|
|
],
|
|
[
|
|
'release-managed-runtime.yml',
|
|
'prepare',
|
|
['release', 'workflow_dispatch'],
|
|
],
|
|
]) {
|
|
const doc = parse(readFileSync(join(workflowsDir, file), 'utf8'));
|
|
const job = doc.jobs[jobId];
|
|
const expression = job['runs-on'];
|
|
it(`${file}: routes release preparation to ECS with hosted fallbacks`, () => {
|
|
assert.equal(
|
|
expression,
|
|
file === 'live-host-release.yml'
|
|
? reference.replace(
|
|
"vars.MAINTAINER_ECS_RUNNER_DISABLED != 'true'",
|
|
"vars.MAINTAINER_ECS_RUNNER_DISABLED != 'true' && github.event_name != 'pull_request'",
|
|
)
|
|
: reference,
|
|
);
|
|
for (const eventName of events) {
|
|
assert.ok(Object.hasOwn(doc.on, eventName));
|
|
assert.deepEqual(
|
|
evalRunsOn(expression, { eventName, ecsDisabled: false }),
|
|
JSON.parse(ECS),
|
|
);
|
|
assert.deepEqual(
|
|
evalRunsOn(expression, { eventName, ecsDisabled: true }),
|
|
JSON.parse(HOSTED),
|
|
);
|
|
assert.deepEqual(
|
|
evalRunsOn(
|
|
expression.replace(
|
|
"github.repository == 'QwenLM/qwen-code'",
|
|
'false',
|
|
),
|
|
{ eventName, ecsDisabled: false },
|
|
),
|
|
JSON.parse(HOSTED),
|
|
);
|
|
}
|
|
});
|
|
it(`${file}: keeps release secrets and PR execution outside this migration`, () => {
|
|
assert.doesNotMatch(JSON.stringify([doc.env, job]), /secrets(?:\.|\[)/);
|
|
assert.equal(job.environment, undefined);
|
|
assert.equal((job.permissions ?? doc.permissions).contents, 'read');
|
|
assert.doesNotMatch(
|
|
expression,
|
|
/ECS_RELEASE_HOST_RUNNER_ENABLED|ecs-qwen-release-host/,
|
|
);
|
|
if (file === 'live-host-release.yml') {
|
|
assert.ok(Object.hasOwn(doc.on, 'pull_request'));
|
|
assert.deepEqual(
|
|
evalRunsOn(expression, {
|
|
eventName: 'pull_request',
|
|
ecsDisabled: false,
|
|
sameRepo: true,
|
|
assoc: 'OWNER',
|
|
}),
|
|
JSON.parse(HOSTED),
|
|
);
|
|
assert.deepEqual(
|
|
evalRunsOn(expression, {
|
|
eventName: 'pull_request',
|
|
ecsDisabled: false,
|
|
sameRepo: false,
|
|
assoc: 'NONE',
|
|
}),
|
|
JSON.parse(HOSTED),
|
|
);
|
|
} else {
|
|
assert.ok(
|
|
!Object.keys(doc.on).some((event) =>
|
|
event.startsWith('pull_request'),
|
|
),
|
|
);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// The patrol holds CI_BOT_PAT and runs a sandboxed agent, so its routing is
|
|
// evaluated here rather than left to substring pins: `== 'true'` contains
|
|
// every term a `includes("!= 'true'")` style assertion looks for, and an
|
|
// inverted kill switch would send every round to the pool it just disabled.
|
|
describe('CI failure patrol routing (#16462)', () => {
|
|
const doc = parse(
|
|
readFileSync(join(workflowsDir, 'qwen-ci-flaky-rerun.yml'), 'utf8'),
|
|
);
|
|
const events = Object.keys(doc.on);
|
|
it('reaches the pool only for this repository with the switch off', () => {
|
|
assert.deepEqual(events.sort(), ['schedule', 'workflow_dispatch']);
|
|
for (const jobName of ['classify', 'act']) {
|
|
const expression = doc.jobs[jobName]['runs-on'];
|
|
for (const eventName of events) {
|
|
assert.deepEqual(
|
|
evalRunsOn(expression, { eventName, ecsDisabled: false }),
|
|
JSON.parse(ECS),
|
|
`${jobName} on ${eventName} with the fleet enabled`,
|
|
);
|
|
assert.deepEqual(
|
|
evalRunsOn(expression, { eventName, ecsDisabled: true }),
|
|
JSON.parse(HOSTED),
|
|
`${jobName} on ${eventName} with the fleet disabled`,
|
|
);
|
|
assert.deepEqual(
|
|
evalRunsOn(
|
|
expression.replace(
|
|
"github.repository == 'QwenLM/qwen-code'",
|
|
'false',
|
|
),
|
|
{ eventName, ecsDisabled: false },
|
|
),
|
|
JSON.parse(HOSTED),
|
|
`${jobName} on ${eventName} in a fork`,
|
|
);
|
|
}
|
|
}
|
|
});
|
|
});
|