Skip to content

Commit 69f3e9d

Browse files
authored
[generate-changelog] Add --format option (facebook#34992)
Adds a new `--format` option which can be `text` (default), `csv`, or `json`. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34992). * facebook#34993 * __->__ facebook#34992
1 parent dd53a94 commit 69f3e9d

File tree

1 file changed

+213
-33
lines changed

1 file changed

+213
-33
lines changed

scripts/tasks/generate-changelog.js

Lines changed: 213 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const repoRoot = path.resolve(__dirname, '..', '..');
2323
function parseArgs(argv) {
2424
const parser = yargs(argv)
2525
.usage(
26-
'Usage: yarn generate-changelog [--codex|--claude] [--debug] [<pkg@version> ...]'
26+
'Usage: yarn generate-changelog [--codex|--claude] [--debug] [--format <text|csv|json>] [<pkg@version> ...]'
2727
)
2828
.example(
2929
'$0 --codex [email protected]',
@@ -50,6 +50,12 @@ function parseArgs(argv) {
5050
describe: 'Enable verbose debug logging.',
5151
default: false,
5252
})
53+
.option('format', {
54+
type: 'string',
55+
describe: 'Output format for the generated changelog.',
56+
choices: ['text', 'csv', 'json'],
57+
default: 'text',
58+
})
5359
.help('help')
5460
.alias('h', 'help')
5561
.version(false)
@@ -61,6 +67,7 @@ function parseArgs(argv) {
6167
const args = parser.scriptName('generate-changelog').parse();
6268
const packageSpecs = [];
6369
const debug = !!args.debug;
70+
const format = args.format || 'text';
6471
let summarizer = null;
6572
if (args.codex && args.claude) {
6673
throw new Error('Choose either --codex or --claude, not both.');
@@ -123,6 +130,7 @@ function parseArgs(argv) {
123130

124131
return {
125132
debug,
133+
format,
126134
summarizer,
127135
packageSpecs,
128136
};
@@ -484,6 +492,22 @@ async function summarizePackageCommits({
484492

485493
function noopLogger() {}
486494

495+
function escapeCsvValue(value) {
496+
if (value == null) {
497+
return '';
498+
}
499+
500+
const stringValue = String(value).replace(/\r?\n|\r/g, ' ');
501+
if (stringValue.includes('"') || stringValue.includes(',')) {
502+
return `"${stringValue.replace(/"/g, '""')}"`;
503+
}
504+
return stringValue;
505+
}
506+
507+
function toCsvRow(values) {
508+
return values.map(escapeCsvValue).join(',');
509+
}
510+
487511
async function runSummarizer(command, prompt) {
488512
const options = {cwd: repoRoot, maxBuffer: 5 * 1024 * 1024};
489513

@@ -634,7 +658,9 @@ async function fetchPullRequestMetadata(prNumber, {log}) {
634658
}
635659

636660
async function main() {
637-
const {packageSpecs, summarizer, debug} = parseArgs(process.argv.slice(2));
661+
const {packageSpecs, summarizer, debug, format} = parseArgs(
662+
process.argv.slice(2)
663+
);
638664
const log = debug
639665
? (...args) => console.log('[generate-changelog]', ...args)
640666
: noopLogger;
@@ -754,60 +780,214 @@ async function main() {
754780
log,
755781
});
756782

757-
const outputLines = [];
783+
const noChangesMessage = 'No changes since the last release.';
784+
const changelogEntries = [];
758785
for (let i = 0; i < packageSpecs.length; i++) {
759786
const spec = packageSpecs[i];
760-
outputLines.push(`## ${spec.name}@${spec.displayVersion || spec.version}`);
787+
const versionText = spec.displayVersion || spec.version;
761788
const commitsForPackage = commitsByPackage.get(spec.name) || [];
789+
const entry = {
790+
package: spec.name,
791+
version: versionText,
792+
hasChanges: commitsForPackage.length > 0,
793+
commits: [],
794+
note: null,
795+
};
762796

763-
if (commitsForPackage.length === 0) {
764-
outputLines.push('* No changes since the last release.');
765-
outputLines.push('');
797+
if (!entry.hasChanges) {
798+
entry.note = noChangesMessage;
799+
changelogEntries.push(entry);
766800
continue;
767801
}
768802

769-
commitsForPackage.forEach(commit => {
803+
const summaryMap = summariesByPackage.get(spec.name) || new Map();
804+
entry.commits = commitsForPackage.map(commit => {
770805
if (commit.prNumber && prMetadata.has(commit.prNumber)) {
771-
commit.authorLogin = prMetadata.get(commit.prNumber).authorLogin;
806+
const metadata = prMetadata.get(commit.prNumber);
807+
if (metadata && metadata.authorLogin) {
808+
commit.authorLogin = metadata.authorLogin;
809+
}
772810
}
773811

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

778-
let authorFragment = commit.authorLogin
779-
? `[@${commit.authorLogin}](https://github.com/${commit.authorLogin})`
780-
: commit.authorName || 'unknown author';
818+
const prNumber = commit.prNumber || null;
819+
const prUrl = prNumber
820+
? `https://github.com/facebook/react/pull/${prNumber}`
821+
: null;
822+
const commitSha = commit.sha;
823+
const commitUrl = `https://github.com/facebook/react/commit/${commitSha}`;
824+
825+
const authorLogin = commit.authorLogin || null;
826+
const authorName = commit.authorName || null;
827+
const authorEmail = commit.authorEmail || null;
828+
829+
let authorUrl = null;
830+
let authorDisplay = authorName || 'unknown author';
831+
832+
if (authorLogin) {
833+
authorUrl = `https://github.com/${authorLogin}`;
834+
authorDisplay = `[@${authorLogin}](${authorUrl})`;
835+
} else if (authorName && authorName.startsWith('@')) {
836+
const username = authorName.slice(1);
837+
authorUrl = `https://github.com/${username}`;
838+
authorDisplay = `[@${username}](${authorUrl})`;
839+
}
781840

782-
if (
783-
!commit.authorLogin &&
784-
commit.authorName &&
785-
commit.authorName.startsWith('@')
786-
) {
787-
const username = commit.authorName.slice(1);
788-
authorFragment = `[@${username}](https://github.com/${username})`;
841+
const referenceDisplay = prNumber
842+
? `[#${prNumber}](${prUrl})`
843+
: `commit ${commitSha.slice(0, 7)}`;
844+
const referenceType = prNumber ? 'pr' : 'commit';
845+
const referenceId = prNumber ? `#${prNumber}` : commitSha.slice(0, 7);
846+
const referenceUrl = prNumber ? prUrl : commitUrl;
847+
848+
return {
849+
summary,
850+
prNumber,
851+
prUrl,
852+
commitSha,
853+
commitUrl,
854+
authorLogin,
855+
authorName,
856+
authorEmail,
857+
authorUrl,
858+
authorDisplay,
859+
referenceDisplay,
860+
referenceType,
861+
referenceId,
862+
referenceUrl,
863+
};
864+
});
865+
866+
changelogEntries.push(entry);
867+
}
868+
869+
log('Generated changelog sections.');
870+
if (format === 'text') {
871+
const outputLines = [];
872+
for (let i = 0; i < changelogEntries.length; i++) {
873+
const entry = changelogEntries[i];
874+
outputLines.push(`## ${entry.package}@${entry.version}`);
875+
if (!entry.hasChanges) {
876+
outputLines.push(`* ${entry.note}`);
877+
outputLines.push('');
878+
continue;
789879
}
790880

791-
const summaryMap = summariesByPackage.get(spec.name) || new Map();
792-
let summary = summaryMap.get(commit.sha) || commit.subject;
881+
entry.commits.forEach(commit => {
882+
outputLines.push(
883+
`* ${commit.summary} (${commit.referenceDisplay} by ${commit.authorDisplay})`
884+
);
885+
});
886+
outputLines.push('');
887+
}
793888

794-
if (commit.prNumber) {
795-
const prPattern = new RegExp(`\\s*\\(#${commit.prNumber}\\)$`);
796-
summary = summary.replace(prPattern, '').trim();
889+
while (outputLines.length && outputLines[outputLines.length - 1] === '') {
890+
outputLines.pop();
891+
}
892+
893+
console.log(outputLines.join('\n'));
894+
return;
895+
}
896+
897+
if (format === 'csv') {
898+
const header = [
899+
'package',
900+
'version',
901+
'summary',
902+
'reference_type',
903+
'reference_id',
904+
'reference_url',
905+
'author_name',
906+
'author_login',
907+
'author_url',
908+
'author_email',
909+
'commit_sha',
910+
'commit_url',
911+
];
912+
const rows = [header];
913+
changelogEntries.forEach(entry => {
914+
if (!entry.hasChanges) {
915+
rows.push([
916+
entry.package,
917+
entry.version,
918+
entry.note,
919+
'',
920+
'',
921+
'',
922+
'',
923+
'',
924+
'',
925+
'',
926+
'',
927+
'',
928+
]);
929+
return;
797930
}
798931

799-
outputLines.push(`* ${summary} (${prFragment} by ${authorFragment})`);
932+
entry.commits.forEach(commit => {
933+
const authorName =
934+
commit.authorName ||
935+
(commit.authorLogin ? `@${commit.authorLogin}` : 'unknown author');
936+
rows.push([
937+
entry.package,
938+
entry.version,
939+
commit.summary,
940+
commit.referenceType,
941+
commit.referenceId,
942+
commit.referenceUrl,
943+
authorName,
944+
commit.authorLogin || '',
945+
commit.authorUrl || '',
946+
commit.authorEmail || '',
947+
commit.commitSha,
948+
commit.commitUrl,
949+
]);
950+
});
800951
});
801952

802-
outputLines.push('');
953+
const csvLines = rows.map(toCsvRow);
954+
console.log(csvLines.join('\n'));
955+
return;
803956
}
804957

805-
while (outputLines.length && outputLines[outputLines.length - 1] === '') {
806-
outputLines.pop();
958+
if (format === 'json') {
959+
const payload = changelogEntries.map(entry => ({
960+
package: entry.package,
961+
version: entry.version,
962+
hasChanges: entry.hasChanges,
963+
note: entry.hasChanges ? undefined : entry.note,
964+
commits: entry.commits.map(commit => ({
965+
summary: commit.summary,
966+
prNumber: commit.prNumber,
967+
prUrl: commit.prUrl,
968+
commitSha: commit.commitSha,
969+
commitUrl: commit.commitUrl,
970+
author: {
971+
login: commit.authorLogin,
972+
name: commit.authorName,
973+
email: commit.authorEmail,
974+
url: commit.authorUrl,
975+
display: commit.authorDisplay,
976+
},
977+
reference: {
978+
type: commit.referenceType,
979+
id: commit.referenceId,
980+
url: commit.referenceUrl,
981+
label: commit.referenceDisplay,
982+
},
983+
})),
984+
}));
985+
986+
console.log(JSON.stringify(payload, null, 2));
987+
return;
807988
}
808989

809-
log('Generated changelog sections.');
810-
console.log(outputLines.join('\n'));
990+
throw new Error(`Unsupported format: ${format}`);
811991
}
812992

813993
main().catch(error => {

0 commit comments

Comments
 (0)