Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
37dead9
ci: gate heavy jobs on a disk floor and persist pressure samples
yiliang114 Aug 28, 2026
7da0eb7
ci: single-quote the failure() condition to satisfy yamllint quoted-s…
yiliang114 Aug 28, 2026
d8ff4f2
ci: keep TMPDIR routing block byte-identical across test legs
yiliang114 Aug 28, 2026
1186e50
fix(ci): validate disk floor overrides
yiliang114 Aug 29, 2026
c17ce29
fix(ci): reject oversized disk floor overrides
yiliang114 Aug 29, 2026
44344d4
test(ci): pin the disk-floor overflow guard boundaries
yiliang114 Aug 29, 2026
6eae982
Merge origin/main into fix/ci-disk-floor-gate-10035
yiliang114 Aug 29, 2026
0c7fe0a
refactor(ci): simplify disk floor validation and sampling
yiliang114 Aug 29, 2026
5c8456d
fix(ci): preserve the TMPDIR sampler sentinel
yiliang114 Aug 29, 2026
fa9bfb0
fix(ci): retain disk samples from dependency install
yiliang114 Aug 29, 2026
26654c5
refactor(ci): reuse the workflow parser in disk tests
yiliang114 Aug 29, 2026
9ac726f
Merge origin/main into fix/ci-preinstall-disk-sampling-10035
yiliang114 Aug 30, 2026
1a4fd1b
chore(ci): trigger checks after retargeting to main
yiliang114 Aug 30, 2026
8a0f5dd
test(ci): pin disk-pressure workflow contract
yiliang114 Aug 30, 2026
8a386ab
Merge branch 'main' into fix/ci-preinstall-disk-sampling-10035
yiliang114 Aug 30, 2026
cbf28ac
fix(ci): harden install disk sampling
yiliang114 Aug 30, 2026
3aac615
Merge branch 'main' into fix/ci-preinstall-disk-sampling-10035
yiliang114 Aug 30, 2026
c0da45d
ci: record ci.yml growth in the size baseline
yiliang114 Aug 30, 2026
9964047
Merge branch 'main' into fix/ci-preinstall-disk-sampling-10035
yiliang114 Aug 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions .github/scripts/ci-disk-pressure.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import {
chmodSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} 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 workflowPath = join(
dirname(fileURLToPath(import.meta.url)),
'..',
'workflows',
'ci.yml',
);
const testSteps = parse(readFileSync(workflowPath, 'utf8')).jobs.test.steps;

function step(name) {
const value = testSteps.find((candidate) => candidate.name === name);
assert.ok(value, `missing ${name} step`);
return value;
}

