mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-23 23:55:50 +00:00
* fix(ci): back-port the checkout-heal wipe guard to the triage and serve-ab wipes The "empty the workspace, keep the directory" idiom exists in three copies; only the review workflow's copy received the #9220 hardening (canonicalization, trailing-slash strip, RUNNER_WORKSPACE allowlist). Measured on main for #9265, the two triage guards let non-canonical spellings of the guarded roots through (/home/, /home/., //usr, /root/, /var/ all reached the rm), and serve-ab's wipe had no guard at all — even `/home` or an empty string arrived at `find … -exec rm -rf`. Port the reference guard to all three sites, keeping each site's exit contract: triage fails loud both before and after external code, serve-ab stays bare under the job's `-eo pipefail` so an unclearable workspace fails before either checkout builds on top of the leftovers. Pin each ported copy with its own tests: bad-path batteries under an rm recorder (the destructive primitive cannot fire under any edit), an allowlist-escaping `..` case gated on a GNU-realpath host probe (the lesson from 90fa6bb4), a realpath-absent trailing-slash RUNNER_WORKSPACE case, and text pins on the ported layers. Every pin was mutation-verified red against a deletion of the layer it guards. * test(ci): pin guarded serve wipe * fix(ci): close wipe guard fallback gaps * fix(ci): fail closed without realpath * fix(ci): keep wipe guards portable * test(ci): pin wipe-guard RWS layers and unmask the pre-run battery - run the rewritten pre-run sweep battery under -e -o pipefail so a failing sweep can no longer report success (bare bash -c masked it) - pin the RWS '..' refusal and degenerate-root refusal text in all copies, and add RUNNER_WORKSPACE='/' exec cases to both copy suites - exercise both pre-run and post-run copies in the realpath-absent refusal test - replace the '..' escape vector with a symlink escape that only the realpath line can refuse, and correct the mutant-outcome comments - add the serve-ab wipe-before-checkouts ordering pin from the sister suite and a happy-path RWS canonicalization pin * test(ci): correct wipe-guard mutant-outcome comments for find -P The symlink-escape comments claimed that with the WS realpath line deleted, find reaches rm through the link target. GNU find's default -P mode does not descend symlink operands: the mutant passes every guard, wipes nothing, and exits 0, so only the non-zero-status assertion catches it — the rm-log assertion passes vacuously. Reword both twin comments (R5-1). --------- Co-authored-by: yiliang114 <yiliang114@users.noreply.github.com>
199 lines
6.5 KiB
JavaScript
199 lines
6.5 KiB
JavaScript
// Runner-routing regression guards for ci.yml and serve-ab.yml.
|
|
//
|
|
// classify_pr carries the routing logic TWICE — the `runs-on` expression
|
|
// (which selects the classify job's own runner) and the `pick_runner` shell
|
|
// step (which publishes `ubuntu_runner` for every downstream Linux job). If
|
|
// they drift, classify and the Test job land on different pools. These tests
|
|
// evaluate BOTH against the same event matrix — including the negative
|
|
// associations that must stay hosted — and assert they agree.
|
|
import assert from 'node:assert/strict';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { 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 classifyRunsOn = String(ciDoc.jobs.classify_pr['runs-on']);
|
|
const pickRunner = ciDoc.jobs.classify_pr.steps.find(
|
|
(s) => s.id === 'pick_runner',
|
|
);
|
|
|
|
// GitHub expression semantics for the classify runs-on, 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.
|
|
function simulateRunsOn({ ecsDisabled, sameRepo, assoc, mergeGroup }) {
|
|
const trusted = TRUSTED.includes(assoc);
|
|
const ecs =
|
|
!ecsDisabled && (sameRepo || trusted || mergeGroup);
|
|
return ecs ? ECS : HOSTED;
|
|
}
|
|
|
|
// Executes the real pick_runner shell with the same inputs and returns the
|
|
// selected runner exactly as CI would publish it.
|
|
function runPickRunner({ ecsDisabled, sameRepo, assoc, eventName, dispatch }) {
|
|
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,
|
|
DISPATCH_LINUX_RUNNER: dispatch ?? '',
|
|
GITHUB_OUTPUT: outputFile,
|
|
},
|
|
encoding: 'utf8',
|
|
});
|
|
rmSync(tmp, { recursive: true, force: true });
|
|
assert.equal(result.status, 0, `pick_runner failed: ${result.stderr}`);
|
|
const line = result.stdout
|
|
.split('\n')
|
|
.find((l) => l.startsWith('Selected Linux runner: '));
|
|
assert.ok(line, `no selection in pick_runner output: ${result.stdout}`);
|
|
return line.slice('Selected Linux runner: '.length);
|
|
}
|
|
|
|
describe('ci.yml classify_pr runner routing', () => {
|
|
it('the expression and the shell step agree on every association', () => {
|
|
const associations = [
|
|
...TRUSTED,
|
|
'CONTRIBUTOR',
|
|
'FIRST_TIME_CONTRIBUTOR',
|
|
'FIRST_TIMER',
|
|
'NONE',
|
|
'',
|
|
];
|
|
for (const sameRepo of [true, false]) {
|
|
for (const assoc of associations) {
|
|
const expected = simulateRunsOn({
|
|
ecsDisabled: false,
|
|
sameRepo,
|
|
assoc,
|
|
mergeGroup: false,
|
|
});
|
|
const actual = runPickRunner({
|
|
ecsDisabled: false,
|
|
sameRepo,
|
|
assoc,
|
|
eventName: 'pull_request',
|
|
});
|
|
assert.equal(
|
|
actual,
|
|
expected,
|
|
`drift for sameRepo=${sameRepo} 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('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.
|
|
assert.match(
|
|
classifyRunsOn,
|
|
/contains\(fromJSON\('\["OWNER","MEMBER","COLLABORATOR"\]'\), github\.event\.pull_request\.author_association\)/,
|
|
);
|
|
assert.match(classifyRunsOn, /vars\.MAINTAINER_ECS_RUNNER_DISABLED != 'true'/);
|
|
assert.match(classifyRunsOn, /github\.event_name == 'merge_group'/);
|
|
});
|
|
});
|
|
|
|
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 before checking out PR code', () => {
|
|
const wipe = serveAbDoc.jobs.ab.steps.find(
|
|
(s) => s.name === 'Wipe stale workspace before checkout',
|
|
);
|
|
assert.ok(wipe, 'self-hosted reuse must not bleed one PR into the next');
|
|
assert.equal(wipe.if, "${{ runner.environment == 'self-hosted' }}");
|
|
assert.match(wipe.run, /find "\$WS" -mindepth 1 -maxdepth 1 -exec rm -rf/);
|
|
});
|
|
});
|