mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
ci: report the test code each pull request adds (#13149)
Nothing counted the test code a PR adds: the size rules count production lines only. Lint & Static now writes each PR's test and production lines for core, cli and web-shell to the job summary, measured from the true merge base, and warns at 2,000 or more net test lines. It reports only; nothing blocks on it.
This commit is contained in:
1 parent
728c13de21
commit
a7deb01bcb
3 files changed
+510
-2
No files matched your search
@@ -0,0 +1,281 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Report how much test code a pull request adds, and flag large additions.
|
||||
*
|
||||
* Nothing else counts test lines: the size rules in AGENTS.md and the triage
|
||||
* skill count production lines only. In September 2026, PRs added 1.84 test
|
||||
* lines per production line in core, cli and web-shell, and the 8% of PRs
|
||||
* adding 2,000 or more net test lines carried half of that growth (#13007
|
||||
* removed 178,000 Core test lines, a few weeks of it). This step writes the
|
||||
* numbers to the job summary on every PR and, at FLAG_THRESHOLD net test
|
||||
* lines, emits a warning annotation so authors and reviewers see them. It
|
||||
* reports only: nothing blocks on it.
|
||||
*
|
||||
* The diff is taken from the true merge base, which the compare API reports:
|
||||
* the PR's base.sha is the base tip, and diffing against it would count the
|
||||
* base's newer changes, reversed, on any branch that is behind. The lane's
|
||||
* checkout is shallow, so the merge base is fetched at depth 1.
|
||||
*
|
||||
* Advisory only: every error is a warning and the step exits 0.
|
||||
*/
|
||||
import { execFile } from 'node:child_process';
|
||||
import { appendFileSync } from 'node:fs';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
export const FLAG_THRESHOLD = 2000;
|
||||
|
||||
export const PACKAGES = ['core', 'cli', 'web-shell'];
|
||||
|
||||
// Test code: test and spec files, test helpers, fixtures, mocks, snapshots.
|
||||
const TEST_PATTERNS = [
|
||||
/\.(test|spec)\.[^/]+$/,
|
||||
/\.test-helper\.[^/]+$/,
|
||||
/\.fixtures\.json$/,
|
||||
/\.snap$/,
|
||||
/(^|\/)(test-setup|testUtils)\.ts$/,
|
||||
/(^|\/)test-utils\.[^/]+$/,
|
||||
/(^|\/)(__tests__|__mocks__|__fixtures__|__snapshots__|__e2e__|fixtures|test-utils|testUtils)\//,
|
||||
/^packages\/web-shell\/client\/(e2e|test)\//,
|
||||
];
|
||||
const GENERATED = /(\.schema\.(ts|json)|\.generated\.ts)$|(^|\/)generated\//;
|
||||
const SOURCE = /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/;
|
||||
|
||||
/** The package a path belongs to, or null outside the three packages. */
|
||||
export function packageOf(path) {
|
||||
const match = /^packages\/([^/]+)\//.exec(path);
|
||||
return match && PACKAGES.includes(match[1]) ? match[1] : null;
|
||||
}
|
||||
|
||||
/** 'test', 'production' or 'other' (data, docs, styles, generated code). */
|
||||
export function classify(path) {
|
||||
if (TEST_PATTERNS.some((pattern) => pattern.test(path))) return 'test';
|
||||
if (GENERATED.test(path) || !SOURCE.test(path)) return 'other';
|
||||
return 'production';
|
||||
}
|
||||
|
||||
/** Parses `git diff --numstat`, skipping binary files ("-\t-\tpath"). */
|
||||
export function parseNumstat(text) {
|
||||
const entries = [];
|
||||
for (const line of text.split('\n')) {
|
||||
const match = /^(\d+)\t(\d+)\t(.+)$/.exec(line);
|
||||
if (match) {
|
||||
entries.push({ added: +match[1], deleted: +match[2], path: match[3] });
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
/** Added and deleted test and production lines per package, plus a total. */
|
||||
export function tally(entries) {
|
||||
const zero = () => ({
|
||||
test: { added: 0, deleted: 0 },
|
||||
production: { added: 0, deleted: 0 },
|
||||
});
|
||||
const byPackage = Object.fromEntries(PACKAGES.map((name) => [name, zero()]));
|
||||
const total = zero();
|
||||
for (const { added, deleted, path } of entries) {
|
||||
const name = packageOf(path);
|
||||
const kind = classify(path);
|
||||
if (!name || kind === 'other') continue;
|
||||
for (const bucket of [byPackage[name][kind], total[kind]]) {
|
||||
bucket.added += added;
|
||||
bucket.deleted += deleted;
|
||||
}
|
||||
}
|
||||
return { byPackage, total };
|
||||
}
|
||||
|
||||
const net = ({ added, deleted }) => added - deleted;
|
||||
const fmt = (n) => Math.abs(n).toLocaleString('en-US');
|
||||
const signed = (n) => (n > 0 ? '+' : n < 0 ? '−' : '') + fmt(n);
|
||||
const cell = (counts) =>
|
||||
`+${fmt(counts.added)} / −${fmt(counts.deleted)} (net ${signed(net(counts))})`;
|
||||
|
||||
export function isFlagged({ total }) {
|
||||
return net(total.test) >= FLAG_THRESHOLD;
|
||||
}
|
||||
|
||||
export function renderSummary(tallied) {
|
||||
const rows = PACKAGES.map(
|
||||
(name) =>
|
||||
`| ${name} | ${cell(tallied.byPackage[name].test)} | ` +
|
||||
`${cell(tallied.byPackage[name].production)} |`,
|
||||
);
|
||||
const lines = [
|
||||
'### Test growth',
|
||||
'',
|
||||
'| Package | Test lines | Production lines |',
|
||||
'| --- | ---: | ---: |',
|
||||
...rows,
|
||||
`| **Total** | ${cell(tallied.total.test)} | ${cell(tallied.total.production)} |`,
|
||||
'',
|
||||
];
|
||||
if (isFlagged(tallied)) {
|
||||
lines.push(
|
||||
`This PR adds ${fmt(net(tallied.total.test))} net test lines, at or ` +
|
||||
`above the ${fmt(FLAG_THRESHOLD)}-line flag. Consider whether shared ` +
|
||||
'fixtures, parameterized tables or whole-result expectations would ' +
|
||||
'cover the same behavior in less code.',
|
||||
'',
|
||||
);
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export function renderAnnotation(tallied) {
|
||||
if (!isFlagged(tallied)) return null;
|
||||
return (
|
||||
`::warning title=Test growth::This PR adds ` +
|
||||
`${fmt(net(tallied.total.test))} net test lines in core, cli and ` +
|
||||
`web-shell (flag at ${fmt(FLAG_THRESHOLD)}). ` +
|
||||
'Consider shared fixtures or parameterized tables; see the job summary.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* curl, not fetch: it honours HTTPS_PROXY on the ECS runners' egress, which
|
||||
* node's global fetch does not (see check-lint-gate-freshness.mjs).
|
||||
*/
|
||||
export async function curlJson(path, token, run = execFileAsync) {
|
||||
try {
|
||||
const { stdout } = await run(
|
||||
'curl',
|
||||
[
|
||||
'-sS',
|
||||
'-fL',
|
||||
'--max-time',
|
||||
'30',
|
||||
'-H',
|
||||
`Authorization: Bearer ${token}`,
|
||||
'-H',
|
||||
'Accept: application/vnd.github+json',
|
||||
'-H',
|
||||
'X-GitHub-Api-Version: 2022-11-28',
|
||||
`https://api.github.com${path}`,
|
||||
],
|
||||
{ maxBuffer: 64 * 1024 * 1024 },
|
||||
);
|
||||
return JSON.parse(stdout);
|
||||
} catch (error) {
|
||||
// execFile's message embeds the full argv, bearer token included, so
|
||||
// rebuild the error from the exit code and curl's last stderr line.
|
||||
const stderr = String(error.stderr ?? '')
|
||||
.trim()
|
||||
.split('\n')
|
||||
.pop();
|
||||
throw new Error(
|
||||
`GET ${path} failed (curl exit ${error.code ?? '?'})${stderr ? `: ${stderr}` : ''}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function git(...args) {
|
||||
const { stdout } = await execFileAsync('git', args, {
|
||||
maxBuffer: 64 * 1024 * 1024,
|
||||
timeout: 120_000,
|
||||
});
|
||||
return stdout;
|
||||
}
|
||||
|
||||
// For one command at a time: fetch the merge base's commit and trees only,
|
||||
// and let `git diff` pull just the blobs it compares. A plain depth-1 fetch
|
||||
// would transfer the whole tree again, about 60 MiB, on every PR.
|
||||
const PARTIAL = [
|
||||
'-c',
|
||||
'extensions.partialClone=origin',
|
||||
'-c',
|
||||
'remote.origin.promisor=true',
|
||||
];
|
||||
|
||||
/** Net test and production lines between the PR's merge base and its head. */
|
||||
export async function measure({ fetchJson, runGit, repo, baseSha, headSha }) {
|
||||
const compare = await fetchJson(
|
||||
`/repos/${repo}/compare/${baseSha}...${headSha}?per_page=1`,
|
||||
);
|
||||
const mergeBase = compare?.merge_base_commit?.sha;
|
||||
if (!mergeBase) throw new Error('compare API returned no merge base');
|
||||
let partial = false;
|
||||
try {
|
||||
await runGit('cat-file', '-e', `${mergeBase}^{commit}`);
|
||||
} catch {
|
||||
const fetch = ['fetch', '--no-tags', '--depth=1'];
|
||||
try {
|
||||
await runGit(
|
||||
...PARTIAL,
|
||||
...fetch,
|
||||
'--filter=blob:none',
|
||||
'origin',
|
||||
mergeBase,
|
||||
);
|
||||
partial = true;
|
||||
} catch {
|
||||
await runGit(...fetch, 'origin', mergeBase);
|
||||
}
|
||||
}
|
||||
try {
|
||||
const numstat = await runGit(
|
||||
...(partial ? PARTIAL : []),
|
||||
'diff',
|
||||
'--numstat',
|
||||
'--no-renames',
|
||||
mergeBase,
|
||||
headSha,
|
||||
'--',
|
||||
...PACKAGES.map((name) => `packages/${name}`),
|
||||
);
|
||||
return { mergeBase, tallied: tally(parseNumstat(numstat)) };
|
||||
} finally {
|
||||
// The filtered fetch records its filter on the remote; drop it so later
|
||||
// fetches in this checkout stay ordinary.
|
||||
if (partial) {
|
||||
await runGit(
|
||||
'config',
|
||||
'--unset',
|
||||
'remote.origin.partialclonefilter',
|
||||
).catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { GITHUB_TOKEN, BASE_SHA, HEAD_SHA, GITHUB_REPOSITORY } = process.env;
|
||||
if (!GITHUB_TOKEN || !BASE_SHA || !HEAD_SHA || !GITHUB_REPOSITORY) {
|
||||
console.log(
|
||||
'::warning::test growth: BASE_SHA, HEAD_SHA, GITHUB_REPOSITORY or ' +
|
||||
'GITHUB_TOKEN missing; skipping the check',
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { mergeBase, tallied } = await measure({
|
||||
fetchJson: (path) => curlJson(path, GITHUB_TOKEN),
|
||||
runGit: git,
|
||||
repo: GITHUB_REPOSITORY,
|
||||
baseSha: BASE_SHA,
|
||||
headSha: HEAD_SHA,
|
||||
});
|
||||
const summary = renderSummary(tallied);
|
||||
console.log(`Merge base ${mergeBase.slice(0, 12)}\n\n${summary}`);
|
||||
if (process.env.GITHUB_STEP_SUMMARY) {
|
||||
appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${summary}\n`);
|
||||
}
|
||||
const annotation = renderAnnotation(tallied);
|
||||
if (annotation) console.log(annotation);
|
||||
} catch (error) {
|
||||
// One line: an annotation ends at the first newline.
|
||||
const reason = String(error.message).split('\n')[0];
|
||||
console.log(
|
||||
`::warning::test growth check could not run (${reason}); skipping it`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
process.argv[1] &&
|
||||
import.meta.url === pathToFileURL(process.argv[1]).href
|
||||
) {
|
||||
await main();
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import {
|
||||
FLAG_THRESHOLD,
|
||||
classify,
|
||||
curlJson,
|
||||
measure,
|
||||
packageOf,
|
||||
parseNumstat,
|
||||
renderAnnotation,
|
||||
renderSummary,
|
||||
tally,
|
||||
} from './check-test-growth.mjs';
|
||||
|
||||
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
||||
|
||||
test('classifies test, production and other files', () => {
|
||||
const cases = {
|
||||
'packages/core/src/a.test.ts': 'test',
|
||||
'packages/cli/src/ui/App.test.tsx': 'test',
|
||||
'packages/core/src/a.spec.ts': 'test',
|
||||
'packages/core/src/tools/shell.test-helper.ts': 'test',
|
||||
'packages/core/src/x/case.fixtures.json': 'test',
|
||||
'packages/core/src/utils/testUtils.ts': 'test',
|
||||
'packages/core/test-setup.ts': 'test',
|
||||
'packages/core/src/__tests__/a.ts': 'test',
|
||||
'packages/cli/src/__snapshots__/App.test.tsx.snap': 'test',
|
||||
'packages/core/src/test-utils/mock-tool.ts': 'test',
|
||||
'packages/cli/src/commands/review/lib/test-utils.ts': 'test',
|
||||
'packages/core/src/lsp/__e2e__/lsp-e2e-test.ts': 'test',
|
||||
'packages/web-shell/client/e2e/utils/mockDaemon.ts': 'test',
|
||||
'packages/web-shell/client/test/setup.ts': 'test',
|
||||
'packages/core/src/core/client.ts': 'production',
|
||||
'packages/web-shell/client/src/App.tsx': 'production',
|
||||
'packages/cli/src/index.mjs': 'production',
|
||||
'packages/core/src/config/settings.schema.ts': 'other',
|
||||
'packages/core/src/generated/git-commit.ts': 'other',
|
||||
'packages/core/src/x.generated.ts': 'other',
|
||||
'packages/core/package.json': 'other',
|
||||
'packages/web-shell/client/src/app.css': 'other',
|
||||
'packages/core/README.md': 'other',
|
||||
};
|
||||
for (const [path, kind] of Object.entries(cases)) {
|
||||
assert.equal(classify(path), kind, path);
|
||||
}
|
||||
});
|
||||
|
||||
test('counts only core, cli and web-shell', () => {
|
||||
assert.equal(packageOf('packages/core/src/a.ts'), 'core');
|
||||
assert.equal(packageOf('packages/web-shell/client/src/a.ts'), 'web-shell');
|
||||
assert.equal(packageOf('packages/sdk-typescript/src/a.ts'), null);
|
||||
assert.equal(packageOf('scripts/build.js'), null);
|
||||
});
|
||||
|
||||
test('tallies numstat per package, skipping binary and other files', () => {
|
||||
const numstat = [
|
||||
'120\t20\tpackages/core/src/a.test.ts',
|
||||
'30\t5\tpackages/core/src/a.ts',
|
||||
'-\t-\tpackages/cli/assets/logo.png',
|
||||
'40\t0\tpackages/cli/src/b.test.tsx',
|
||||
'9\t1\tpackages/cli/package.json',
|
||||
'7\t2\tpackages/sdk-typescript/src/c.test.ts',
|
||||
'',
|
||||
].join('\n');
|
||||
const entries = parseNumstat(numstat);
|
||||
assert.equal(entries.length, 5);
|
||||
assert.deepEqual(tally(entries), {
|
||||
byPackage: {
|
||||
core: {
|
||||
test: { added: 120, deleted: 20 },
|
||||
production: { added: 30, deleted: 5 },
|
||||
},
|
||||
cli: {
|
||||
test: { added: 40, deleted: 0 },
|
||||
production: { added: 0, deleted: 0 },
|
||||
},
|
||||
'web-shell': {
|
||||
test: { added: 0, deleted: 0 },
|
||||
production: { added: 0, deleted: 0 },
|
||||
},
|
||||
},
|
||||
total: {
|
||||
test: { added: 160, deleted: 20 },
|
||||
production: { added: 30, deleted: 5 },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('flags at the threshold of net test lines, not below', () => {
|
||||
const withNet = (n) =>
|
||||
tally([
|
||||
{ added: n + 100, deleted: 100, path: 'packages/core/src/a.test.ts' },
|
||||
]);
|
||||
assert.equal(FLAG_THRESHOLD, 2000);
|
||||
|
||||
const below = withNet(FLAG_THRESHOLD - 1);
|
||||
assert.equal(renderAnnotation(below), null);
|
||||
assert.doesNotMatch(renderSummary(below), /net test lines, at or above/);
|
||||
|
||||
const at = withNet(FLAG_THRESHOLD);
|
||||
assert.match(
|
||||
renderAnnotation(at),
|
||||
/^::warning title=Test growth::This PR adds 2,000 net test lines/,
|
||||
);
|
||||
assert.match(renderSummary(at), /\| core \| \+2,100 \/ −100 \(net \+2,000\)/);
|
||||
assert.match(
|
||||
renderSummary(at),
|
||||
/2,000 net test lines, at or above the 2,000-line flag/,
|
||||
);
|
||||
});
|
||||
|
||||
test('measures from the merge base, fetching it only when missing', async () => {
|
||||
const [base, head, mergeBase] = ['b', 'h', 'm'].map((c) => c.repeat(40));
|
||||
const run = async ({ present, partialFails = false }) => {
|
||||
const calls = [];
|
||||
const result = await measure({
|
||||
repo: 'QwenLM/qwen-code',
|
||||
baseSha: base,
|
||||
headSha: head,
|
||||
fetchJson: async (path) => {
|
||||
assert.equal(
|
||||
path,
|
||||
`/repos/QwenLM/qwen-code/compare/${base}...${head}?per_page=1`,
|
||||
);
|
||||
return { merge_base_commit: { sha: mergeBase } };
|
||||
},
|
||||
runGit: async (...args) => {
|
||||
calls.push(args.join(' '));
|
||||
if (args[0] === 'cat-file' && !present) throw new Error('missing');
|
||||
if (partialFails && args.includes('--filter=blob:none')) {
|
||||
throw new Error('no partial clone');
|
||||
}
|
||||
// The diff must start at the merge base, not the base tip.
|
||||
if (args.includes('diff')) return '3\t1\tpackages/core/src/a.test.ts\n';
|
||||
return '';
|
||||
},
|
||||
});
|
||||
assert.deepEqual(result.tallied.total.test, { added: 3, deleted: 1 });
|
||||
return calls;
|
||||
};
|
||||
const PARTIAL =
|
||||
'-c extensions.partialClone=origin -c remote.origin.promisor=true';
|
||||
const DIFF = `diff --numstat --no-renames ${mergeBase} ${head} -- packages/core packages/cli packages/web-shell`;
|
||||
|
||||
assert.deepEqual(await run({ present: true }), [
|
||||
`cat-file -e ${mergeBase}^{commit}`,
|
||||
DIFF,
|
||||
]);
|
||||
assert.deepEqual(await run({ present: false }), [
|
||||
`cat-file -e ${mergeBase}^{commit}`,
|
||||
`${PARTIAL} fetch --no-tags --depth=1 --filter=blob:none origin ${mergeBase}`,
|
||||
`${PARTIAL} ${DIFF}`,
|
||||
'config --unset remote.origin.partialclonefilter',
|
||||
]);
|
||||
assert.deepEqual(
|
||||
(await run({ present: false, partialFails: true })).slice(2),
|
||||
[`fetch --no-tags --depth=1 origin ${mergeBase}`, DIFF],
|
||||
);
|
||||
});
|
||||
|
||||
test('ci.yml runs the check in lint_and_static on pull requests', () => {
|
||||
const ci = readFileSync(join(repoRoot, '.github/workflows/ci.yml'), 'utf8');
|
||||
const jobStart = ci.indexOf('\n lint_and_static:');
|
||||
const jobEnd = ci.indexOf('\n test_macos:', jobStart);
|
||||
assert.ok(jobStart > 0 && jobEnd > jobStart, 'lint_and_static job not found');
|
||||
const job = ci.slice(jobStart, jobEnd);
|
||||
|
||||
const step = job.indexOf("- name: 'Report test growth'");
|
||||
assert.ok(step > 0, 'test growth step missing');
|
||||
const stepBlock = job.slice(step, job.indexOf('\n - name:', step + 1));
|
||||
for (const expected of [
|
||||
"github.event_name == 'pull_request'",
|
||||
"skip_ci != 'true'",
|
||||
'github.event.pull_request.base.sha',
|
||||
'github.event.pull_request.head.sha',
|
||||
'node .github/scripts/check-test-growth.mjs',
|
||||
]) {
|
||||
assert.ok(stepBlock.includes(expected), `step must include ${expected}`);
|
||||
}
|
||||
assert.ok(
|
||||
step < job.indexOf("- name: 'Install dependencies'"),
|
||||
'the check needs no dependencies and must run before the install',
|
||||
);
|
||||
|
||||
for (const list of ['HELPER_TESTS_DEP_FREE', 'HELPER_TESTS']) {
|
||||
const line = ci.split('\n').find((l) => l.trim().startsWith(`${list}:`));
|
||||
assert.ok(
|
||||
line?.includes('.github/scripts/check-test-growth.test.mjs'),
|
||||
`${list} must run this test`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('API errors never carry the token', async () => {
|
||||
const failing = async () => {
|
||||
throw Object.assign(
|
||||
new Error('Command failed: curl -H Authorization: Bearer secret-token'),
|
||||
{
|
||||
code: 22,
|
||||
stderr: 'curl: (22) The requested URL returned error: 401\n',
|
||||
},
|
||||
);
|
||||
};
|
||||
await assert.rejects(
|
||||
curlJson('/repos/o/r/compare/a...b', 'secret-token', failing),
|
||||
(error) => {
|
||||
assert.doesNotMatch(error.message, /secret-token/);
|
||||
assert.match(error.message, /curl exit 22\): curl: \(22\).*401$/);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -87,8 +87,8 @@ env:
|
||||
# re-runs every suite after `npm ci`, and a push to main always classifies
|
||||
# `full` (classify_pr computes a profile only for pull_request), so what
|
||||
# the fast lane skips is still re-checked post-merge.
|
||||
HELPER_TESTS_DEP_FREE: '.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/dsw-swe-verified/make-manifest.test.mjs .github/scripts/dsw-swe-verified/make-terminal-bench-manifest.test.mjs .github/scripts/update-ecs-runner-qwen-workflow.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/autofix-status-heartbeat.test.mjs .github/scripts/check-disk-floor.test.mjs .github/scripts/check-lint-gate-freshness.test.mjs .github/scripts/review-runner-schedule.test.mjs .github/scripts/ecs-runner/qwen-docker-cleanup.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/update-ecs-runner-qwen-workflow.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/check-lint-gate-freshness.test.mjs .github/scripts/ci-disk-pressure.test.mjs .github/scripts/e2e-build.test.mjs integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.test.mjs .github/scripts/review-runner-schedule.test.mjs .github/scripts/ecs-runner/qwen-docker-cleanup.test.mjs .github/scripts/ecs-runner/review-scratch-cleanup.test.mjs'
|
||||
HELPER_TESTS_DEP_FREE: '.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/dsw-swe-verified/make-manifest.test.mjs .github/scripts/dsw-swe-verified/make-terminal-bench-manifest.test.mjs .github/scripts/update-ecs-runner-qwen-workflow.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/autofix-status-heartbeat.test.mjs .github/scripts/check-disk-floor.test.mjs .github/scripts/check-lint-gate-freshness.test.mjs .github/scripts/review-runner-schedule.test.mjs .github/scripts/ecs-runner/qwen-docker-cleanup.test.mjs .github/scripts/check-test-growth.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/update-ecs-runner-qwen-workflow.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/check-lint-gate-freshness.test.mjs .github/scripts/ci-disk-pressure.test.mjs .github/scripts/e2e-build.test.mjs integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.test.mjs .github/scripts/review-runner-schedule.test.mjs .github/scripts/ecs-runner/qwen-docker-cleanup.test.mjs .github/scripts/ecs-runner/review-scratch-cleanup.test.mjs .github/scripts/check-test-growth.test.mjs'
|
||||
# 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
|
||||
@@ -1097,6 +1097,17 @@ jobs:
|
||||
HEAD_SHA: '${{ github.event.pull_request.head.sha }}'
|
||||
run: 'node .github/scripts/check-lint-gate-freshness.mjs'
|
||||
|
||||
# Advisory test-growth summary and flag; see the script header.
|
||||
- name: 'Report test growth'
|
||||
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && github.event_name == 'pull_request' }}"
|
||||
continue-on-error: true
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
BASE_SHA: '${{ github.event.pull_request.base.sha }}'
|
||||
HEAD_SHA: '${{ github.event.pull_request.head.sha }}'
|
||||
run: 'node .github/scripts/check-test-growth.mjs'
|
||||
|
||||
# Avoid setup-node downloads on ECS, where nodejs.org may be unreachable
|
||||
# through the egress proxy; reuse the machine's Node instead.
|
||||
- name: 'Set up Node.js 22.x (hosted)'
|
||||
|
||||
Reference in new issue
Block a user