describe('ci.yml disk-pressure evidence', () => {
it('starts sampling before npm ci and preserves those samples for upload', () => {
const install = step('Install dependencies').run;
const npmCi = install.indexOf('npm ci');

assert.match(
install,
/DISK_SAMPLES="\$\{RUNNER_TEMP\}\/disk-pressure-samples\.log"/,
);
assert.ok(npmCi > install.indexOf('DFSAMPLE '));
assert.match(install, /\( while sleep 10; do sample_disk; done \) &/);
assert.ok(npmCi > install.indexOf('( while sleep 10'));
assert.match(install, /trap .*SAMPLER_PID.* EXIT/);

const tests = step('Run tests and generate reports').run;
assert.match(
tests,
/DISK_SAMPLES="\$\{RUNNER_TEMP\}\/disk-pressure-samples\.log"\nif \[ ! -s "\$DISK_SAMPLES" \]; then\n {2}echo "DISKCONTEXT .*" > "\$DISK_SAMPLES" 2>\/dev\/null \|\| true\nfi/,
);
assert.ok(tests.indexOf('export TMPDIR=') > tests.indexOf('DISK_SAMPLES='));

const sampleFormat = (script) => {
const match = script.match(
/sample="DFSAMPLE .*\/proc\/meminfo 2>\/dev\/null(?: \|\| true)?\)\]"/,
);
assert.ok(match);
return match[0]
.replaceAll('${RUNNER_TEMP:-/tmp}', '${TMPDIR}')
.replace(
' /proc/meminfo 2>/dev/null || true)]',
' /proc/meminfo 2>/dev/null)]',
);
Comment on lines +56 to +61

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R2-1: The sampleFormat normalizer carries a branch that can never fire: the .replace(...) call below targets the spelling /proc/meminfo 2>/dev/null || true)] (guard inside the command substitution), but neither sampler copy at HEAD has it — the install copy carries its || true outside the assignment's closing quote (ci.yml:485) and the test-step copy has no guard there at all (ci.yml:685). The branch is not merely dead: if a future edit adds || true inside one copy's awk substitution, this normalizer silently absorbs the divergence and the parity assertion stays green — the two samplers drift, and a mixed-format timeline reaches the humans correlating ENOSPC failures, the artifact's only consumer. Drop the (?: \|\| true)? optional group from the regex on line 53 as well; the regex already ends its match at )]", which excludes both copies' trailing guards by construction.

Witness:

intact test + diverged ci.yml (" || true" added inside the install awk substitution):
# pass 2 / # fail 0          <- divergence absorbed
fixed test (.replace call and optional group removed) + diverged ci.yml:
not ok 1 ... AssertionError  <- divergence flagged (# fail 1)
fixed test on HEAD ci.yml:
# pass 2                     <- fix safe at HEAD
Suggested change
return match[0]
.replaceAll('${RUNNER_TEMP:-/tmp}', '${TMPDIR}')
.replace(
' /proc/meminfo 2>/dev/null || true)]',
' /proc/meminfo 2>/dev/null)]',
);
return match[0].replaceAll('${RUNNER_TEMP:-/tmp}', '${TMPDIR}');
中文说明

sampleFormat 归一化器里有一个永远不会触发的分支:下面的 .replace(...) 针对的是 /proc/meminfo 2>/dev/null || true)](guard 位于命令替换内部)这种写法,但 HEAD 上的两份采样器副本都不是这种写法——install 副本的 || true 位于赋值引号之外(ci.yml:485),test 步骤副本在该位置完全没有 guard(ci.yml:685)。这个分支不只是死代码:如果未来某次编辑把 || true 加进某个副本的 awk 替换内部,该归一化会悄悄吸收掉这次格式分歧,奇偶断言仍然为绿——两个采样器从此漂移,混杂格式的 timeline 会送到关联 ENOSPC 故障的人工排查者手中,而这个 artifact 只有这一个消费者。请同时去掉第 53 行正则中的 (?: \|\| true)? 可选组;正则本身以 )]" 结尾,构造上已经排除了两份副本尾部的 guard。

— qwen3.8-max via Qwen Code /review (v0.22.3)

};
const headerLine = (script) =>
script
.split('\n')
.find((line) => line.trimStart().startsWith('echo "DISKCONTEXT '))
?.trim();
assert.equal(headerLine(install), headerLine(tests));
assert.equal(sampleFormat(install), sampleFormat(tests));
Comment on lines +68 to +69

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R2-2: The contract suite pins the sampler wiring selectively, and this round demonstrated three behaviours that can silently regress with the whole suite green. (1) The EXIT trap is pinned only by presence, not by ordering against npm ci — the trap can move below npm ci while every assertion stays green, and then bash -e aborts at a failing npm ci before the trap ever registers, so the orphaned sampler holds the step's stdout and the runner waits for output EOF until the job timeout (60 hosted / 90 ECS) instead of failing red promptly. (2) sampleFormat's replaceAll('${RUNNER_TEMP:-/tmp}', '${TMPDIR}') is symmetric while the intended asymmetry is directional — regressing either copy's tmpdir expression is absorbed, so the test step can end up sampling a filesystem other than the routed one, or the install step can sample empty space/inode fields when TMPDIR is unset. (3) The test-step sampler's append to $DISK_SAMPLES (ci.yml:685) is pinned by nothing — deleting that segment leaves the suite green, and a job that fails mid-suite then uploads an artifact containing only install-phase samples, silently losing the ENOSPC-during-test spike the artifact exists to capture. The block below adds the three demonstrated pins; beyond them, please stop extending the suite pin by pin — pin each sampler block as one contiguous literal (the way the guard block is already pinned) or extend the behavioural execution the second test uses to the test-step sampler, so any future edit to the sampler wiring turns the suite red instead of one unpinned behaviour surfacing per review round.

Witness:

