@ckeditor/ckeditor5-dev-release-tools 32.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/LICENSE.md +16 -0
  2. package/README.md +89 -0
  3. package/lib/index.js +28 -0
  4. package/lib/tasks/bumpversions.js +354 -0
  5. package/lib/tasks/generatechangelogformonorepository.js +723 -0
  6. package/lib/tasks/generatechangelogforsinglepackage.js +202 -0
  7. package/lib/tasks/releasesubrepositories.js +929 -0
  8. package/lib/tasks/updateckeditor5dependencies.js +392 -0
  9. package/lib/templates/commit.hbs +29 -0
  10. package/lib/templates/footer.hbs +10 -0
  11. package/lib/templates/header.hbs +23 -0
  12. package/lib/templates/release-package.json +12 -0
  13. package/lib/templates/template.hbs +37 -0
  14. package/lib/utils/changelog.js +67 -0
  15. package/lib/utils/cli.js +324 -0
  16. package/lib/utils/creategithubrelease.js +35 -0
  17. package/lib/utils/displaycommits.js +105 -0
  18. package/lib/utils/displayskippedpackages.js +32 -0
  19. package/lib/utils/executeonpackages.js +26 -0
  20. package/lib/utils/generatechangelog.js +121 -0
  21. package/lib/utils/getchangedfilesforcommit.js +33 -0
  22. package/lib/utils/getcommits.js +104 -0
  23. package/lib/utils/getnewversiontype.js +53 -0
  24. package/lib/utils/getpackagejson.js +24 -0
  25. package/lib/utils/getpackagespaths.js +90 -0
  26. package/lib/utils/getpackagestorelease.js +152 -0
  27. package/lib/utils/getwriteroptions.js +33 -0
  28. package/lib/utils/parseroptions.js +26 -0
  29. package/lib/utils/transformcommitfactory.js +492 -0
  30. package/lib/utils/transformcommitutils.js +163 -0
  31. package/lib/utils/updatedependenciesversions.js +32 -0
  32. package/lib/utils/validatepackagetorelease.js +54 -0
  33. package/lib/utils/versions.js +59 -0
  34. package/package.json +52 -0
