fix(ci): keep the pooled visuals capture fork-only

The capture's runs-on was rewritten in acbbc6d9 to read only
github.repository and the kill-switch, so every same-repo PR was routed
onto the disposable pool too — contradicting the design doc's "trusted
and same-repository PR routes stay unchanged" rule and the PR's own
purpose (moving fork CI off GitHub-hosted runners, not relocating jobs
that already had a lane).

The expression now carries the fork clause: untrusted fork PRs ride the
ephemeral pool, same-repo PRs stay hosted. The routed timeout follows the
same fork term (90 pool / 45 hosted), the routing matrix pins both arms
per association, and both design docs record the fork-only scope.

Co-authored-by: Qwen-Coder <[email protected]>
This commit is contained in:
yiliang114andQwen-Coder committed 2026-11-09 00:39:17 +08:00
1 parent ed4d460580
commit f90e7f0960
4 files changed
+55 -26

No files matched your search

+35 -11
View File
@@ -93,6 +93,10 @@ function evalRunsOn(expression, { ecsDisabled, eventName, sameRepo, assoc }) {
/github\.event\.pull_request\.head\.repo\.full_name == github\.repository/,
String(sameRepo),
],
[
/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)),
@@ -877,16 +881,20 @@ describe('credential-free jobs on the disposable pool', () => {
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.
// The pool exists for untrusted fork PRs only: same-repo PRs stay on
// the hosted lane, where this job ran before the migration.
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'/);
// The expression reads neither head.repo nor author_association (the
// repo clause is substituted to a constant), so one association sweep
// covers every fork and same-repo shape.
assert.match(
runsOn,
/github\.event\.pull_request\.head\.repo\.full_name != github\.repository/,
'capture must keep the fork clause: only fork PRs route to the disposable pool',
);
for (const assoc of ASSOCIATIONS) {
// Fork PR: the disposable pool, whoever authored it.
assert.deepEqual(
evalRunsOn(runsOn, {
ecsDisabled: false,
@@ -895,8 +903,23 @@ describe('credential-free jobs on the disposable pool', () => {
assoc,
}),
EPHEMERAL,
`capture assoc='${assoc}'`,
`capture fork assoc='${assoc}'`,
);
// Same-repo PR: stays hosted — the disposable pool must never serve
// trusted runs, and the kill-switch question never arises there.
for (const ecsDisabled of [false, true]) {
assert.deepEqual(
evalRunsOn(runsOn, {
ecsDisabled,
eventName: 'pull_request',
sameRepo: true,
assoc,
}),
JSON.parse(HOSTED),
`capture same-repo assoc='${assoc}' ecsDisabled=${ecsDisabled}`,
);
}
// Kill-switch reverts fork runs to hosted too.
assert.deepEqual(
evalRunsOn(runsOn, {
ecsDisabled: true,
@@ -905,7 +928,7 @@ describe('credential-free jobs on the disposable pool', () => {
assoc,
}),
JSON.parse(HOSTED),
'kill-switch must revert capture to hosted',
`kill-switch must revert fork capture to hosted assoc='${assoc}'`,
);
}
// Only the event-scoped read-only GITHUB_TOKEN may reach the disposable
@@ -994,13 +1017,14 @@ describe('credential-free jobs on the disposable pool', () => {
Number(diskFloor?.env?.DISK_FLOOR_MAX_USED_PCT) < 89,
'the ceiling must refuse the measured 89%-used failure slot',
);
// The pool pays two cold installs (measured: 22m18s install, Chromium
// still downloading at 36m48s), so the hosted 45-minute cap cancels
// the job before any render. The ceiling is routed like runs-on —
// runner.environment is not available in timeout-minutes.
// The fork pool pays two cold installs (measured: 22m18s install,
// Chromium still downloading at 36m48s), so the hosted 45-minute cap
// cancels the job before any render. The ceiling is routed like
// runs-on — runner.environment is not available in timeout-minutes —
// keyed off the same fork term: pool (fork, not disabled) 90, else 45.
assert.equal(
String(doc.jobs.capture['timeout-minutes']),
"${{ vars.MAINTAINER_ECS_RUNNER_DISABLED == 'true' && 45 || 90 }}",
"${{ (github.event.pull_request.head.repo.full_name != github.repository && vars.MAINTAINER_ECS_RUNNER_DISABLED != 'true') && 90 || 45 }}",
);
// Unpinned workers size Chromium from the HOST's cores inside a 4-CPU
// container: the first pooled run lost its runner to CPU/memory
+9 -8
View File
@@ -56,15 +56,16 @@ jobs:
capture:
name: 'Capture web-shell visuals (ubuntu-latest, Node 22.x)'
if: "${{ github.repository == 'QwenLM/qwen-code' }}"
# 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"]'') }}'
# The disposable pool exists for untrusted fork PRs only (design doc:
# "Trusted and same-repository PR routes stay unchanged"). Same-repo PRs
# stay on the hosted lane, where this job ran before. The kill-switch
# reverts forks to hosted too, where every step behaves as before.
runs-on: '${{ (github.repository == ''QwenLM/qwen-code'' && vars.MAINTAINER_ECS_RUNNER_DISABLED != ''true'' && github.event.pull_request.head.repo.full_name != github.repository) && fromJSON(''["self-hosted", "ecs-qwen-ephemeral"]'') || fromJSON(''["ubuntu-latest"]'') }}'
# Cold-store pool runs measured 22m+ installs before any render, so the
# pool arm gets 90 and hosted keeps 45. runner.environment is not
# available here; key off the same variable runs-on reads. Rationale and
# measurements: docs/design/2026-11-07-ephemeral-container-runners.md.
timeout-minutes: "${{ vars.MAINTAINER_ECS_RUNNER_DISABLED == 'true' && 45 || 90 }}"
# fork pool arm gets 90 and same-repo/hosted keeps 45. runner.environment
# is not available here; key off the same fork term runs-on reads.
# Rationale and measurements: docs/design/2026-11-07-ephemeral-container-runners.md.
timeout-minutes: "${{ (github.event.pull_request.head.repo.full_name != github.repository && vars.MAINTAINER_ECS_RUNNER_DISABLED != 'true') && 90 || 45 }}"
steps:
- name: 'Checkout PR head'
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
@@ -183,9 +183,12 @@ keep their current routing. This avoids migrating browser system dependencies
that the new image has not verified, and leaves the routing image relationship
of the existing classifier job and the other workflows unchanged.
Update 2026-11-08: the credential-free Web-shell Visuals capture job has
moved onto this pool; the main CI `Test (ubuntu-latest, Node 22.x)`
aggregate gate was tried here and moved back off (below).
Update 2026-11-08: the credential-free Web-shell Visuals capture job
moves onto this pool **for untrusted fork PRs only** — same-repo PRs stay
on the hosted lane, where the job ran before (the original rule above,
"trusted and same-repository PR routes stay unchanged", still governs).
The main CI `Test (ubuntu-latest, Node 22.x)` aggregate gate was tried
here and moved back off (below).
The capture job matches the pool's security
model exactly (untrusted PR code, read-only token, only the event-scoped
`GITHUB_TOKEN`), which supersedes the browser-dependency exclusion above for
@@ -144,10 +144,11 @@ JIT 提供正常协议下的单任务注册语义;宿主机管理程序负责
保持当前路由。这样不把新镜像尚未验证的浏览器系统依赖一起迁入,也不改变
现有分类 job 与其他 workflow 的路由镜像关系。
2026-11-08 更新:无凭据的 Web-shell Visuals capture 任务已迁入本池;
主 CI 的 `Test (ubuntu-latest, Node 22.x)` 聚合门禁曾迁到这里、又迁了
回去(见下)。capture
任务与本池安全模型完全一致(不可信 PR 代码、只读 token、仅事件级
2026-11-08 更新:无凭据的 Web-shell Visuals capture 任务迁入本池,
**仅限非可信 fork PR**——同仓 PR 留在托管泳道(即本任务迁移前所在
位置;上文「受信与同仓 PR 路由不变」的规则仍然有效)。主 CI 的
`Test (ubuntu-latest, Node 22.x)` 聚合门禁曾迁到这里、又迁了回去
(见下)。capture 任务与本池安全模型完全一致(不可信 PR 代码、只读 token、仅事件级
`GITHUB_TOKEN`),因此上面「不迁浏览器依赖」的排除对这一个任务不再适用:
它在 self-hosted 前置检查后复用镜像自带 Node,先过按本泳道标定的磁盘
水位门禁(剩余 ≥8 GiB 且已用 ≤85%——共享的 2 GiB 默认值曾放行一个