Skip to content

Commit d718472

Browse files
mrdoobclaude
andcommitted
Improved changelog.js
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e7df696 commit d718472

1 file changed

Lines changed: 97 additions & 44 deletions

File tree

utils/changelog.js

Lines changed: 97 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ const categoryPaths = [
99
[ 'src/renderers/common', 'Renderer' ],
1010

1111
// Main sections
12+
[ 'utils/docs', 'Docs' ],
1213
[ 'docs', 'Docs' ],
1314
[ 'manual', 'Manual' ],
15+
[ 'devtools', 'Devtools' ],
1416
[ 'editor', 'Editor' ],
1517
[ 'test', 'Tests' ],
1618
[ 'playground', 'Playground' ],
@@ -34,11 +36,15 @@ const skipPatterns = [
3436
/^Update copyright year/i,
3537
/^Update \w+\.js\.?$/i, // Generic "Update File.js" commits
3638
/^Updated? docs\.?$/i,
37-
/^Update REVISION/i
39+
/^Update REVISION/i,
40+
/^r\d+(\s*\(bis\))*$/i
3841
];
3942

43+
// Authors to skip (bots)
44+
const skipAuthors = new Set( [ 'dependabot', 'app/renovate', 'renovate[bot]' ] );
45+
4046
// Categories that map to sections
41-
const sectionCategories = [ 'Docs', 'Manual', 'Examples', 'Editor', 'Tests', 'Utils', 'Build' ];
47+
const sectionCategories = [ 'Docs', 'Manual', 'Examples', 'Devtools', 'Editor', 'Tests', 'Utils', 'Build' ];
4248

4349
function exec( command ) {
4450

@@ -54,16 +60,10 @@ function exec( command ) {
5460

5561
}
5662

57-
function getLastTag() {
58-
59-
return exec( 'git describe --tags --abbrev=0' );
60-
61-
}
62-
63-
function getCommitsSinceTag( tag ) {
63+
function getCommitsBetweenTags( fromTag, toTag ) {
6464

65-
// Get commits since tag, oldest first, excluding merge commits
66-
const log = exec( `git log ${tag}..HEAD --no-merges --reverse --format="%H|%s|%an"` );
65+
// Get commits between tags (exclusive fromTag, inclusive toTag), oldest first, excluding merge commits
66+
const log = exec( `git log ${fromTag}..${toTag} --no-merges --reverse --format="%H|%s|%an"` );
6767

6868
if ( ! log ) return [];
6969

@@ -83,11 +83,18 @@ function getChangedFiles( hash ) {
8383

8484
}
8585

86-
function getCoAuthors( hash ) {
86+
function getCoAuthorsFromPR( prNumber ) {
87+
88+
const result = exec( `gh pr view ${prNumber} --json commits --jq '[.commits[].authors[].login] | unique | .[]' 2>/dev/null` );
89+
return result ? result.split( '\n' ).filter( Boolean ) : [];
90+
91+
}
92+
93+
function getCoAuthorsFromCommit( hash ) {
8794

8895
const body = exec( `git log -1 --format="%b" ${hash}` );
8996
const regex = /Co-authored-by:\s*([^<]+)\s*<[^>]+>/gi;
90-
return [ ...body.matchAll( regex ) ].map( m => m[ 1 ].trim() );
97+
return [ ...body.matchAll( regex ) ].map( m => normalizeAuthor( m[ 1 ].trim() ) );
9198

9299
}
93100

@@ -124,6 +131,9 @@ function categorizeFile( file ) {
124131

125132
if ( file.startsWith( 'src/' ) || isAddon ) {
126133

134+
// Skip barrel/index files
135+
if ( /\/Three(\.\w+)?\.js$/.test( file ) ) return { category: 'Global', isAddon: false };
136+
127137
const match = file.match( /\/([^/]+)\.js$/ );
128138
if ( match ) return { category: match[ 1 ], isAddon };
129139

@@ -152,7 +162,10 @@ function categorizeFile( file ) {
152162

153163
function categorizeCommit( files ) {
154164

165+
files = files.filter( f => ! f.startsWith( 'examples/screenshots/' ) );
166+
155167
const categoryCounts = {};
168+
const srcCategoryCounts = {};
156169
const sectionCounts = {};
157170
let hasAddon = false;
158171
let addonCategory = null;
@@ -167,7 +180,12 @@ function categorizeCommit( files ) {
167180
categoryCounts[ cat ] = ( categoryCounts[ cat ] || 0 ) + 1;
168181

169182
// Track src files vs addon files
170-
if ( file.startsWith( 'src/' ) ) srcCount ++;
183+
if ( file.startsWith( 'src/' ) ) {
184+
185+
srcCount ++;
186+
srcCategoryCounts[ cat ] = ( srcCategoryCounts[ cat ] || 0 ) + 1;
187+
188+
}
171189

172190
if ( result.isAddon ) {
173191

@@ -213,16 +231,20 @@ function categorizeCommit( files ) {
213231

214232
}
215233

216-
// Find the most common section (excluding Tests unless it's dominant)
234+
// If commit touches src/, treat as core change — category from src/ files only
235+
if ( srcCount > 0 ) {
236+
237+
const srcCategory = Object.entries( srcCategoryCounts ).sort( ( a, b ) => b[ 1 ] - a[ 1 ] )[ 0 ][ 0 ];
238+
return { category: srcCategory, isAddon: false, section: null };
239+
240+
}
241+
242+
// Find the most common section
217243
let maxSection = null;
218244
let maxSectionCount = 0;
219-
const totalFiles = files.length;
220245

221246
for ( const [ sec, count ] of Object.entries( sectionCounts ) ) {
222247

223-
// Only use Tests/Build section if it's the majority of files
224-
if ( ( sec === 'Tests' || sec === 'Build' ) && count < totalFiles * 0.5 ) continue;
225-
226248
if ( count > maxSectionCount ) {
227249

228250
maxSectionCount = count;
@@ -330,7 +352,7 @@ function addToGroup( groups, key, value ) {
330352

331353
}
332354

333-
function validateEnvironment() {
355+
function validateEnvironment( tag ) {
334356

335357
if ( ! exec( 'gh --version 2>/dev/null' ) ) {
336358

@@ -340,16 +362,38 @@ function validateEnvironment() {
340362

341363
}
342364

343-
const lastTag = getLastTag();
365+
if ( ! tag ) {
366+
367+
console.error( 'Usage: node utils/changelog.js <tag>' );
368+
console.error( 'Example: node utils/changelog.js r185' );
369+
process.exit( 1 );
370+
371+
}
372+
373+
// Verify the tag exists
374+
const resolved = exec( `git rev-parse --verify ${tag}` );
375+
376+
if ( ! resolved ) {
377+
378+
console.error( `Invalid tag: ${tag}` );
379+
process.exit( 1 );
380+
381+
}
382+
383+
// Get the previous tag
384+
const version = parseInt( tag.replace( 'r', '' ) );
385+
const previousTag = `r${version - 1}`;
344386

345-
if ( ! lastTag ) {
387+
const previousResolved = exec( `git rev-parse --verify ${previousTag}` );
346388

347-
console.error( 'No tags found in repository' );
389+
if ( ! previousResolved ) {
390+
391+
console.error( `Previous tag not found: ${previousTag}` );
348392
process.exit( 1 );
349393

350394
}
351395

352-
return lastTag;
396+
return { tag, previousTag, version };
353397

354398
}
355399

@@ -393,6 +437,9 @@ function processCommit( commit, revertedTitles ) {
393437

394438
if ( prInfo ) {
395439

440+
// Skip commits from bots
441+
if ( skipAuthors.has( prInfo.author ) ) return null;
442+
396443
author = prInfo.author;
397444
if ( prInfo.title ) subject = prInfo.title;
398445
if ( prInfo.files && prInfo.files.length > 0 ) files = prInfo.files;
@@ -405,19 +452,26 @@ function processCommit( commit, revertedTitles ) {
405452
if ( ! files ) files = getChangedFiles( commit.hash );
406453
if ( ! author ) author = commit.author;
407454

455+
// Skip commits from bots (check normalized name for git author fallback)
456+
if ( skipAuthors.has( normalizeAuthor( author ) ) ) return null;
457+
408458
const result = categorizeCommit( files );
409459
let { category, section } = result;
410460
const { isAddon } = result;
411461

412-
// Override category if title has a clear prefix
413-
const titleCategory = extractCategoryFromTitle( subject );
462+
// Use title prefix as category only if file-based didn't assign a section
463+
if ( ! section ) {
464+
465+
const titleCategory = extractCategoryFromTitle( subject );
414466

415-
if ( titleCategory ) {
467+
if ( titleCategory ) {
416468

417-
category = titleCategory;
418-
if ( category === 'Puppeteer' ) category = 'Tests';
419-
if ( category === 'Scripts' ) category = 'Utils';
420-
section = sectionCategories.includes( category ) ? category : null;
469+
category = titleCategory;
470+
if ( category === 'Scripts' ) category = 'Utils';
471+
if ( category === 'Puppeteer' || category === 'E2E' ) category = 'Tests';
472+
section = sectionCategories.includes( category ) ? category : null;
473+
474+
}
421475

422476
}
423477

@@ -428,7 +482,7 @@ function processCommit( commit, revertedTitles ) {
428482

429483
}
430484

431-
const coAuthors = getCoAuthors( commit.hash );
485+
const coAuthors = ( prNumber ? getCoAuthorsFromPR( prNumber ) : getCoAuthorsFromCommit( commit.hash ) ).filter( login => login !== author );
432486

433487
return {
434488
entry: {
@@ -445,15 +499,13 @@ function processCommit( commit, revertedTitles ) {
445499

446500
}
447501

448-
function formatOutput( lastTag, coreChanges, addonChanges, sections ) {
502+
function formatOutput( version, coreChanges, addonChanges, sections ) {
449503

450504
let output = '';
451505

452-
// Migration guide and milestone links
453-
const version = lastTag.replace( 'r', '' );
454-
const nextVersion = parseInt( version ) + 1;
455-
output += `https://github.com/mrdoob/three.js/wiki/Migration-Guide#${version}--${nextVersion}\n`;
456-
output += 'https://github.com/mrdoob/three.js/milestone/XX?closed=1\n\n';
506+
const previousVersion = version - 1;
507+
output += `https://github.com/mrdoob/three.js/wiki/Migration-Guide#${previousVersion}--${version}\n`;
508+
output += `https://github.com/mrdoob/three.js/milestone/${version - 87}?closed=1\n\n`;
457509

458510
// Core changes (Global first, then alphabetically)
459511
const sortedCore = Object.keys( coreChanges ).sort( ( a, b ) => {
@@ -477,7 +529,7 @@ function formatOutput( lastTag, coreChanges, addonChanges, sections ) {
477529
}
478530

479531
// Output sections in order
480-
const sectionOrder = [ 'Docs', 'Manual', 'Examples', 'Addons', 'Editor', 'Tests', 'Utils', 'Build' ];
532+
const sectionOrder = [ 'Docs', 'Manual', 'Examples', 'Addons', 'Devtools', 'Editor', 'Tests', 'Utils', 'Build' ];
481533

482534
for ( const sectionName of sectionOrder ) {
483535

@@ -530,15 +582,15 @@ function formatOutput( lastTag, coreChanges, addonChanges, sections ) {
530582

531583
function generateChangelog() {
532584

533-
const lastTag = validateEnvironment();
585+
const { tag, previousTag, version } = validateEnvironment( process.argv[ 2 ] );
534586

535-
console.error( `Generating changelog since ${lastTag}...\n` );
587+
console.error( `Generating changelog ${previousTag}..${tag}\n` );
536588

537-
const commits = getCommitsSinceTag( lastTag );
589+
const commits = getCommitsBetweenTags( previousTag, tag );
538590

539591
if ( commits.length === 0 ) {
540592

541-
console.error( 'No commits found since last tag' );
593+
console.error( `No commits found between ${previousTag} and ${tag}` );
542594
process.exit( 1 );
543595

544596
}
@@ -554,6 +606,7 @@ function generateChangelog() {
554606
Docs: [],
555607
Manual: [],
556608
Examples: [],
609+
Devtools: [],
557610
Editor: [],
558611
Tests: [],
559612
Utils: [],
@@ -608,7 +661,7 @@ function generateChangelog() {
608661

609662
}
610663

611-
console.log( formatOutput( lastTag, coreChanges, addonChanges, sections ) );
664+
console.log( formatOutput( version, coreChanges, addonChanges, sections ) );
612665

613666
}
614667

0 commit comments

Comments
 (0)