ci: route the Test aggregate gate and web-shell visuals capture to the disposable pool

Both jobs match the ecs-qwen-ephemeral pool's security model: the Test
aggregate runs one shell line and checks out nothing, and the visuals
capture builds untrusted PR code with a read-only token and no
long-lived secrets. Routing them off the hosted queue removes the
9-minute waits the one-line gate saw after its shards finished, and
takes a 45-minute render job off hosted capacity.

The MAINTAINER_ECS_RUNNER_DISABLED kill-switch reverts both to hosted.
The E2E OpenTUI leg deliberately stays hosted: it carries long-lived
model secrets, which the disposable pool's boundary forbids until a
separate trusted pool exists.

Co-authored-by: Qwen-Coder <[email protected]>
This commit is contained in:
yiliang114andQwen-Coder committed 2026-11-08 18:05:15 +08:00
1 parent e4e28d7ee0
commit acbbc6d9a1
4 files changed
+113 -9

No files matched your search

@@ -797,6 +797,105 @@ describe('ci.yml lint HOME isolation', () => {
});
});
describe('credential-free jobs on the disposable pool', () => {
const EPHEMERAL = ['self-hosted', 'ecs-qwen-ephemeral'];
it('ci.yml test_ubuntu: the checkout-free gate rides the disposable pool', () => {
// The gate runs one `test` shell line and checks out nothing, so every
// trigger and association may share the disposable pool with the fork
// shards; only the kill-switch reverts it to hosted.
const job = ciDoc.jobs.test_ubuntu;
const runsOn = String(job['runs-on']);
assert.match(runsOn, /github\.repository == 'QwenLM\/qwen-code'/);
for (const eventName of [
'pull_request',
'push',
'merge_group',
'workflow_dispatch',
]) {
for (const sameRepo of [true, false]) {
for (const assoc of ASSOCIATIONS) {
assert.deepEqual(
evalRunsOn(runsOn, {
ecsDisabled: false,
eventName,
sameRepo,
assoc,
}),
EPHEMERAL,
`gate ${eventName} sameRepo=${sameRepo} assoc='${assoc}'`,
);
assert.deepEqual(
evalRunsOn(runsOn, {
ecsDisabled: true,
eventName,
sameRepo,
assoc,
}),
JSON.parse(HOSTED),
`kill-switch must revert the gate on ${eventName}`,
);
}
}
}
assert.ok(
!job.steps.some((s) =>
String(s.uses || '').startsWith('actions/checkout'),
),
'the gate must stay checkout-free to keep the disposable-pool routing',
);
});
it('web-shell-visuals.yml capture: untrusted renders ride the disposable pool', () => {
// The capture builds and renders untrusted PR code with a read-only
// token and no secrets — the disposable pool's exact security model.
// The privileged publisher stays wherever it was; only capture moves.
const doc = parse(
readFileSync(join(workflowsDir, 'web-shell-visuals.yml'), 'utf8'),
);
const runsOn = String(doc.jobs.capture['runs-on']);
assert.match(runsOn, /github\.repository == 'QwenLM\/qwen-code'/);
for (const assoc of ASSOCIATIONS) {
for (const sameRepo of [true, false]) {
assert.deepEqual(
evalRunsOn(runsOn, {
ecsDisabled: false,
eventName: 'pull_request',
sameRepo,
assoc,
}),
EPHEMERAL,
`capture sameRepo=${sameRepo} assoc='${assoc}'`,
);
assert.deepEqual(
evalRunsOn(runsOn, {
ecsDisabled: true,
eventName: 'pull_request',
sameRepo,
assoc,
}),
JSON.parse(HOSTED),
'kill-switch must revert capture to hosted',
);
}
}
// Only the event-scoped read-only GITHUB_TOKEN may reach the disposable
// pool through this workflow; any other secret reference breaks the
// pool's no-long-lived-secrets boundary.
const text = readFileSync(
join(workflowsDir, 'web-shell-visuals.yml'),
'utf8',
);
const secretRefs = text.match(/secrets\.[A-Za-z_]+/g) ?? [];
assert.deepEqual(
[...new Set(secretRefs)],
['secrets.GITHUB_TOKEN'],
'capture must not reference any secret beyond the read-only event token',
);
assert.equal(doc.permissions.contents, 'read');
});
});
describe('ci.yml web_shell_e2e_smoke runner routing', () => {
const expression = String(ciDoc.jobs.web_shell_e2e_smoke['runs-on'])
.replace(/^\$\{\{\s*/, '')
+4 -3
View File
@@ -1520,9 +1520,10 @@ jobs:
name: 'Test (ubuntu-latest, Node 22.x)'
needs: ['classify_pr', 'test_build', 'test']
if: "${{ !cancelled() && github.event_name != 'schedule' }}"
# The shards' runners: a hosted runner queued this one-line check for
# 9 minutes after the last shard finished.
runs-on: '${{ fromJSON(needs.classify_pr.outputs.ubuntu_runner || ''["ubuntu-latest"]'') }}'
# A one-line aggregate that checks out nothing: the disposable pool runs
# it on every trigger (a hosted runner queued it for 9 minutes after the
# last shard finished), with the kill-switch as the only hosted fallback.
runs-on: '${{ (github.repository == ''QwenLM/qwen-code'' && vars.MAINTAINER_ECS_RUNNER_DISABLED != ''true'') && fromJSON(''["self-hosted", "ecs-qwen-ephemeral"]'') || fromJSON(''["ubuntu-latest"]'') }}'
timeout-minutes: 5
permissions:
contents: 'read'
+7 -3
View File
@@ -9,8 +9,9 @@ name: 'Web-shell Visuals'
#
# Security model: this workflow BUILDS AND RENDERS untrusted PR code, so it runs
# on the `pull_request` trigger (fork PRs get a read-only token and NO secrets),
# on an ephemeral hosted runner, with `contents: read` and no secrets of its
# own. It produces only image/video bytes as an artifact. The privileged step
# on a disposable runner (the ecs-qwen-ephemeral pool; hosted only under the
# kill-switch), with `contents: read` and no secrets of its own.
# It produces only image/video bytes as an artifact. The privileged step
# that needs a write token — pushing the images and commenting on the PR — lives
# in the separate workflow_run workflow that never checks out PR code.
on:
@@ -55,7 +56,10 @@ jobs:
capture:
name: 'Capture web-shell visuals (ubuntu-latest, Node 22.x)'
if: "${{ github.repository == 'QwenLM/qwen-code' }}"
runs-on: 'ubuntu-latest'
# The disposable pool matches this job's security model exactly (untrusted
# PR code, read-only token, no secrets) and takes it off the hosted queue.
# The kill-switch reverts to hosted, where every step behaves as before.
runs-on: '${{ (github.repository == ''QwenLM/qwen-code'' && vars.MAINTAINER_ECS_RUNNER_DISABLED != ''true'') && fromJSON(''["self-hosted", "ecs-qwen-ephemeral"]'') || fromJSON(''["ubuntu-latest"]'') }}'
# Leave room for cold dependency setup before both render passes.
timeout-minutes: 45
steps:
+3 -3
View File
@@ -390,10 +390,10 @@ it('reports the required Linux check only when the build and every shard pass',
const gate = ci.jobs.test_ubuntu;
expect(gate.name).toBe('Test (ubuntu-latest, Node 22.x)');
expect(gate.needs).toEqual(['classify_pr', 'test_build', 'test']);
// The gate remains on the original association-gated runner, while test
// shards can opt into the disposable pool for untrusted forks.
// The gate checks out nothing, so it rides the disposable pool on every
// trigger; only the kill-switch reverts it to hosted.
expect(gate['runs-on']).toBe(
'${{ fromJSON(needs.classify_pr.outputs.ubuntu_runner || \'["ubuntu-latest"]\') }}',
"${{ (github.repository == 'QwenLM/qwen-code' && vars.MAINTAINER_ECS_RUNNER_DISABLED != 'true') && fromJSON('[\"self-hosted\", \"ecs-qwen-ephemeral\"]') || fromJSON('[\"ubuntu-latest\"]') }}",
);
expect(condOf('test_ubuntu')).toBe(condOf('test'));
expect(gate.steps[0].run).toBe(