mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
fix(ci): route the E2E build job through the pool-first runner expression (#15885)
* fix(ci): route the E2E build job through the pool-first runner expression * fix(ci): carry the full self-hosted contract on the E2E build job * test(ci): pin the pool contract's step order and sync the build-routing design note * test(ci): pin the npm-cache position and heal body on every pool job The pool-contract loop only asserted that `Configure persistent npm cache (self-hosted)` exists, so moving it below `Install dependencies` or dropping its `if:` stayed green while the pool install silently fell back to the default npm cache and re-downloaded through the slow egress the step exists to avoid. It also left the build job's ownership-heal body unpinned: the full body was only asserted on the Linux leg, so dropping the `sudo -n` fallbacks from the build copy stayed green while the next checkout on a pool host still holding root-owned leftovers dies with EACCES. Pin the cache step's position and `if:`, and compare every pool job's heal body against the leg copy the docker-lock ownership suite already pins in full. Three mutants (cache step moved below install, cache `if:` stripped, build heal `sudo -n` dropped) now each fail one test; unmutated 130/130 pass. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmvu6810z1y * docs(design): pair the E2E build-once note with its zh-CN version docs/design/README.md asks that updating an existing single-language design adds the missing version and aligns the pair, and this PR rewrites the section that records why the build left the hosted runner. Add e2e-build-once.zh-CN.md with the same section order and the same decisions, and link both directions immediately under each title. Also drop the "only hosted-pinned structural job" claim from the English note: `e2e-interactive-opentui` is hosted-pinned too, which the same note already records two paragraphs down. The remaining sentence keeps the actual cause - every leg waits on `build`, and `build` sat on hosted ubuntu. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmvu6810z1y --------- Co-authored-by: Qwen-Coder <[email protected]>
This commit is contained in:
1 parent
d896c1816a
commit
54fd8e0372
4 files changed
+242
-30
No files matched your search
@@ -32,31 +32,73 @@ jobs:
|
||||
# pool host, so the six Linux shards spent more of the shared pool
|
||||
# rebuilding one commit than testing it. The outputs are platform-
|
||||
# independent JavaScript (native modules come from each leg's own
|
||||
# `npm ci`), so a hosted VM builds once, off the pool, and every leg
|
||||
# downloads the result. See .github/scripts/e2e-build-pack.sh.
|
||||
# `npm ci`), so one job builds once and every leg downloads the result.
|
||||
# The build shares the legs' pool-first routing: hosted ubuntu jobs sat
|
||||
# queued for hours while the pool idled (2026-10-28), and one build per
|
||||
# push adds no measurable pool load. The OpenTUI interactive leg is still
|
||||
# hosted-pinned, so a run can still wait on hosted capacity for that leg —
|
||||
# pool routing takes the wait off the critical first step, it does not
|
||||
# remove every hosted dependency. See .github/scripts/e2e-build-pack.sh.
|
||||
build:
|
||||
name: 'Build for E2E'
|
||||
runs-on: 'ubuntu-latest'
|
||||
runs-on: '${{ (github.repository == ''QwenLM/qwen-code'' && vars.MAINTAINER_ECS_RUNNER_DISABLED != ''true'') && fromJSON(''["self-hosted", "linux", "x64", "ecs-qwen"]'') || fromJSON(''["ubuntu-latest"]'') }}'
|
||||
timeout-minutes: 30
|
||||
# Same fork gate as the legs, so a skipped build skips them all for the
|
||||
# same reason rather than failing them on a missing artifact.
|
||||
if: |-
|
||||
${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
|
||||
steps:
|
||||
- name: 'Restore workspace ownership'
|
||||
if: "${{ runner.environment == 'self-hosted' }}"
|
||||
run: |-
|
||||
set -uo pipefail
|
||||
RUNNER_UID="$(id -u)"
|
||||
RUNNER_GID="$(id -g)"
|
||||
if [ "$RUNNER_UID" != "0" ]; then
|
||||
chown -R "$RUNNER_UID:$RUNNER_GID" "$GITHUB_WORKSPACE" 2>/dev/null || sudo -n chown -R "$RUNNER_UID:$RUNNER_GID" "$GITHUB_WORKSPACE" || echo "::warning::could not restore workspace ownership; checkout may fail on leftover root-owned files"
|
||||
# The docker leg's host-side locks live in the runner's home: a
|
||||
# root-owned leftover under ~/.cache/qwen-code-ci fails every
|
||||
# `exec 9>` against them with EACCES before a test runs (#11990).
|
||||
if [ -d "${HOME}/.cache/qwen-code-ci" ]; then
|
||||
chown -R "$RUNNER_UID:$RUNNER_GID" "${HOME}/.cache/qwen-code-ci" 2>/dev/null || sudo -n chown -R "$RUNNER_UID:$RUNNER_GID" "${HOME}/.cache/qwen-code-ci" || echo "::warning::could not restore CI cache ownership; docker sandbox locks may stay unreadable"
|
||||
fi
|
||||
fi
|
||||
chmod -R u+rwX "$GITHUB_WORKSPACE" 2>/dev/null || sudo -n chmod -R u+rwX "$GITHUB_WORKSPACE" || echo "::warning::could not restore workspace write permissions; checkout may fail on leftover read-only files"
|
||||
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
|
||||
|
||||
- name: 'Set up Node.js'
|
||||
# Hosted-only, mirroring the legs: on the pool, nodejs.org may be
|
||||
# unreachable through the egress proxy, and the action's post step
|
||||
# uploads the npm cache to GitHub — a save that ran 14+ minutes over
|
||||
# the pool's slow egress on security-checks' first pool run
|
||||
# (2026-08-26). The pool reuses the machine's Node and its persistent
|
||||
# npm cache instead.
|
||||
- name: 'Set up Node.js (hosted)'
|
||||
if: "${{ runner.environment == 'github-hosted' }}"
|
||||
uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
package-manager-cache: false
|
||||
registry-url: 'https://registry.npmjs.org/'
|
||||
|
||||
- name: 'Use pre-installed Node.js (self-hosted)'
|
||||
if: "${{ runner.environment == 'self-hosted' }}"
|
||||
uses: './.github/actions/self-hosted-node'
|
||||
|
||||
- name: 'Cache pnpm store (hosted)'
|
||||
if: "${{ runner.environment == 'github-hosted' }}"
|
||||
uses: './.github/actions/pnpm-store-cache'
|
||||
|
||||
- name: 'Configure persistent npm cache (self-hosted)'
|
||||
if: "${{ runner.environment == 'self-hosted' }}"
|
||||
run: |-
|
||||
cache_dir="${HOME}/.cache/qwen-code/npm"
|
||||
mkdir -p "${cache_dir}"
|
||||
echo "NPM_CONFIG_CACHE=${cache_dir}" >> "${GITHUB_ENV}"
|
||||
echo "Using persistent npm cache at ${cache_dir}"
|
||||
du -sh "${cache_dir}" 2>/dev/null || true
|
||||
|
||||
- name: 'Configure npm for rate limiting'
|
||||
run: |-
|
||||
npm config set fetch-retry-mintimeout 20000
|
||||
@@ -64,6 +106,10 @@ jobs:
|
||||
npm config set fetch-retries 5
|
||||
npm config set fetch-timeout 300000
|
||||
|
||||
- name: 'Disk floor gate (self-hosted)'
|
||||
if: "${{ runner.environment == 'self-hosted' }}"
|
||||
run: 'bash .github/scripts/check-disk-floor.sh "${GITHUB_WORKSPACE}" "${RUNNER_TEMP:-/tmp}"'
|
||||
|
||||
# A bare `corepack pnpm install` has no retry: run 34700339334's
|
||||
# build leg died at this step before any test ran, on a tree whose
|
||||
# install reproduces clean, and every leg behind `needs: [build]`
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
# E2E: build once, unpack on every leg
|
||||
|
||||
Design note for the `build` job in `.github/workflows/e2e.yml`: one hosted
|
||||
runner builds and bundles the commit, and the eleven test legs download the
|
||||
[English](e2e-build-once.md) | [简体中文](e2e-build-once.zh-CN.md)
|
||||
|
||||
Design note for the `build` job in `.github/workflows/e2e.yml`: one job
|
||||
builds and bundles the commit, and the eleven test legs download the
|
||||
result instead of each rebuilding it.
|
||||
|
||||
## Context
|
||||
@@ -18,16 +20,16 @@ run, so this repeats many times a day.
|
||||
|
||||
## Decision
|
||||
|
||||
- A `build` job on `ubuntu-latest` installs (with `QWEN_SKIP_PREPARE=1`),
|
||||
builds, bundles, and packs the outputs with
|
||||
`.github/scripts/e2e-build-pack.sh`: the bundle (`dist/`), every workspace
|
||||
`dist/` under the roots that root `package.json`'s `workspaces` declares
|
||||
(node_modules pruned, a matched `dist/` not descended again), and an
|
||||
`e2e-build.sha` stamp of the commit. The script fails closed — with an
|
||||
`::error::` line — when the bundle is missing or when the scan finds no
|
||||
workspace `dist/` at all, the same under-pack contract as release.yml's
|
||||
Pack Build Outputs. The archive is one gzip tarball uploaded as the
|
||||
`e2e-build` artifact with one day of retention.
|
||||
- A `build` job installs (with `QWEN_SKIP_PREPARE=1`), builds, bundles, and
|
||||
packs the outputs with `.github/scripts/e2e-build-pack.sh`: the bundle
|
||||
(`dist/`), every workspace `dist/` under the roots that root
|
||||
`package.json`'s `workspaces` declares (node_modules pruned, a matched
|
||||
`dist/` not descended again), and an `e2e-build.sha` stamp of the commit.
|
||||
The script fails closed — with an `::error::` line — when the bundle is
|
||||
missing or when the scan finds no workspace `dist/` at all, the same
|
||||
under-pack contract as release.yml's Pack Build Outputs. The archive is one
|
||||
gzip tarball uploaded as the `e2e-build` artifact with one day of
|
||||
retention.
|
||||
- Every leg gains `needs: [build]`, keeps its own `npm ci` (node_modules,
|
||||
native modules, and the `generate` step that `prepare` still runs), and
|
||||
replaces its build and bundle steps with a download plus
|
||||
@@ -40,13 +42,31 @@ run, so this repeats many times a day.
|
||||
skips them for the same reason instead of failing them on a missing
|
||||
artifact.
|
||||
|
||||
## Why a hosted builder
|
||||
## Why the build left the hosted runner
|
||||
|
||||
The build is platform-independent JavaScript; the platform-specific parts are
|
||||
native modules that each leg's own `npm ci` installs. A hosted VM builds it in
|
||||
about four minutes with no pool contention, and the ECS legs then pay a
|
||||
download rather than a build. Building on the pool would put the one job the
|
||||
whole run waits on behind the same queue that made the legs slow.
|
||||
The build was hosted-pinned because it is platform-independent JavaScript —
|
||||
the platform-specific parts are native modules that each leg's own `npm ci`
|
||||
installs. A hosted VM built it in about four minutes with no pool contention,
|
||||
and the ECS legs then paid a download rather than a build. Building on the
|
||||
pool was expected to put the one job the whole run waits on behind the same
|
||||
queue that made the legs slow.
|
||||
|
||||
That premise inverted on 2026-10-28. Hosted ubuntu allocations for this
|
||||
repository degraded across one day from a 47-minute wait (run created 02:19
|
||||
UTC, build started 03:06) to 2 hours 16 minutes (build started 04:56) to 2
|
||||
hours 52 minutes (run created 05:26 UTC, build started 08:18), while the
|
||||
self-hosted pool — several hundred runners, mostly idle — went unused. Every
|
||||
leg waits on `build`, and `build` sat on hosted ubuntu, so each push to `main`
|
||||
accumulated a queued run and the backlog could not drain. The build now shares
|
||||
the legs' pool-first routing (the ecs-qwen pool, falling back to hosted ubuntu
|
||||
only when the pool is disabled) and carries the pool's self-hosted contract:
|
||||
the workspace-ownership heal before checkout, the pre-installed Node preflight,
|
||||
the persistent npm cache, and the disk-floor gate. On a probe run of the change
|
||||
(40935790797), the build scheduled onto the pool 4 seconds after creation and
|
||||
finished in 6m02s.
|
||||
|
||||
The OpenTUI interactive leg remains hosted-pinned, so a run can still end on
|
||||
a hosted wait for that leg; rerouting it is a separate decision.
|
||||
|
||||
## Measured locally
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
# E2E:一次构建,每条 leg 各自解包
|
||||
|
||||
[English](e2e-build-once.md) | [简体中文](e2e-build-once.zh-CN.md)
|
||||
|
||||
`.github/workflows/e2e.yml` 里 `build` job 的设计说明:由一个 job 构建并打包
|
||||
本次提交,十一条测试 leg 下载它的产物,而不是各自重新构建一次。
|
||||
|
||||
## 背景
|
||||
|
||||
E2E workflow 的每一条 leg —— 共享 ECS 池上的六个 Linux 分片、两个 macOS 分片、
|
||||
OpenTUI 交互式 leg 以及两条 nightly 隔离 leg —— 在跑到第一个测试之前,都要在自己的
|
||||
runner 上执行 `npm ci`、`npm run build` 和 `npm run bundle`。托管 VM 上这是 4–8 分钟;
|
||||
在繁忙的池机器上,光是 build 这一步就要 10–17 分钟(run 33718808438:先构建 10–17
|
||||
分钟,再跑 12–39 分钟测试)。于是六个池分片花在「重复构建同一个提交」上的共享机器时间
|
||||
比花在测试上还多,并且在一次 21 小时的采样里,E2E 占掉了池子 slot-minutes 的 11%。
|
||||
每次推送到 `main` 都会启动一次新的 run,所以这件事每天要重复很多次。
|
||||
|
||||
## 决策
|
||||
|
||||
- 一个 `build` job 负责安装(带 `QWEN_SKIP_PREPARE=1`)、构建、打包,并用
|
||||
`.github/scripts/e2e-build-pack.sh` 归档产物:主产物(`dist/`)、根 `package.json`
|
||||
的 `workspaces` 所声明的每个根目录下的 workspace `dist/`(剔除 node_modules,已匹配的
|
||||
`dist/` 不再向下递归),以及一个记录提交号的 `e2e-build.sha` 戳。当主产物缺失,或扫描
|
||||
下来一个 workspace `dist/` 都没有时,脚本会以 `::error::` 行 fail closed —— 与
|
||||
release.yml 的 Pack Build Outputs 用的是同一套「打包不足即失败」契约。归档是一个 gzip
|
||||
tarball,以 `e2e-build` artifact 上传,保留一天。
|
||||
- 每条 leg 都加上 `needs: [build]`,保留自己的 `npm ci`(node_modules、原生模块,以及
|
||||
`prepare` 仍会执行的 `generate` 步骤),并把自己的 build 与 bundle 步骤换成一次下载加
|
||||
`.github/scripts/e2e-build-unpack.sh`。该脚本在解包之前就会拒绝戳了不同提交号的归档,
|
||||
或其中没有 `dist/cli.js` 的归档;解包之后还会复查主产物是否就位。
|
||||
- docker sandbox 镜像不变:它在 Docker 内部、按每机锁从 checkout 构建,因此不属于归档内容。
|
||||
- build job 带着与 leg 相同的 fork 门禁,所以 build 被跳过时,leg 也因同一个原因被跳过,
|
||||
而不是因为缺 artifact 而失败。
|
||||
|
||||
## 为什么 build 离开了托管 runner
|
||||
|
||||
build 原先被钉在托管 runner 上,因为它是平台无关的 JavaScript —— 平台相关的部分是原生
|
||||
模块,由各条 leg 自己的 `npm ci` 安装。托管 VM 构建它约四分钟,且不与池子争抢资源,ECS
|
||||
leg 随后只需付出一次下载的成本。当时的预期是:把构建放到池上,会让整个 run 都要等的那个
|
||||
job 排到同一条让 leg 变慢的队列里。
|
||||
|
||||
这个前提在 2026-10-28 反转了。本仓库的托管 ubuntu 分配在一天之内持续恶化:从等待 47 分钟
|
||||
(run 创建于 02:19 UTC,build 03:06 启动),到 2 小时 16 分钟(build 04:56 启动),再到
|
||||
2 小时 52 分钟(run 创建于 05:26 UTC,build 08:18 启动);与此同时,自建池 —— 几百台
|
||||
runner,大多空闲 —— 一直没被用上。每条 leg 都要等 `build`,而 `build` 待在托管 ubuntu 上,
|
||||
于是每次推送 `main` 都会积压一个排队中的 run,堆积无法消化。现在 build 与各条 leg 共用
|
||||
pool-first 路由(优先 ecs-qwen 池,仅在池被禁用时回落到托管 ubuntu),并且带上池的
|
||||
self-hosted 契约:checkout 之前的 workspace 属主修复、预装 Node 的 preflight、常驻 npm
|
||||
缓存,以及磁盘水位门禁。在该改动的一次探测 run(40935790797)里,build 在创建后 4 秒就
|
||||
被调度到池上,6 分 02 秒完成。
|
||||
|
||||
OpenTUI 交互式 leg 仍然钉在托管 runner 上,所以一次 run 仍可能因为等它而结束在托管排队上;
|
||||
重新路由它是另一个独立决策。
|
||||
|
||||
## 本机实测
|
||||
|
||||
- 构建 `a8248eaed3` 得到的归档 gzip 压缩后 85 MB、14,408 个文件;打包耗时 16 秒。
|
||||
- 同一提交上的一个干净 worktree,用 `QWEN_SKIP_PREPARE=1` 安装(各处都没有 `dist/`),
|
||||
可以解开该归档并从还原出来的产物跑通 no-AK 集成门禁。戳与 `GITHUB_SHA` 不一致的归档,
|
||||
解包步骤会直接拒绝,且不解出任何内容。
|
||||
- `.github/scripts/e2e-build.test.mjs` 覆盖打包与解包契约(workspace dist/ 选取、
|
||||
node_modules 剔除、权限位保留、戳校验),并在 CI 的 helper-test 步骤中运行。
|
||||
|
||||
## 已考虑的替代方案
|
||||
|
||||
- **直接用 `upload-artifact` 上传目录。** artifact 存储不保留文件权限位,而且每次 run 要
|
||||
上传 1.4 万个对象;tarball 是一个对象,并且保留权限位。
|
||||
- **在池上做同一主机共享的构建缓存。** 早前已被否决(#10129),理由是它会在同一台机器上的并发
|
||||
job 之间引入共享可变状态。
|
||||
- **只接六个 Linux 分片。** macOS、OpenTUI 和 nightly leg 构建的是同一份 JavaScript,因此
|
||||
它们消费同一个归档;把它们排除在外只会让每次 run 多跑三次构建,信号上没有任何收益。
|
||||
|
||||
## 后续工作
|
||||
|
||||
- 如果池上的下载耗时确实成为问题,把归档裁剪到测试真正会解析的那些 workspace `dist/` 目录树。
|
||||
- 等队列能力开启后,取消 `main` 上被取代的 run,让一批集中合并只构建并测试最新的那个提交。
|
||||
@@ -1260,18 +1260,84 @@ describe('e2e workflow', () => {
|
||||
expect(gateIndex).toBeLessThan(names.indexOf('Install dependencies'));
|
||||
});
|
||||
|
||||
it('keeps the gate on every pool-routed job', () => {
|
||||
it('keeps the self-hosted contract on every pool-routed job', () => {
|
||||
// A new pool-routed leg without the gate reddens here instead of on a
|
||||
// saturated host; the hosted legs stay ungated on fresh VMs.
|
||||
// saturated host; the hosted legs stay ungated on fresh VMs. The same
|
||||
// holds for the Node preflight (nodejs.org may be unreachable through
|
||||
// the pool's egress proxy) and the ownership heal (docker shards leave
|
||||
// root-owned files on the runners).
|
||||
const poolJobs = Object.entries(yml.jobs).filter(([, job]) =>
|
||||
String(job['runs-on'] ?? '').includes('ecs-qwen'),
|
||||
);
|
||||
expect(poolJobs.map(([id]) => id)).toEqual(['e2e-test-linux']);
|
||||
expect(poolJobs.map(([id]) => id)).toEqual(['build', 'e2e-test-linux']);
|
||||
// Keep the build aligned with the leg, and pin the workspace fallback
|
||||
// chains below so editing both copies cannot silently remove them.
|
||||
const pinnedHeal = yml.jobs['e2e-test-linux'].steps.find(
|
||||
(step) => step.name === 'Restore workspace ownership',
|
||||
);
|
||||
for (const [id, job] of poolJobs) {
|
||||
const gate = (job.steps ?? []).find(
|
||||
const steps = job.steps ?? [];
|
||||
const names = steps.map((step) => step.name);
|
||||
const checkoutIndex = steps.findIndex((step) =>
|
||||
String(step.uses ?? '').includes('actions/checkout'),
|
||||
);
|
||||
const installIndex = names.indexOf('Install dependencies');
|
||||
// Presence alone is not the contract: the heal must precede
|
||||
// checkout, the gate must sit between checkout and install, and the
|
||||
// preflight must be the pool's own action — a reordered or
|
||||
// re-pointed step fails here instead of on a runner.
|
||||
const heal = steps.find(
|
||||
(step) => step.name === 'Restore workspace ownership',
|
||||
);
|
||||
expect(heal?.if, id).toBe("${{ runner.environment == 'self-hosted' }}");
|
||||
expect(names.indexOf('Restore workspace ownership'), id).toBeLessThan(
|
||||
checkoutIndex,
|
||||
);
|
||||
const gate = steps.find(
|
||||
(step) => step.name === 'Disk floor gate (self-hosted)',
|
||||
);
|
||||
expect(gate, id).toBeDefined();
|
||||
const gateIndex = names.indexOf('Disk floor gate (self-hosted)');
|
||||
expect(gateIndex, id).toBeGreaterThan(checkoutIndex);
|
||||
expect(gateIndex, id).toBeLessThan(installIndex);
|
||||
const poolNode = steps.find(
|
||||
(step) => step.name === 'Use pre-installed Node.js (self-hosted)',
|
||||
);
|
||||
expect(poolNode?.uses, id).toBe('./.github/actions/self-hosted-node');
|
||||
const hostedNode = steps.find(
|
||||
(step) =>
|
||||
typeof step.name === 'string' &&
|
||||
step.name.startsWith('Set up Node.js'),
|
||||
);
|
||||
expect(hostedNode?.if, id).toBe(
|
||||
"${{ runner.environment == 'github-hosted' }}",
|
||||
);
|
||||
const npmCacheIndex = names.indexOf(
|
||||
'Configure persistent npm cache (self-hosted)',
|
||||
);
|
||||
// Presence alone is not the contract here either: below the install it
|
||||
// exports NPM_CONFIG_CACHE after npm has already picked a cache, so
|
||||
// the pool re-downloads through the slow egress this step avoids, and
|
||||
// without the `if:` the hosted lane points npm at a directory no cache
|
||||
// step ever restores.
|
||||
expect(npmCacheIndex, id).toBeGreaterThanOrEqual(0);
|
||||
expect(npmCacheIndex, id).toBeLessThan(installIndex);
|
||||
expect(steps[npmCacheIndex]?.if, id).toBe(
|
||||
"${{ runner.environment == 'self-hosted' }}",
|
||||
);
|
||||
expect(steps[npmCacheIndex]?.run, id).toContain(
|
||||
'cache_dir="${HOME}/.cache/qwen-code/npm"',
|
||||
);
|
||||
expect(steps[npmCacheIndex]?.run, id).toContain(
|
||||
'echo "NPM_CONFIG_CACHE=${cache_dir}" >> "${GITHUB_ENV}"',
|
||||
);
|
||||
expect(heal?.run, id).toBe(pinnedHeal?.run);
|
||||
expect(heal?.run, id).toContain(
|
||||
'chown -R "$RUNNER_UID:$RUNNER_GID" "$GITHUB_WORKSPACE" 2>/dev/null || sudo -n chown -R "$RUNNER_UID:$RUNNER_GID" "$GITHUB_WORKSPACE"',
|
||||
);
|
||||
expect(heal?.run, id).toContain(
|
||||
'chmod -R u+rwX "$GITHUB_WORKSPACE" 2>/dev/null || sudo -n chmod -R u+rwX "$GITHUB_WORKSPACE"',
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1279,8 +1345,8 @@ describe('e2e workflow', () => {
|
||||
describe('one build for every leg', () => {
|
||||
// Each leg used to build and bundle on its own runner — 4–8 minutes on a
|
||||
// hosted VM, 10–17 on a busy pool host, eleven times per run. The `build`
|
||||
// job does it once on a hosted VM and the legs unpack its archive; these
|
||||
// pins keep a leg from quietly growing its own build back.
|
||||
// job does it once and the legs unpack its archive; these pins keep a leg
|
||||
// from quietly growing its own build back.
|
||||
const build = yml.jobs.build;
|
||||
const legs = [
|
||||
'e2e-test-linux',
|
||||
@@ -1289,9 +1355,13 @@ describe('e2e workflow', () => {
|
||||
'isolated-nightly',
|
||||
];
|
||||
|
||||
it('builds once, on a hosted runner, off the shared pool', () => {
|
||||
it("builds once, sharing the legs' pool-first runner routing", () => {
|
||||
expect(build.needs).toBeUndefined();
|
||||
expect(build['runs-on']).toBe('ubuntu-latest');
|
||||
// Hosted ubuntu jobs queued for hours while the pool idled
|
||||
// (2026-10-28), so the build rides the exact routing expression every
|
||||
// other pool job uses instead of pinning a hosted-only label.
|
||||
expect(build['runs-on']).toBe(yml.jobs['e2e-test-linux']['runs-on']);
|
||||
expect(String(build['runs-on'])).toContain('ecs-qwen');
|
||||
const names = build.steps.map((step) => step.name);
|
||||
expect(names).toContain('Build project');
|
||||
expect(names).toContain('Bundle CLI for E2E tests');
|
||||
|
||||
Reference in new issue
Block a user