Skip to content
Merged
Changes from all 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
246 changes: 213 additions & 33 deletions scripts/tasks/generate-changelog.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const repoRoot = path.resolve(__dirname, '..', '..');
function parseArgs(argv) {
const parser = yargs(argv)
.usage(
'Usage: yarn generate-changelog [--codex|--claude] [--debug] [<pkg@version> ...]'
'Usage: yarn generate-changelog [--codex|--claude] [--debug] [--format <text|csv|json>] [<pkg@version> ...]'
)
.example(
'$0 --codex eslint-plugin-react-hooks@7.0.1',
Expand All @@ -50,6 +50,12 @@ function parseArgs(argv) {
describe: 'Enable verbose debug logging.',
default: false,
})
.option('format', {
type: 'string',
describe: 'Output format for the generated changelog.',
choices: ['text', 'csv', 'json'],
default: 'text',
})
.help('help')
.alias('h', 'help')
.version(false)
Expand All @@ -61,6 +67,7 @@ function parseArgs(argv) {
const args = parser.scriptName('generate-changelog').parse();
const packageSpecs = [];
const debug = !!args.debug;
const format = args.format || 'text';
let summarizer = null;
if (args.codex && args.claude) {
throw new Error('Choose either --codex or --claude, not both.');
Expand Down Expand Up @@ -123,6 +130,7 @@ function parseArgs(argv) {

return {
debug,
format,
summarizer,
packageSpecs,
};
Expand Down Expand Up @@ -484,6 +492,22 @@ async function summarizePackageCommits({

function noopLogger() {}

function escapeCsvValue(value) {
if (value == null) {
return '';
}

const stringValue = String(value).replace(/\r?\n|\r/g, ' ');
if (stringValue.includes('"') || stringValue.includes(',')) {
return `"${stringValue.replace(/"/g, '""')}"`;
}
return stringValue;
}

function toCsvRow(values) {
return values.map(escapeCsvValue).join(',');
}

async function runSummarizer(command, prompt) {
const options = {cwd: repoRoot, maxBuffer: 5 * 1024 * 1024};

Expand Down Expand Up @@ -634,7 +658,9 @@ async function fetchPullRequestMetadata(prNumber, {log}) {
}

async function main() {
const {packageSpecs, summarizer, debug} = parseArgs(process.argv.slice(2));
const {packageSpecs, summarizer, debug, format} = parseArgs(
process.argv.slice(2)
);
const log = debug
? (...args) => console.log('[generate-changelog]', ...args)
: noopLogger;
Expand Down Expand Up @@ -754,60 +780,214 @@ async function main() {
log,
});

const outputLines = [];
const noChangesMessage = 'No changes since the last release.';
const changelogEntries = [];
for (let i = 0; i < packageSpecs.length; i++) {
const spec = packageSpecs[i];
outputLines.push(`## ${spec.name}@${spec.displayVersion || spec.version}`);
const versionText = spec.displayVersion || spec.version;
const commitsForPackage = commitsByPackage.get(spec.name) || [];
const entry = {
package: spec.name,
version: versionText,
hasChanges: commitsForPackage.length > 0,
commits: [],
note: null,
};

if (commitsForPackage.length === 0) {
outputLines.push('* No changes since the last release.');
outputLines.push('');
if (!entry.hasChanges) {
entry.note = noChangesMessage;
changelogEntries.push(entry);
continue;
}

commitsForPackage.forEach(commit => {
const summaryMap = summariesByPackage.get(spec.name) || new Map();
entry.commits = commitsForPackage.map(commit => {
if (commit.prNumber && prMetadata.has(commit.prNumber)) {
commit.authorLogin = prMetadata.get(commit.prNumber).authorLogin;
const metadata = prMetadata.get(commit.prNumber);
if (metadata && metadata.authorLogin) {
commit.authorLogin = metadata.authorLogin;
}
}

const prFragment = commit.prNumber
? `[#${commit.prNumber}](https://github.com/facebook/react/pull/${commit.prNumber})`
: `commit ${commit.sha.slice(0, 7)}`;
let summary = summaryMap.get(commit.sha) || commit.subject;
if (commit.prNumber) {
const prPattern = new RegExp(`\\s*\\(#${commit.prNumber}\\)$`);
summary = summary.replace(prPattern, '').trim();
}

let authorFragment = commit.authorLogin
? `[@${commit.authorLogin}](https://github.com/${commit.authorLogin})`
: commit.authorName || 'unknown author';
const prNumber = commit.prNumber || null;
const prUrl = prNumber
? `https://github.com/facebook/react/pull/${prNumber}`
: null;
const commitSha = commit.sha;
const commitUrl = `https://github.com/facebook/react/commit/${commitSha}`;

const authorLogin = commit.authorLogin || null;
const authorName = commit.authorName || null;
const authorEmail = commit.authorEmail || null;

let authorUrl = null;
let authorDisplay = authorName || 'unknown author';

if (authorLogin) {
authorUrl = `https://github.com/${authorLogin}`;
authorDisplay = `[@${authorLogin}](${authorUrl})`;
} else if (authorName && authorName.startsWith('@')) {
const username = authorName.slice(1);
authorUrl = `https://github.com/${username}`;
authorDisplay = `[@${username}](${authorUrl})`;
}

if (
!commit.authorLogin &&
commit.authorName &&
commit.authorName.startsWith('@')
) {
const username = commit.authorName.slice(1);
authorFragment = `[@${username}](https://github.com/${username})`;
const referenceDisplay = prNumber
? `[#${prNumber}](${prUrl})`
: `commit ${commitSha.slice(0, 7)}`;
const referenceType = prNumber ? 'pr' : 'commit';
const referenceId = prNumber ? `#${prNumber}` : commitSha.slice(0, 7);
const referenceUrl = prNumber ? prUrl : commitUrl;

return {
summary,
prNumber,
prUrl,
commitSha,
commitUrl,
authorLogin,
authorName,
authorEmail,
authorUrl,
authorDisplay,
referenceDisplay,
referenceType,
referenceId,
referenceUrl,
};
});

changelogEntries.push(entry);
}

log('Generated changelog sections.');
if (format === 'text') {
const outputLines = [];
for (let i = 0; i < changelogEntries.length; i++) {
const entry = changelogEntries[i];
outputLines.push(`## ${entry.package}@${entry.version}`);
if (!entry.hasChanges) {
outputLines.push(`* ${entry.note}`);
outputLines.push('');
continue;
}

const summaryMap = summariesByPackage.get(spec.name) || new Map();
let summary = summaryMap.get(commit.sha) || commit.subject;
entry.commits.forEach(commit => {
outputLines.push(
`* ${commit.summary} (${commit.referenceDisplay} by ${commit.authorDisplay})`
);
});
outputLines.push('');
}

if (commit.prNumber) {
const prPattern = new RegExp(`\\s*\\(#${commit.prNumber}\\)$`);
summary = summary.replace(prPattern, '').trim();
while (outputLines.length && outputLines[outputLines.length - 1] === '') {
outputLines.pop();
}

console.log(outputLines.join('\n'));
return;
}

if (format === 'csv') {
const header = [
'package',
'version',
'summary',
'reference_type',
'reference_id',
'reference_url',
'author_name',
'author_login',
'author_url',
'author_email',
'commit_sha',
'commit_url',
];
const rows = [header];
changelogEntries.forEach(entry => {
if (!entry.hasChanges) {
rows.push([
entry.package,
entry.version,
entry.note,
'',
'',
'',
'',
'',
'',
'',
'',
'',
]);
return;
}

outputLines.push(`* ${summary} (${prFragment} by ${authorFragment})`);
entry.commits.forEach(commit => {
const authorName =
commit.authorName ||
(commit.authorLogin ? `@${commit.authorLogin}` : 'unknown author');
rows.push([
entry.package,
entry.version,
commit.summary,
commit.referenceType,
commit.referenceId,
commit.referenceUrl,
authorName,
commit.authorLogin || '',
commit.authorUrl || '',
commit.authorEmail || '',
commit.commitSha,
commit.commitUrl,
]);
});
});

outputLines.push('');
const csvLines = rows.map(toCsvRow);
console.log(csvLines.join('\n'));
return;
}

while (outputLines.length && outputLines[outputLines.length - 1] === '') {
outputLines.pop();
if (format === 'json') {
const payload = changelogEntries.map(entry => ({
package: entry.package,
version: entry.version,
hasChanges: entry.hasChanges,
note: entry.hasChanges ? undefined : entry.note,
commits: entry.commits.map(commit => ({
summary: commit.summary,
prNumber: commit.prNumber,
prUrl: commit.prUrl,
commitSha: commit.commitSha,
commitUrl: commit.commitUrl,
author: {
login: commit.authorLogin,
name: commit.authorName,
email: commit.authorEmail,
url: commit.authorUrl,
display: commit.authorDisplay,
},
reference: {
type: commit.referenceType,
id: commit.referenceId,
url: commit.referenceUrl,
label: commit.referenceDisplay,
},
})),
}));

console.log(JSON.stringify(payload, null, 2));
return;
}

log('Generated changelog sections.');
console.log(outputLines.join('\n'));
throw new Error(`Unsupported format: ${format}`);
}

main().catch(error => {
Expand Down
Loading