trap moved below npm ci: test 1 still "ok 1"; EOF model intact stdoutEofAtMs=120 vs mutant null (15s window), DFSAMPLE written 10090ms after the step's bash exited
tmpdir mutants (either copy regressed): # pass 2 / # fail 0 (absorbed); direction pins vs each mutant: not ok 1 AssertionError
append segment deleted at ci.yml:685: # pass 2 / # fail 0; with the added assertion: not ok 1 (# fail 1); intact + assertion: # pass 2
Suggested change
assert.equal(headerLine(install), headerLine(tests));
assert.equal(sampleFormat(install), sampleFormat(tests));
assert.equal(headerLine(install), headerLine(tests));
assert.equal(sampleFormat(install), sampleFormat(tests));
assert.ok(install.indexOf('trap ') > install.indexOf('SAMPLER_PID=$!'));
assert.ok(npmCi > install.indexOf('trap '));
assert.match(install, /tmpdir\[\$\{RUNNER_TEMP:-\/tmp\}\]/);
assert.match(tests, /tmpdir\[\$\{TMPDIR\}\]/);
assert.match(
tests,
/echo "\$sample" >> "\$DISK_SAMPLES" 2>\/dev\/null \|\| true/,
);

The trap pin must keep the trap after SAMPLER_PID=$! (ci.yml:491-492), the tmpdir pins must keep the deliberate asymmetry — the install step samples ${RUNNER_TEMP:-/tmp} before any routing exists and the test step samples the routed ${TMPDIR} (ci.yml:669-670, routing block ci.yml:676-684) — and the append pin must keep the 2>/dev/null || true guard (ci.yml:685, install copy at ci.yml:487), because under the workflow's bash -e an unguarded failing append would kill the sampler loop. Each added assertion must go red against its demonstrated mutant: remove any one of the three pins, re-apply that mutant to ci.yml, and confirm the suite fails — every pin flipped red on its mutant and stayed green on the intact tree during this review.

中文说明

合约测试套件对采样器接线只做选择性固定(pin),本轮演示了三种可以在整套测试保持绿色的情况下悄悄回归的行为。(1) EXIT trap 只被固定了"存在",没有固定相对 npm ci 的顺序——trap 可以移到 npm ci 之下而所有断言仍为绿;随后 npm ci 失败时 bash -e 会在 trap 注册之前中止,孤儿采样器继续持有该步骤的 stdout,runner 会一直等待输出 EOF 直到 job 超时(托管 60 分钟 / ECS 90 分钟),而不是立刻红掉。(2) sampleFormatreplaceAll('${RUNNER_TEMP:-/tmp}', '${TMPDIR}') 是对称的,但预期的不对称是有方向的——任一副本的 tmpdir 表达式回归都会被吸收,test 步骤可能采样到非路由文件系统的磁盘压力,或 install 步骤在 TMPDIR 未设置时采样到空的 space/inode 字段。(3) test 步骤采样器向 $DISK_SAMPLES 的追加(ci.yml:685)没有任何断言固定——删除该段后整套测试仍为绿,job 在测试中途失败时上传的 artifact 将只包含安装阶段的样本,悄悄丢失这个 artifact 本要捕获的测试期间 ENOSPC 峰值。下方代码块补上三个已演示的固定;在此之外,请停止逐个补 pin——把每个采样器块作为一整段连续字面量固定(就像守卫块已有的固定方式),或把第二个测试使用的行为执行扩展到 test 步骤的采样器,让未来任何对采样器接线的修改都能把测试变红,而不是每轮评审都浮现一个未固定行为。

约束:trap 的固定必须保持 trap 位于 SAMPLER_PID=$! 之后(ci.yml:491-492);tmpdir 固定必须保留刻意的不对称——install 步骤在路由存在之前采样 ${RUNNER_TEMP:-/tmp},test 步骤采样路由后的 ${TMPDIR}(ci.yml:669-670,路由块 ci.yml:676-684);追加固定必须保留 2>/dev/null || true 守卫(ci.yml:685,install 副本见 ci.yml:487),因为在 workflow 的 bash -e 下,未守卫的失败追加会杀死采样循环。修复见证:每个新增断言都必须在其演示的突变体下变红——删除三个固定中的任意一个,把对应的突变体重新应用到 ci.yml,确认套件失败;本轮评审中每个固定都已在突变体下变红、在完好代码下保持绿色。

