qwen-code/scripts/tests/workflow-size.test.js
Shaojin Wen 3b3818db87
fix(ci): keep qwen-autofix.yml under GitHub's 500 KB start-runs limit (#9517)
GitHub does not start runs for a workflow file larger than 500 KB (512,000
bytes) and reports nothing when it stops. qwen-autofix.yml crossed that line
on 2026-08-19 at 512,782 bytes: schedule ticks stopped firing, every
workflow_dispatch sat "queued" forever with zero jobs and could not be
cancelled, and issues/issue_comment went quiet — while pull_request_review
runs kept succeeding, because a PR event resolves the workflow from the PR's
own branch and those carry older, smaller copies of this file. The loop
therefore looked half-alive and stayed dark for a day.

Move 75 long comment blocks (1,326 lines) verbatim into a sibling design
record, .github/workflows/qwen-autofix.md, leaving each block's opening lines
plus a `qwen-autofix.md#af-NNN` pointer where it sat: 518,055 -> 426,437
bytes. No executable line changes — the YAML parses to an identical document
outside `run:`, every `run:` script still passes `bash -n`, and the only lines
removed anywhere are comments. Steps that are duplicated verbatim across jobs
share one pointer so they stay byte-identical.

Add .github/scripts/check-workflow-size.sh (gate at 470,000 bytes), wired into
CI on every profile: a .github-only PR classifies as `github_ci_only` and
skips the `full`-only checks, which is exactly the PR that can trip this.
Tests pin the gate, every workflow's size, and pointer/section symmetry.

Delete qwen-autofix-recovery.yml. It was cloned during the incident on the
theory that the workflow ENTITY was wedged, but it carried the same oversized
file, so its dispatches queued identically and its schedule never fired.
2026-08-20 01:56:45 +00:00

81 lines
2.8 KiB
JavaScript

/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { readFileSync, readdirSync } from 'node:fs';
import { join } from 'node:path';
import { describe, expect, it } from 'vitest';
// GitHub does not start runs for a workflow file over 500 KB (512,000 bytes)
// and reports nothing when it stops — see .github/scripts/check-workflow-size.sh
// and .github/workflows/qwen-autofix.md for the incident this encodes.
const GITHUB_LIMIT_BYTES = 512_000;
const WORKFLOW_DIR = '.github/workflows';
const gateScript = readFileSync(
'.github/scripts/check-workflow-size.sh',
'utf8',
);
const ciWorkflow = readFileSync(join(WORKFLOW_DIR, 'ci.yml'), 'utf8');
const gateBytes = Number(
gateScript.match(/GATE_BYTES="\$\{WORKFLOW_SIZE_GATE_BYTES:-(\d+)\}"/)?.[1],
);
const workflowFiles = readdirSync(WORKFLOW_DIR)
.filter((name) => name.endsWith('.yml') || name.endsWith('.yaml'))
.map((name) => join(WORKFLOW_DIR, name));
describe('workflow file size', () => {
it('keeps the gate below GitHub 500 KB start-runs limit', () => {
expect(gateBytes).toBeGreaterThan(0);
expect(gateBytes).toBeLessThan(GITHUB_LIMIT_BYTES);
});
it.each(workflowFiles)('%s stays under the gate', (file) => {
const bytes = Buffer.byteLength(readFileSync(file));
expect(bytes).toBeLessThan(gateBytes);
});
it('runs the gate on every CI profile, not just full', () => {
// A .github-only PR classifies as `github_ci_only`; gating the check on the
// `full` profile would skip it for exactly the changes that can trip it.
const step = ciWorkflow.match(
/- name: 'Check workflow file size'[\s\S]*?run: '(.+?)'/,
);
expect(step?.[1]).toBe('.github/scripts/check-workflow-size.sh');
expect(step?.[0]).toContain(
'if: "${{ needs.classify_pr.outputs.skip_ci != \'true\' }}"',
);
expect(step?.[0]).not.toContain('ci_profile');
});
});
describe('qwen-autofix.yml design-record pointers', () => {
const workflow = readFileSync(join(WORKFLOW_DIR, 'qwen-autofix.yml'), 'utf8');
const doc = readFileSync(join(WORKFLOW_DIR, 'qwen-autofix.md'), 'utf8');
const pointers = [...workflow.matchAll(/qwen-autofix\.md#(af-\d+)/g)].map(
(m) => m[1],
);
const anchors = [...doc.matchAll(/<a id="(af-\d+)"><\/a>/g)].map((m) => m[1]);
it('every pointer resolves to a section', () => {
expect(pointers.length).toBeGreaterThan(0);
expect(
[...new Set(pointers)].filter((id) => !anchors.includes(id)),
).toEqual([]);
});
it('every section is still pointed at from the workflow', () => {
expect(anchors.filter((id) => !pointers.includes(id))).toEqual([]);
});
it('lists every section in the contents table', () => {
const listed = [...doc.matchAll(/^- \[\d+\..*?\]\(#(af-\d+)\)$/gm)].map(
(m) => m[1],
);
expect(listed).toEqual(anchors);
});
});