Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
97 changes: 74 additions & 23 deletions .github/scripts/classify-release-notes.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
#!/usr/bin/env node

import { execFileSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { parseGeneratedEntries } from '../../scripts/generate-release-notes.js';

const INTERNAL_LABELS = new Set([
'category/development',
Expand Down Expand Up @@ -55,28 +57,8 @@ export function shouldAutoSkipChangelog({ title, labels = [], files = [] }) {
);
}

function main() {
const repo = process.env.GITHUB_REPOSITORY || '';
const number = process.env.PR_NUMBER || '';
if (
!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repo) ||
!/^[1-9]\d*$/.test(number)
) {
throw new Error(
'GITHUB_REPOSITORY and PR_NUMBER must identify a pull request.',
);
}

const metadata = JSON.parse(
execFileSync(
'gh',
['pr', 'view', number, '--repo', repo, '--json', 'title,labels'],
{
encoding: 'utf8',
},
),
);
const files = execFileSync(
function fetchFiles(repo, number) {
return execFileSync(
'gh',
[
'api',
Expand All @@ -89,14 +71,83 @@ function main() {
)
.split(/\r?\n/)
.filter(Boolean);
}

function fetchMetadata(repo, number) {
return JSON.parse(
execFileSync(
'gh',
['pr', 'view', number, '--repo', repo, '--json', 'title,labels'],
{ encoding: 'utf8' },
),
);
}

function mainSingle() {
const repo = process.env.GITHUB_REPOSITORY || '';
const number = process.env.PR_NUMBER || '';
Comment thread
yiliang114 marked this conversation as resolved.
Resolved
if (
!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repo) ||
!/^[1-9]\d*$/.test(number)
) {
throw new Error(
'GITHUB_REPOSITORY and PR_NUMBER must identify a pull request.',
);
}

const metadata = fetchMetadata(repo, number);
const files = fetchFiles(repo, number);
process.stdout.write(
`${shouldAutoSkipChangelog({ ...metadata, files }) ? 'skip' : 'include'}\n`,
);
}

function mainBatch() {
const repo = process.env.GITHUB_REPOSITORY || '';
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repo)) {
throw new Error('GITHUB_REPOSITORY must be set to owner/repo.');
}

const prs = parseGeneratedEntries(readFileSync(0, 'utf8'));
const labeled = [];

for (const pr of prs) {
const number = String(pr.number);
if (!/^[1-9]\d*$/.test(number)) continue;
try {
const metadata = fetchMetadata(repo, number);
const files = fetchFiles(repo, number);
if (shouldAutoSkipChangelog({ ...metadata, files })) {
execFileSync('gh', [
'pr',
'edit',
number,
'--repo',
repo,
'--add-label',
AUTO_LABEL,
]);
labeled.push(number);
}
Comment thread
yiliang114 marked this conversation as resolved.
Resolved
} catch {
process.stderr.write(
`::warning::Failed to classify PR #${number}; skipping.\n`,
);
}
}

if (labeled.length > 0) {
process.stdout.write(`Labeled: ${labeled.join(', ')}\n`);
}
}

if (
process.argv[1] &&
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
) {
main();
if (process.argv.includes('--batch')) {
mainBatch();
} else {
mainSingle();
}
}
81 changes: 68 additions & 13 deletions .github/scripts/classify-release-notes.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -305,26 +305,81 @@ describe('release note classification', () => {
}
});

it('wires reclassification and exclusion to the same automatic label', () => {
const workflow = readFileSync(
join(import.meta.dirname, '../workflows/classify-release-notes.yml'),
it('wires batch labeling and exclusion through release.yml', () => {
const release = readFileSync(
join(import.meta.dirname, '../workflows/release.yml'),
'utf8',
);
const release = readFileSync(
const changelog = readFileSync(
join(import.meta.dirname, '../release.yml'),
'utf8',
);

for (const action of ['synchronize', 'edited', 'labeled', 'unlabeled']) {
assert.match(workflow, new RegExp(`- '${action}'`));
}
assert.match(workflow, /AUTO_LABEL: 'skip-changelog-auto'/);
assert.match(changelog, /- 'skip-changelog-auto'/);
assert.match(release, /classify-release-notes\.mjs --batch/);
assert.match(release, /Auto-label internal CI PRs/);
assert.match(
workflow,
/github\.event\.label\.name != 'skip-changelog-auto'/,
release,
/Auto-label internal CI PRs for release notes exclusion'\s+continue-on-error: true/,
);
assert.match(workflow, /classification failed; including this PR/);
assert.doesNotMatch(workflow, /decision=unchanged/);
assert.match(release, /- 'skip-changelog-auto'/);
assert.match(release, /releases\/generate-notes/);
assert.doesNotMatch(release, /--search "merged:/);
});

it('batch mode labels qualifying PRs from stdin', () => {
const dir = mkdtempSync(join(tmpdir(), 'release-note-classifier-'));
try {
const labeled = join(dir, 'labeled.txt');
const gh = join(dir, 'gh');
writeFileSync(
gh,
[
'#!/usr/bin/env node',
'const args = process.argv.slice(2);',
"if (args[0] === 'pr' && args[1] === 'view') {",
" const title = args[2] === '10' ? 'ci: speed up checks' : 'feat: new feature';",
' process.stdout.write(JSON.stringify({ title, labels: [] }));',
' process.exit(0);',
'}',
"if (args[0] === 'api' && args.includes('.[] | .filename, (.previous_filename // empty)')) {",
" process.stdout.write('.github/workflows/ci.yml\\n');",
' process.exit(0);',
'}',
"if (args[0] === 'pr' && args[1] === 'edit') {",
` require('node:fs').appendFileSync(${JSON.stringify(labeled)}, args[2] + '\\n');`,
' process.exit(0);',
'}',
'process.exit(1);',
].join('\n'),
);
chmodSync(gh, 0o755);

const input = [
"## What's Changed",
'* ci: speed up checks by @alice in https://github.com/QwenLM/qwen-code/pull/10',
'* feat: new feature by @alice in https://github.com/QwenLM/qwen-code/pull/11',
].join('\n');

const output = execFileSync(
process.execPath,
[join(import.meta.dirname, 'classify-release-notes.mjs'), '--batch'],
{
encoding: 'utf8',
input,
env: {
...process.env,
GITHUB_REPOSITORY: 'QwenLM/qwen-code',
PATH: `${dir}:${process.env.PATH}`,
},
},
);

assert.match(output, /Labeled: 10/);
assert.doesNotMatch(output, /11/);
const labeledContent = readFileSync(labeled, 'utf8').trim();
assert.equal(labeledContent, '10');
Comment thread
yiliang114 marked this conversation as resolved.
Resolved
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
});
66 changes: 0 additions & 66 deletions .github/workflows/classify-release-notes.yml

This file was deleted.

17 changes: 17 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,23 @@ jobs:
run: |-
npm run verify:installation-release -- --dir dist/standalone

- name: 'Auto-label internal CI PRs for release notes exclusion'
continue-on-error: true
if: |-
${{ needs.prepare.outputs.is_dry_run == 'false' }}
env:
GITHUB_TOKEN: '${{ secrets.CI_BOT_PAT }}'
PREVIOUS_RELEASE_TAG: '${{ needs.prepare.outputs.previous_release_tag }}'
RELEASE_BRANCH: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
RELEASE_TAG: '${{ needs.prepare.outputs.release_tag }}'
run: |-
gh label create 'skip-changelog-auto' --repo "${GITHUB_REPOSITORY}" --color 'ededed' --description 'Automatically exclude internal CI changes from release notes' --force
gh api --method POST "repos/${GITHUB_REPOSITORY}/releases/generate-notes" \
-f "tag_name=${RELEASE_TAG}" \
-f "previous_tag_name=${PREVIOUS_RELEASE_TAG}" \
-f "target_commitish=${RELEASE_BRANCH}" --jq '.body' | \
node .github/scripts/classify-release-notes.mjs --batch

- name: 'Create GitHub Release and Tag'
if: |-
${{ needs.prepare.outputs.is_dry_run == 'false' }}
Expand Down
Loading