— qwen3.8-max via Qwen Code /review (v0.22.3)


const upload = step('Upload disk-pressure samples');
assert.equal(upload.if, '${{ failure() }}');
assert.equal(upload.with['if-no-files-found'], 'ignore');
Comment thread
yiliang114 marked this conversation as resolved.
assert.equal(
upload.with.path,
'${{ runner.temp }}/disk-pressure-samples.log',
);
});

it('keeps install failure status while writing the pre-install sample', () => {
const root = mkdtempSync(join(tmpdir(), 'ci-disk-pressure-'));
const npm = join(root, 'npm');
writeFileSync(npm, '#!/usr/bin/env bash\nexit 42\n');
chmodSync(npm, 0o755);

try {
const result = spawnSync(
'bash',
['-e', '-o', 'pipefail', '-c', step('Install dependencies').run],
Comment thread
yiliang114 marked this conversation as resolved.
{
encoding: 'utf8',
timeout: 30_000,
env: {
...process.env,
PATH: `${root}:${process.env.PATH}`,
RUNNER_TEMP: root,
},
},
);

assert.equal(result.error, undefined);
assert.equal(
result.status,
42,
`signal: ${result.signal}\nerror: ${result.error}\nstdout: ${result.stdout}\nstderr: ${result.stderr}`,
);
const samples = readFileSync(
join(root, 'disk-pressure-samples.log'),
'utf8',
);
assert.match(samples, /^DISKCONTEXT /m);
assert.match(samples, /^DFSAMPLE /m);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
});
2 changes: 1 addition & 1 deletion .github/workflows/.size-baseline
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
9256 build-and-publish-image.yml
49610 cd-cua-driver.yml
2076 cd-mobile-mcp.yml
98043 ci.yml
102891 ci.yml
1482 codeql.yml
9389 comment-attachment-guard.yml
31677 desktop-release.yml
Expand Down
17 changes: 15 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ env:
# BOTH the github_ci_only helper step and the full-profile Test step, so a
# new helper test can't be added to one path and silently dropped from the
# other.
HELPER_TESTS: '.github/scripts/pr-safety-precheck.test.mjs .github/scripts/cap-release-notes.test.mjs .github/scripts/ci/classify-profile.test.mjs .github/scripts/ci/classify-platform-sensitivity.test.mjs .github/scripts/ci/classify-pr-profile.test.mjs .github/scripts/upsert-bot-comment.test.mjs .github/scripts/ci/main-failure-signature.test.mjs .github/scripts/classify-release-notes.test.mjs .github/scripts/dsw-swe-verified/make-manifest.test.mjs .github/scripts/dsw-swe-verified/make-terminal-bench-manifest.test.mjs .github/scripts/resolve-sandbox-image.test.mjs .github/scripts/web-shell-visuals-publish.test.mjs .github/scripts/web-shell-visuals-compose.test.mjs .github/scripts/serve-ab-diff.test.mjs .github/scripts/serve-ab-drive.test.mjs .github/scripts/qwen-triage-workflow.test.mjs .github/scripts/assign-issue-owner.test.mjs .github/scripts/auto-minimize-spam.test.mjs .github/scripts/ci-runner-routing.test.mjs .github/scripts/autofix-status-heartbeat.test.mjs .github/scripts/assign-pr-owner.test.mjs .github/scripts/check-disk-floor.test.mjs'
HELPER_TESTS: '.github/scripts/pr-safety-precheck.test.mjs .github/scripts/cap-release-notes.test.mjs .github/scripts/ci/classify-profile.test.mjs .github/scripts/ci/classify-platform-sensitivity.test.mjs .github/scripts/ci/classify-pr-profile.test.mjs .github/scripts/upsert-bot-comment.test.mjs .github/scripts/ci/main-failure-signature.test.mjs .github/scripts/classify-release-notes.test.mjs .github/scripts/dsw-swe-verified/make-manifest.test.mjs .github/scripts/dsw-swe-verified/make-terminal-bench-manifest.test.mjs .github/scripts/resolve-sandbox-image.test.mjs .github/scripts/web-shell-visuals-publish.test.mjs .github/scripts/web-shell-visuals-compose.test.mjs .github/scripts/serve-ab-diff.test.mjs .github/scripts/serve-ab-drive.test.mjs .github/scripts/qwen-triage-workflow.test.mjs .github/scripts/assign-issue-owner.test.mjs .github/scripts/auto-minimize-spam.test.mjs .github/scripts/ci-runner-routing.test.mjs .github/scripts/autofix-status-heartbeat.test.mjs .github/scripts/assign-pr-owner.test.mjs .github/scripts/check-disk-floor.test.mjs .github/scripts/ci-disk-pressure.test.mjs'
Comment thread
yiliang114 marked this conversation as resolved.
# The growth ratchet and its vitest mirror compare each workflow against
# the PR's base commit to tell "this PR grew the file" apart from "the
# baseline went stale on main" (#9904). Wired once here so every lane
Expand Down Expand Up @@ -479,6 +479,17 @@ jobs:
- name: 'Install dependencies'
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"
run: |-
DISK_SAMPLES="${RUNNER_TEMP}/disk-pressure-samples.log"
echo "DISKCONTEXT $(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null) runner[${RUNNER_NAME:-unknown}] run[${GITHUB_RUN_ID:-local}/${GITHUB_RUN_ATTEMPT:-1}] job[${GITHUB_JOB:-test}]" > "$DISK_SAMPLES" 2>/dev/null || true
Comment thread
yiliang114 marked this conversation as resolved.
sample_disk() {
sample="DFSAMPLE $(date -u +%H:%M:%S 2>/dev/null) tmpdir[${RUNNER_TEMP:-/tmp}] space[$(df -h "${RUNNER_TEMP:-/tmp}" 2>/dev/null | tail -1)] inodes[$(df -i "${RUNNER_TEMP:-/tmp}" 2>/dev/null | tail -1)] memavail[$(awk '/MemAvailable/ {print $2, $3}' /proc/meminfo 2>/dev/null)]" || true
echo "$sample"
echo "$sample" >> "$DISK_SAMPLES" 2>/dev/null || true
}
sample_disk
( while sleep 10; do sample_disk; done ) &
Comment thread
yiliang114 marked this conversation as resolved.
SAMPLER_PID=$!
trap 'pkill -TERM -P "$SAMPLER_PID" 2>/dev/null || true; kill "$SAMPLER_PID" 2>/dev/null || true' EXIT
npm ci --prefer-offline --no-audit --progress=false

- name: 'Report npm cache usage (self-hosted)'
Expand Down Expand Up @@ -659,7 +670,9 @@ jobs:
# the test/test_macos/test_windows legs; each DFSAMPLE line already
# carries the routed tmpdir, so the header only needs job/runner ids.
DISK_SAMPLES="${RUNNER_TEMP}/disk-pressure-samples.log"
echo "DISKCONTEXT $(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null) runner[${RUNNER_NAME:-unknown}] run[${GITHUB_RUN_ID:-local}/${GITHUB_RUN_ATTEMPT:-1}] job[${GITHUB_JOB:-test}]" > "$DISK_SAMPLES" 2>/dev/null || true
if [ ! -s "$DISK_SAMPLES" ]; then
echo "DISKCONTEXT $(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null) runner[${RUNNER_NAME:-unknown}] run[${GITHUB_RUN_ID:-local}/${GITHUB_RUN_ATTEMPT:-1}] job[${GITHUB_JOB:-test}]" > "$DISK_SAMPLES" 2>/dev/null || true
fi
Comment thread
yiliang114 marked this conversation as resolved.
export TMPDIR="${RUNNER_TEMP:-${TMPDIR:-/tmp}}"
if [ "${RUNNER_OS:-}" = "Linux" ]; then
QWEN_CI_TMPDIR="$(mktemp -d /var/tmp/qwen-ci-XXXXXX 2>/dev/null || true)"
Expand Down
6 changes: 6 additions & 0 deletions scripts/tests/ci-platform-lanes.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,12 @@ describe('platform lanes — the retired sensitivity classifier', () => {
});

describe('GitHub helper tests', () => {
it('includes the disk-pressure contract suite', () => {
expect(ci.env.HELPER_TESTS).toContain(
'.github/scripts/ci-disk-pressure.test.mjs',
);
});

it('runs every invocation serially', () => {
const helperSteps = Object.values(ci.jobs)
.flatMap((job) => job.steps ?? [])
Expand Down
Loading