qwen-code/.github/scripts/classify-release-notes.test.mjs
易良 cda0e0348d
ci: move release-note classifier from per-PR workflow to release-time batch (#7339)
* ci: move release-note classifier from per-PR workflow to release-time batch

* fix(ci): enumerate release PRs from generated notes

* fix(ci): reconcile release note labels

* fix(ci): address release classifier review

* fix(ci): surface release classifier failures
2026-07-20 23:52:09 +00:00

285 lines
8.6 KiB
JavaScript

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 { join } from 'node:path';
import { describe, it } from 'node:test';
import { parse } from 'yaml';
import { shouldAutoSkipChangelog } from './classify-release-notes.mjs';
describe('release note classification', () => {
it('only skips internal CI changes', () => {
const cases = [
{
title: 'ci: speed up PR checks',
files: ['.github/workflows/ci.yml'],
expected: true,
},
{
title: 'ci(autofix): harden review automation',
files: [
'.github/workflows/qwen-autofix.yml',
'.qwen/skills/autofix/SKILL.md',
'scripts/tests/qwen-autofix-workflow.test.js',
],
expected: true,
},
{
title: 'ci: update test helpers',
files: ['packages/core/src/__tests__/utils.ts'],
expected: true,
},
{
title: 'ci: update vitest config',
files: ['packages/cli/vitest.config.ts'],
expected: true,
},
{
title: 'ci: update spec tests',
files: ['packages/core/src/auth.spec.ts'],
expected: true,
},
{
title: 'ci: update playwright config',
files: ['packages/e2e/playwright.config.ts'],
expected: true,
},
{
title: 'ci: update release classifier tests',
files: ['.github/scripts/classify-release-notes.test.mjs'],
expected: true,
},
{
title: 'Fix flaky CI routing',
labels: [{ name: 'scope/ci-cd' }],
files: ['.github/workflows/ci.yml'],
expected: true,
},
{
title: 'ci!: breaking dispatch change',
files: ['.github/workflows/ci.yml'],
expected: false,
},
{
title: 'Update action permissions',
labels: ['scope/github-actions'],
files: ['.github/workflows/ci.yml'],
expected: true,
},
{
title: 'fix(ci): repair check results',
labels: ['scope/ci-cd'],
files: ['.github/workflows/ci.yml'],
expected: false,
},
{
title: 'ci: fix check',
labels: ['bug'],
files: ['.github/workflows/ci.yml'],
expected: false,
},
{
title: 'ci: fix check',
labels: ['breaking-change'],
files: ['.github/workflows/ci.yml'],
expected: false,
},
{
title: 'ci: keep automatic exclusion stable',
labels: ['skip-changelog-auto'],
files: ['.github/workflows/ci.yml'],
expected: true,
},
{
title: 'Update docs',
files: ['.github/workflows/ci.yml'],
expected: false,
},
{
title: 'ci: empty',
files: [],
expected: false,
},
{
title: 'ci: update release automation',
files: ['.github/workflows/finalize-release.yml'],
expected: false,
},
{
title: 'ci: update release helper',
files: ['.github/scripts/publish-release.mjs'],
expected: false,
},
{
title: 'ci: update changelog helper',
files: ['.github/workflows/update-changelog.yml'],
expected: false,
},
{
title: 'ci: update deploy pipeline',
files: ['.github/workflows/deploy-app.yml'],
expected: false,
},
{
title: 'ci: bump sync action',
files: ['.github/workflows/sync-labels.yml'],
expected: false,
},
{
title: 'ci: update prebuild pipeline',
files: ['.github/workflows/prebuild.yml'],
expected: false,
},
{
title: 'ci: update package pipeline',
files: ['.github/workflows/package-cli.yml'],
expected: false,
},
{
title: 'ci: update installer pipeline',
files: ['.github/workflows/build-installer.yml'],
expected: false,
},
{
title: 'ci: update artifact upload',
files: ['.github/workflows/upload-artifact.yml'],
expected: false,
},
{
title: 'ci: update image build',
files: ['.github/scripts/build-image.mjs'],
expected: false,
},
{
title: 'ci: update cd pipeline',
files: ['.github/workflows/cd-pages.yml'],
expected: false,
},
{
title: 'ci: support a new platform build',
labels: ['category/platform'],
files: ['.github/workflows/ci.yml'],
expected: false,
},
{
title: 'ci: update checks and runtime',
files: ['.github/workflows/ci.yml', 'packages/core/src/index.ts'],
expected: false,
},
{
title: 'ci: move a runtime file into automation',
files: ['.github/scripts/runtime.ts', 'packages/core/src/runtime.ts'],
expected: false,
},
{
title: 'ci: keep manual exclusion',
labels: ['skip-changelog'],
files: ['.github/workflows/ci.yml'],
expected: false,
},
];
for (const { expected, ...pullRequest } of cases) {
assert.equal(
shouldAutoSkipChangelog(pullRequest),
expected,
pullRequest.title,
);
}
});
it('wires batch labeling and exclusion through release.yml', () => {
const release = readFileSync(
join(import.meta.dirname, '../workflows/release.yml'),
'utf8',
);
const changelog = readFileSync(
join(import.meta.dirname, '../release.yml'),
'utf8',
);
const workflow = parse(release);
const publish = workflow.jobs.publish;
const autoLabel = publish.steps.find(
(step) =>
step.name === 'Auto-label internal CI PRs for release notes exclusion',
);
assert.match(changelog, /- 'skip-changelog-auto'/);
assert.equal(autoLabel['continue-on-error'], true);
assert.equal(autoLabel.env.GITHUB_TOKEN, '${{ github.token }}');
assert.equal(publish.permissions.issues, 'write');
assert.equal(publish.permissions['pull-requests'], 'write');
assert.match(autoLabel.run, /classify-release-notes\.mjs/);
assert.match(autoLabel.run, /commits="\$\(git rev-list/);
assert.match(autoLabel.run, /Cannot enumerate commits/);
assert.match(autoLabel.run, /Failed to fetch PRs for commit/);
});
it('updates labels after a lookup failure and exits non-zero', () => {
const dir = mkdtempSync(join(tmpdir(), 'release-note-classifier-'));
try {
const updates = join(dir, 'updates.txt');
const gh = join(dir, 'gh');
writeFileSync(
gh,
[
'#!/usr/bin/env node',
'const args = process.argv.slice(2);',
"if (args[0] === 'api' && args.includes('.[] | .filename, (.previous_filename // empty)')) {",
" if (args.some((arg) => arg.endsWith('/pulls/11/files'))) { process.stderr.write('lookup failed\\n'); process.exit(1); }",
" process.stdout.write('.github/workflows/ci.yml\\n');",
' process.exit(0);',
'}',
"if (args[0] === 'pr' && args[1] === 'edit') {",
` const action = args.includes('--remove-label') ? 'remove' : 'add';`,
` require('node:fs').appendFileSync(${JSON.stringify(updates)}, args[2] + ' ' + action + '\\n');`,
' process.exit(0);',
'}',
'process.exit(1);',
].join('\n'),
);
chmodSync(gh, 0o755);
const input = JSON.stringify([
{ number: 10, title: 'ci: speed up checks', labels: [] },
{ number: 11, title: 'ci: broken lookup', labels: [] },
{
number: 12,
title: 'fix: user-visible bug',
labels: [{ name: 'skip-changelog-auto' }],
},
{ number: 13, title: 'feat: new feature', labels: [] },
]);
const result = spawnSync(
process.execPath,
[join(import.meta.dirname, 'classify-release-notes.mjs')],
{
encoding: 'utf8',
input,
env: {
...process.env,
GITHUB_REPOSITORY: 'QwenLM/qwen-code',
PATH: `${dir}:${process.env.PATH}`,
},
},
);
assert.equal(result.status, 1);
assert.match(result.stdout, /Labeled: 10/);
assert.match(result.stdout, /Unlabeled: 12/);
assert.match(result.stderr, /Failed to process PR #11/);
assert.match(result.stderr, /lookup failed/);
const updateContent = readFileSync(updates, 'utf8').trim();
assert.equal(updateContent, '10 add\n12 remove');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
});