@@ -0,0 +1,723 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md.
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const fs = require( 'fs' );
9
+ const path = require( 'path' );
10
+ const { tools, logger } = require( '@ckeditor/ckeditor5-dev-utils' );
11
+ const compareFunc = require( 'compare-func' );
12
+ const chalk = require( 'chalk' );
13
+ const semver = require( 'semver' );
14
+ const changelogUtils = require( '../utils/changelog' );
15
+ const cli = require( '../utils/cli' );
16
+ const displayCommits = require( '../utils/displaycommits' );
17
+ const displaySkippedPackages = require( '../utils/displayskippedpackages' );
18
+ const generateChangelog = require( '../utils/generatechangelog' );
19
+ const getPackageJson = require( '../utils/getpackagejson' );
20
+ const getPackagesPaths = require( '../utils/getpackagespaths' );
21
+ const getCommits = require( '../utils/getcommits' );
22
+ const getNewVersionType = require( '../utils/getnewversiontype' );
23
+ const getWriterOptions = require( '../utils/getwriteroptions' );
24
+ const { getRepositoryUrl } = require( '../utils/transformcommitutils' );
25
+ const transformCommitFactory = require( '../utils/transformcommitfactory' );
26
+
27
+ const VERSIONING_POLICY_URL = 'https://ckeditor.com/docs/ckeditor5/latest/framework/guides/support/versioning-policy.html';
28
+ const noteInfo = `[ℹ️](${ VERSIONING_POLICY_URL }#major-and-minor-breaking-changes)`;
29
+
30
+ /**
31
+ * Generates the single changelog for the mono repository. It means that changes which have been done in all packages
32
+ * will be described in the changelog file located in the `options.cwd` directory.
33
+ *
34
+ * The typed version will be the same for all packages. See: https://github.com/ckeditor/ckeditor5/issues/7323.
35
+ *
36
+ * @param {Object} options
37
+ *
38
+ * @param {String} options.cwd Current working directory (packages) from which all paths will be resolved.
39
+ *
40
+ * @param {String} options.packages Where to look for packages.
41
+ *
42
+ * @param {Function} options.transformScope A function that returns a URL to a package from a scope of a commit.
43
+ *
44
+ * @param {String} [options.scope] Package names have to match to specified glob pattern in order to be processed.
45
+ *
46
+ * @param {Array.<String>} [options.skipPackages=[]] Name of packages which won't be touched.
47
+ *
48
+ * @param {Boolean} [options.skipLinks=false] If set on true, links to release or commits will be omitted.
49
+ *
50
+ * @param {String} [options.from] A commit or tag name that will be the first param of the range of commits to collect.
51
+ *
52
+ * @param {Boolean} [options.highlightsPlaceholder=false] Whether to add a note about release highlights.
53
+ *
54
+ * @param {Boolean} [options.collaborationFeatures=false] Whether to add a note about collaboration features.
55
+ *
56
+ * @param {String} [options.releaseBranch='master'] A name of the branch that should be used for releasing packages.
57
+ *
58
+ * @param {Array.<ExternalRepository>} [options.externalRepositories=[]] An array of object with additional repositories
59
+ * that the function takes into consideration while gathering commits. It assumes that those directories are also mono repositories.
60
+ *
61
+ * @returns {Promise}
62
+ */
63
+ module.exports = async function generateChangelogForMonoRepository( options ) {
64
+ const log = logger();
65
+ const cwd = process.cwd();
66
+ const pkgJson = getPackageJson( options.cwd );
67
+
68
+ logProcess( 'Collecting paths to packages...' );
69
+
70
+ const pathsCollection = gatherAllPackagesPaths( {
71
+ cwd: options.cwd,
72
+ packages: options.packages,
73
+ scope: options.scope || null,
74
+ skipPackages: options.skipPackages || [],
75
+ externalRepositories: options.externalRepositories || []
76
+ } );
77
+
78
+ logProcess( 'Collecting all commits since the last release...' );
79
+
80
+ // Collection of all entries (real commits + additional "fake" commits extracted from descriptions).
81
+ let allCommits;
82
+
83
+ // Collection of public entries that will be inserted in the changelog.
84
+ let publicCommits;
85
+
86
+ // The next version for the upcoming release.
87
+ let nextVersion = null;
88
+
89
+ // A map contains packages and their new versions.
90
+ const packagesVersion = new Map();
91
+
92
+ // A map contains packages and their current versions.
93
+ const currentPackagesVersion = new Map();
94
+
95
+ // A map contains packages and their paths (where they are located)
96
+ const packagesPaths = new Map();
97
+
98
+ const commitOptions = {
99
+ cwd: options.cwd,
100
+ from: options.from ? options.from : 'v' + pkgJson.version,
101
+ releaseBranch: options.releaseBranch || 'master',
102
+ externalRepositories: options.externalRepositories || []
103
+ };
104
+
105
+ return gatherAllCommits( commitOptions )
106
+ .then( commits => {
107
+ allCommits = commits;
108
+
109
+ logInfo( `Found ${ commits.length } entries to parse.`, { indentLevel: 1, startWithNewLine: true } );
110
+ } )
111
+ .then( () => typeNewVersionForAllPackages() )
112
+ .then( () => generateChangelogFromCommits() )
113
+ .then( changesFromCommits => saveChangelog( changesFromCommits ) )
114
+ .then( () => {
115
+ logProcess( 'Summary' );
116
+
117
+ displaySkippedPackages( new Set( [
118
+ ...pathsCollection.skipped
119
+ ].sort() ) );
120
+
121
+ // Make a commit from the repository where we started.
122
+ process.chdir( options.cwd );
123
+ tools.shExec( `git add ${ changelogUtils.changelogFile }`, { verbosity: 'error' } );
124
+ tools.shExec( 'git commit -m "Docs: Changelog. [skip ci]"', { verbosity: 'error' } );
125
+
126
+ logInfo(
127
+ `Changelog for "${ chalk.underline( pkgJson.name ) }" (v${ packagesVersion.get( pkgJson.name ) }) has been generated.`,
128
+ { indentLevel: 1 }
129
+ );
130
+
131
+ process.chdir( cwd );
132
+ } )
133
+ .catch( err => {
134
+ console.log( err );
135
+ } );
136
+
137
+ /**
138
+ * Returns collections with packages found in the `options.cwd` directory and the external repositories.
139
+ *
140
+ * @param {Object} options
141
+ * @param {String} options.cwd Current working directory (packages) from which all paths will be resolved.
142
+ * @param {String} options.packages Where to look for packages.
143
+ * @param {String} options.scope Package names have to match to specified glob pattern in order to be processed.
144
+ * @param {Array.<String>} options.skipPackages Name of packages which won't be touched.
145
+ * @param {Array.<ExternalRepository>} options.externalRepositories An array of object with additional repositories
146
+ * that the function takes into consideration while gathering packages.
147
+ * @returns {PathsCollection}
148
+ */
149
+ function gatherAllPackagesPaths( options ) {
150
+ logInfo( `Processing "${ options.cwd }"...`, { indentLevel: 1 } );
151
+
152
+ const pathsCollection = getPackagesPaths( {
153
+ cwd: options.cwd,
154
+ packages: options.packages,
155
+ scope: options.scope,
156
+ skipPackages: options.skipPackages,
157
+ skipMainRepository: true
158
+ } );
159
+
160
+ for ( const externalRepository of options.externalRepositories ) {
161
+ logInfo( `Processing "${ externalRepository.cwd }"...`, { indentLevel: 1 } );
162
+
163
+ const externalPackages = getPackagesPaths( {
164
+ cwd: externalRepository.cwd,
165
+ packages: externalRepository.packages,
166
+ scope: externalRepository.scope || null,
167
+ skipPackages: externalRepository.skipPackages || [],
168
+ skipMainRepository: true
169
+ } );
170
+
171
+ // The main package in an external repository is a private package.
172
+ externalPackages.skipped.delete( externalRepository.cwd );
173
+
174
+ // Merge results with the object that will be returned.
175
+ [ ...externalPackages.matched ].forEach( item => pathsCollection.matched.add( item ) );
176
+ [ ...externalPackages.skipped ].forEach( item => pathsCollection.skipped.add( item ) );
177
+ }
178
+
179
+ // The main repository should be at the end of the list.
180
+ pathsCollection.skipped.delete( options.cwd );
181
+ pathsCollection.matched.add( options.cwd );
182
+
183
+ return pathsCollection;
184
+ }
185
+
186
+ /**
187
+ * Returns a promise that resolves an array of commits since the last tag specified as `options.from`.
188
+ *
189
+ * @param {Object} options
190
+ * @param {String} options.cwd Current working directory (packages) from which all paths will be resolved.
191
+ * @param {String} options.from A commit or tag name that will be the first param of the range of commits to collect.
192
+ * @param {String} options.releaseBranch A name of the branch that should be used for releasing packages.
193
+ * @param {Array.<ExternalRepository>} options.externalRepositories An array of object with additional repositories
194
+ * that the function takes into consideration while gathering commits.
195
+ * @returns {Promise.<Array.<Commit>>}
196
+ */
197
+ function gatherAllCommits( options ) {
198
+ logInfo( `Processing "${ options.cwd }"...`, { indentLevel: 1 } );
199
+
200
+ const transformCommit = transformCommitFactory( {
201
+ useExplicitBreakingChangeGroups: true
202
+ } );
203
+
204
+ const commitOptions = {
205
+ from: options.from,
206
+ releaseBranch: options.releaseBranch
207
+ };
208
+
209
+ let promise = getCommits( transformCommit, commitOptions )
210
+ .then( commits => {
211
+ logInfo( `Found ${ commits.length } entries in "${ options.cwd }".`, { indentLevel: 1 } );
212
+
213
+ return commits;
214
+ } );
215
+
216
+ for ( const externalRepository of options.externalRepositories ) {
217
+ promise = promise.then( commits => {
218
+ logInfo( `Processing "${ externalRepository.cwd }"...`, { indentLevel: 1, startWithNewLine: true } );
219
+ process.chdir( externalRepository.cwd );
220
+
221
+ const commitOptions = {
222
+ from: externalRepository.from || options.from,
223
+ releaseBranch: externalRepository.releaseBranch || options.releaseBranch
224
+ };
225
+
226
+ return getCommits( transformCommit, commitOptions )
227
+ .then( newCommits => {
228
+ logInfo( `Found ${ newCommits.length } entries in "${ externalRepository.cwd }".`, { indentLevel: 1 } );
229
+
230
+ for ( const singleCommit of newCommits ) {
231
+ singleCommit.skipCommitsLink = externalRepository.skipLinks || false;
232
+ }
233
+
234
+ // Merge arrays with the commits.
235
+ return [].concat( commits, newCommits );
236
+ } );
237
+ } );
238
+ }
239
+
240
+ return promise.then( commits => {
241
+ process.chdir( options.cwd );
242
+
243
+ return commits;
244
+ } );
245
+ }
246
+
247
+ /**
248
+ * Asks the user about the new version for all packages for the upcoming release.
249
+ *
250
+ * @returns {Promise}
251
+ */
252
+ function typeNewVersionForAllPackages() {
253
+ logProcess( 'Determining the new version...' );
254
+
255
+ displayAllChanges();
256
+
257
+ // Find the highest version in all packages.
258
+ const [ packageHighestVersion, highestVersion ] = [ ...pathsCollection.matched ]
259
+ .reduce( ( currentHighest, repositoryPath ) => {
260
+ const packageJson = getPackageJson( repositoryPath );
261
+
262
+ currentPackagesVersion.set( packageJson.name, packageJson.version );
263
+
264
+ if ( semver.gt( packageJson.version, currentHighest[ 1 ] ) ) {
265
+ return [ packageJson.name, packageJson.version ];
266
+ }
267
+
268
+ return currentHighest;
269
+ }, [ null, '0.0.0' ] );
270
+
271
+ let bumpType = getNewVersionType( allCommits );
272
+
273
+ // When made commits are not public, bump the `patch` version.
274
+ if ( bumpType === 'internal' ) {
275
+ bumpType = 'patch';
276
+ }
277
+
278
+ return cli.provideNewVersionForMonoRepository( highestVersion, packageHighestVersion, bumpType, { indentLevel: 1 } )
279
+ .then( version => {
280
+ nextVersion = version;
281
+
282
+ let promise = Promise.resolve();
283
+
284
+ // Update the version for all packages.
285
+ for ( const packagePath of pathsCollection.matched ) {
286
+ promise = promise.then( () => {
287
+ const pkgJson = getPackageJson( packagePath );
288
+
289
+ packagesPaths.set( pkgJson.name, packagePath );
290
+ packagesVersion.set( pkgJson.name, nextVersion );
291
+ } );
292
+ }
293
+
294
+ return promise;
295
+ } );
296
+ }
297
+
298
+ /**
299
+ * Displays breaking changes and commits.
300
+ */
301
+ function displayAllChanges() {
302
+ const majorBreakingChangesCommits = filterCommitsByNoteTitle( allCommits, 'MAJOR BREAKING CHANGES' );
303
+ const infoOptions = { indentLevel: 1, startWithNewLine: true };
304
+
305
+ if ( majorBreakingChangesCommits.length > 0 ) {
306
+ logInfo( `🔸 Found ${ chalk.bold( 'MAJOR BREAKING CHANGES' ) }:`, infoOptions );
307
+ displayCommits( majorBreakingChangesCommits, { attachLinkToCommit: true, indentLevel: 2 } );
308
+ } else {
309
+ logInfo( `🔸 ${ chalk.bold( 'MAJOR BREAKING CHANGES' ) } commits have not been found.`, infoOptions );
310
+ }
311
+
312
+ const minorBreakingChangesCommits = filterCommitsByNoteTitle( allCommits, 'MINOR BREAKING CHANGES' );
313
+
314
+ if ( minorBreakingChangesCommits.length > 0 ) {
315
+ logInfo( `🔸 Found ${ chalk.bold( 'MINOR BREAKING CHANGES' ) }:`, infoOptions );
316
+ displayCommits( minorBreakingChangesCommits, { attachLinkToCommit: true, indentLevel: 2 } );
317
+ } else {
318
+ logInfo( `🔸 ${ chalk.bold( 'MINOR BREAKING CHANGES' ) } commits have not been found.`, infoOptions );
319
+ }
320
+
321
+ logInfo( '🔸 Commits since the last release:', infoOptions );
322
+
323
+ const commits = allCommits.sort( sortFunctionFactory( 'scope' ) );
324
+
325
+ displayCommits( commits, { indentLevel: 2 } );
326
+
327
+ logInfo( '💡 Review commits listed above and propose the new version for all packages in the upcoming release.', infoOptions );
328
+ }
329
+
330
+ /**
331
+ * Finds commits that contain a note which matches to `titleNote`.
332
+ *
333
+ * @returns {Array.<Commit>}
334
+ */
335
+ function filterCommitsByNoteTitle( commits, titleNote ) {
336
+ return commits.filter( commit => {
337
+ if ( !commit.isPublicCommit ) {
338
+ return false;
339
+ }
340
+
341
+ for ( const note of commit.notes ) {
342
+ if ( note.title.startsWith( titleNote ) ) {
343
+ return true;
344
+ }
345
+ }
346
+
347
+ return false;
348
+ } );
349
+ }
350
+
351
+ /**
352
+ * Finds commits that touched the package under `packagePath` directory.
353
+ *
354
+ * @param {Array.<Commit>} commits
355
+ * @param {String} packagePath
356
+ * @returns {Array.<Commit>}
357
+ */
358
+ function filterCommitsByPath( commits, packagePath ) {
359
+ const shortPackagePath = packagePath.replace( options.cwd, '' )
360
+ .replace( new RegExp( `^\\${ path.sep }` ), '' );
361
+
362
+ return commits.filter( commit => {
363
+ return commit.files.some( file => {
364
+ // The main repository.
365
+ if ( shortPackagePath === '' ) {
366
+ return !file.startsWith( 'packages' );
367
+ }
368
+
369
+ return file.startsWith( shortPackagePath );
370
+ } );
371
+ } );
372
+ }
373
+
374
+ /**
375
+ * Generates a list of changes based on the commits in the main repository.
376
+ *
377
+ * @returns {Promise.<String>}
378
+ */
379
+ function generateChangelogFromCommits() {
380
+ logProcess( 'Generating the changelog...' );
381
+
382
+ const version = packagesVersion.get( pkgJson.name );
383
+
384
+ const writerContext = {
385
+ version,
386
+ commit: 'commit',
387
+ repoUrl: getRepositoryUrl( options.cwd ),
388
+ currentTag: 'v' + version,
389
+ previousTag: 'v' + pkgJson.version,
390
+ isPatch: semver.diff( version, pkgJson.version ) === 'patch',
391
+ highlightsPlaceholder: options.highlightsPlaceholder || false,
392
+ collaborationFeatures: options.collaborationFeatures || false,
393
+ skipCommitsLink: Boolean( options.skipLinks ),
394
+ skipCompareLink: Boolean( options.skipLinks )
395
+ };
396
+
397
+ const writerOptions = getWriterOptions( {
398
+ // We do not allow modifying the commit hash value by the generator itself.
399
+ hash: hash => hash
400
+ } );
401
+
402
+ writerOptions.commitsSort = sortFunctionFactory( 'rawScope' );
403
+ writerOptions.notesSort = sortFunctionFactory( 'rawScope' );
404
+
405
+ publicCommits = [ ...allCommits ]
406
+ .filter( commit => commit.isPublicCommit )
407
+ .map( commit => {
408
+ commit.rawScope = commit.scope;
409
+
410
+ // Transforms a scope to markdown link.
411
+ if ( Array.isArray( commit.scope ) ) {
412
+ commit.scope = commit.scope.map( scopeToLink );
413
+ }
414
+
415
+ // Attaches an icon to notes.
416
+ commit.notes = commit.notes.map( note => {
417
+ note.title += ' ' + noteInfo;
418
+ note.rawScope = note.scope;
419
+
420
+ // Transforms a scope to markdown link.
421
+ if ( Array.isArray( note.scope ) ) {
422
+ note.scope = note.scope.map( scopeToLink );
423
+ }
424
+
425
+ return note;
426
+ } );
427
+
428
+ return commit;
429
+ } );
430
+
431
+ return generateChangelog( publicCommits, writerContext, writerOptions )
432
+ .then( changes => {
433
+ logInfo( 'Changes based on commits have been generated.', { indentLevel: 1 } );
434
+
435
+ return Promise.resolve( changes );
436
+ } );
437
+
438
+ function scopeToLink( name ) {
439
+ return `[${ name }](${ options.transformScope( name ) })`;
440
+ }
441
+ }
442
+
443
+ /**
444
+ * Combines the generated changes based on commits and summary of version changes in packages.
445
+ * Appends those changes at the beginning of the changelog file.
446
+ *
447
+ * @param {String} changesFromCommits Generated entries based on commits.
448
+ */
449
+ function saveChangelog( changesFromCommits ) {
450
+ logProcess( 'Saving changelog...' );
451
+
452
+ if ( !fs.existsSync( changelogUtils.changelogFile ) ) {
453
+ logInfo( 'Changelog file does not exist. Creating...', { isWarning: true, indentLevel: 1 } );
454
+
455
+ changelogUtils.saveChangelog( changelogUtils.changelogHeader );
456
+ }
457
+
458
+ logInfo( 'Preparing a summary of version changes in packages.', { indentLevel: 1 } );
459
+
460
+ const dependenciesSummary = generateSummaryOfChangesInPackages();
461
+
462
+ let currentChangelog = changelogUtils.getChangelog();
463
+
464
+ // Remove header from current changelog.
465
+ currentChangelog = currentChangelog.replace( changelogUtils.changelogHeader, '' ).trim();
466
+
467
+ // Concat header, new entries and old changelog to single string.
468
+ let newChangelog = changelogUtils.changelogHeader +
469
+ changesFromCommits.trim() +
470
+ '\n\n' +
471
+ dependenciesSummary +
472
+ '\n\n\n' +
473
+ currentChangelog;
474
+
475
+ newChangelog = newChangelog.trim() + '\n';
476
+
477
+ // Save the changelog.
478
+ changelogUtils.saveChangelog( newChangelog );
479
+
480
+ logInfo( 'Saved.', { indentLevel: 1 } );
481
+ }
482
+
483
+ /**
484
+ * Prepares a summary that describes what has changed in all dependencies.
485
+ *
486
+ * @returns {String}
487
+ */
488
+ function generateSummaryOfChangesInPackages() {
489
+ const dependencies = new Map();
490
+
491
+ for ( const [ packageName, nextVersion ] of packagesVersion ) {
492
+ // Skip the package hosted in the main repository.
493
+ if ( packageName === pkgJson.name ) {
494
+ continue;
495
+ }
496
+
497
+ dependencies.set( packageName, {
498
+ next: nextVersion,
499
+ current: currentPackagesVersion.get( packageName )
500
+ } );
501
+ }
502
+
503
+ const newPackages = getNewPackages( dependencies );
504
+ const majorBreakingChangesPackages = getPackagesMatchedToScopesFromNotes( dependencies, 'MAJOR BREAKING CHANGES' );
505
+ const minorBreakingChangesPackages = getPackagesMatchedToScopesFromNotes( dependencies, 'MINOR BREAKING CHANGES' );
506
+ const newFeaturesPackages = getPackagesWithNewFeatures( dependencies );
507
+
508
+ const entries = [
509
+ '### Released packages\n',
510
+ `Check out the [Versioning policy](${ VERSIONING_POLICY_URL }) guide for more information.\n`,
511
+ '<details>',
512
+ '<summary>Released packages (summary)</summary>'
513
+ ];
514
+
515
+ if ( newPackages.size ) {
516
+ entries.push( '\nNew packages:\n' );
517
+
518
+ for ( const [ packageName, version ] of [ ...newPackages ].sort( sortByPackageName ) ) {
519
+ entries.push( formatChangelogEntry( packageName, version.next ) );
520
+ }
521
+ }
522
+
523
+ if ( majorBreakingChangesPackages.size ) {
524
+ entries.push( '\nMajor releases (contain major breaking changes):\n' );
525
+
526
+ for ( const [ packageName, version ] of [ ...majorBreakingChangesPackages ].sort( sortByPackageName ) ) {
527
+ entries.push( formatChangelogEntry( packageName, version.next, version.current ) );
528
+ }
529
+ }
530
+
531
+ if ( minorBreakingChangesPackages.size ) {
532
+ entries.push( '\nMinor releases (contain minor breaking changes):\n' );
533
+
534
+ for ( const [ packageName, version ] of [ ...minorBreakingChangesPackages ].sort( sortByPackageName ) ) {
535
+ entries.push( formatChangelogEntry( packageName, version.next, version.current ) );
536
+ }
537
+ }
538
+
539
+ if ( newFeaturesPackages.size ) {
540
+ entries.push( '\nReleases containing new features:\n' );
541
+
542
+ for ( const [ packageName, version ] of [ ...newFeaturesPackages ].sort( sortByPackageName ) ) {
543
+ entries.push( formatChangelogEntry( packageName, version.next, version.current ) );
544
+ }
545
+ }
546
+
547
+ if ( dependencies.size ) {
548
+ entries.push( '\nOther releases:\n' );
549
+
550
+ for ( const [ packageName, version ] of [ ...dependencies ].sort( sortByPackageName ) ) {
551
+ entries.push( formatChangelogEntry( packageName, version.next, version.current ) );
552
+ }
553
+ }
554
+
555
+ entries.push( '</details>' );
556
+
557
+ return entries.join( '\n' ).trim();
558
+
559
+ function sortByPackageName( a, b ) {
560
+ return a[ 0 ] > b[ 0 ] ? 1 : -1;
561
+ }
562
+ }
563
+
564
+ /**
565
+ * @param {Map.<String, Version>} dependencies
566
+ * @returns {Map.<String, Version>}
567
+ */
568
+ function getNewPackages( dependencies ) {
569
+ const packages = new Map();
570
+
571
+ for ( const [ packageName, version ] of dependencies ) {
572
+ if ( semver.eq( version.current, '0.0.1' ) ) {
573
+ packages.set( packageName, version );
574
+ dependencies.delete( packageName );
575
+ }
576
+ }
577
+
578
+ return packages;
579
+ }
580
+
581
+ /**
582
+ * Returns packages where scope of changes described in the commits' notes match to packages' names.
583
+ *
584
+ * @param {Map.<String, Version>} dependencies
585
+ * @param {String} noteTitle
586
+ * @returns {Map.<String, Version>}
587
+ */
588
+ function getPackagesMatchedToScopesFromNotes( dependencies, noteTitle ) {
589
+ const packages = new Map();
590
+ const scopes = new Set();
591
+
592
+ for ( const commit of filterCommitsByNoteTitle( publicCommits, noteTitle ) ) {
593
+ for ( const note of commit.notes ) {
594
+ if ( Array.isArray( note.rawScope ) ) {
595
+ scopes.add( note.rawScope[ 0 ] );
596
+ }
597
+ }
598
+ }
599
+
600
+ for ( const [ packageName, version ] of dependencies ) {
601
+ const packageScope = packageName.replace( /^@ckeditor\/ckeditor5?-/, '' );
602
+
603
+ for ( const singleScope of scopes ) {
604
+ if ( packageScope === singleScope ) {
605
+ packages.set( packageName, version );
606
+ dependencies.delete( packageName );
607
+ }
608
+ }
609
+ }
610
+
611
+ return packages;
612
+ }
613
+
614
+ /**
615
+ * Returns packages that contain new features.
616
+ *
617
+ * @param {Map.<String, Version>} dependencies
618
+ * @returns {Map.<String, Version>}
619
+ */
620
+ function getPackagesWithNewFeatures( dependencies ) {
621
+ const packages = new Map();
622
+
623
+ for ( const [ packageName, version ] of dependencies ) {
624
+ const packagePath = packagesPaths.get( packageName );
625
+ const commits = filterCommitsByPath( publicCommits, packagePath );
626
+ const hasFeatures = commits.some( commit => commit.rawType === 'Feature' );
627
+
628
+ if ( hasFeatures ) {
629
+ packages.set( packageName, version );
630
+ dependencies.delete( packageName );
631
+ }
632
+ }
633
+
634
+ return packages;
635
+ }
636
+
637
+ /**
638
+ * Returns a formatted entry (string) for the changelog.
639
+ *
640
+ * @param {String} packageName
641
+ * @param {String} nextVersion
642
+ * @param {String} currentVersion
643
+ * @returns {String}
644
+ */
645
+ function formatChangelogEntry( packageName, nextVersion, currentVersion = null ) {
646
+ const npmUrl = `https://www.npmjs.com/package/${ packageName }`;
647
+
648
+ if ( currentVersion ) {
649
+ return `* [${ packageName }](${ npmUrl }): v${ currentVersion } => v${ nextVersion }`;
650
+ }
651
+
652
+ return `* [${ packageName }](${ npmUrl }): v${ nextVersion }`;
653
+ }
654
+
655
+ /**
656
+ * Returns a function that is being used when sorting commits.
657
+ *
658
+ * @param {String} scopeField A name of the field that saves the commit's scope.
659
+ * @returns {Function}
660
+ */
661
+ function sortFunctionFactory( scopeField ) {
662
+ return compareFunc( item => {
663
+ if ( Array.isArray( item[ scopeField ] ) ) {
664
+ // A hack that allows moving all scoped commits from the main repository/package at the beginning of the list.
665
+ if ( item[ scopeField ][ 0 ] === pkgJson.name ) {
666
+ return 'a'.repeat( 15 );
667
+ }
668
+
669
+ return item[ scopeField ][ 0 ];
670
+ }
671
+
672
+ // A hack that allows moving all non-scoped commits or breaking changes notes at the end of the list.
673
+ return 'z'.repeat( 15 );
674
+ } );
675
+ }
676
+
677
+ function logProcess( message ) {
678
+ log.info( '\n📍 ' + chalk.cyan( message ) );
679
+ }
680
+
681
+ /**
682
+ * @param {String} message
683
+ * @param {Object} [options={}]
684
+ * @param {Number} [options.indentLevel=0]
685
+ * @param {Boolean} [options.startWithNewLine=false] Whether to append a new line before the message.
686
+ * @param {Boolean} [options.isWarning=false] Whether to use `warning` method instead of `log`.
687
+ */
688
+ function logInfo( message, options = {} ) {
689
+ const indentLevel = options.indentLevel || 0;
690
+ const startWithNewLine = options.startWithNewLine || false;
691
+ const method = options.isWarning ? 'warning' : 'info';
692
+
693
+ log[ method ]( `${ startWithNewLine ? '\n' : '' }${ ' '.repeat( indentLevel * cli.INDENT_SIZE ) }` + message );
694
+ }
695
+ };
696
+
697
+ /**
698
+ * @typedef {Object} Version
699
+ *
700
+ * @param {Boolean} current The current version defined in the `package.json` file.
701
+ *
702
+ * @param {Boolean} next The next version defined during generating the changelog file.
703
+ */
704
+
705
+ /**
706
+ * @typedef {Object} ExternalRepository
707
+ *
708
+ * @param {String} cwd An absolute path to the repository.
709
+ *
710
+ * @param {String} packages Subdirectory in a given `cwd` that should searched for packages. E.g. `'packages'`.
711
+ *
712
+ * @param {String} [scope] Glob pattern for package names to be processed.
713
+ *
714
+ * @param {Array.<String>} [skipPackages] Name of packages which won't be touched.
715
+ *
716
+ * @param {Boolean} [skipLinks] If set on `true`, a URL to commit (hash) will be omitted.
717
+ *
718
+ * @param {String} [from] A commit or tag name that will be the first param of the range of commits to collect. If not specified,
719
+ * the option will inherit its value from the function's `options` object.
720
+ *
721
+ * @param {String} [releaseBranch] A name of the branch that should be used for releasing packages. If not specified, the branch
722
+ * used for the main repository will be used.
723
+ */