@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,392 @@
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 readline = require( 'readline' );
11
+ const { execSync } = require( 'child_process' );
12
+ const chalk = require( 'chalk' );
13
+ const glob = require( 'glob' );
14
+ const { diffLines: diff } = require( 'diff' );
15
+
16
+ // The pattern defines CKEditor 5 dependencies.
17
+ const CKEDITOR5_DEPENDENCY_PATTERN = /^@ckeditor\/ckeditor5-(.*)|^ckeditor5$/;
18
+
19
+ // Packages that match the CKEditor 5 pattern but should not be updated because they aren't a dependency of the project.
20
+ const PATTERNS_TO_SKIP = [
21
+ /^@ckeditor\/ckeditor5-dev$/,
22
+ /^@ckeditor\/ckeditor5-dev-.*/,
23
+ '@ckeditor/ckeditor5-angular',
24
+ '@ckeditor/ckeditor5-react',
25
+ '@ckeditor/ckeditor5-vue',
26
+ '@ckeditor/ckeditor5-inspector'
27
+ ];
28
+
29
+ /**
30
+ * The purpose of this script is to update a version of `ckeditor5` and `@ckeditor/ckeditor5-*` dependencies to
31
+ * a version specified in `options.version`.
32
+ *
33
+ * The script can commit changes for all packages in specified directories. See the `CKEditor5EntryItem` interface.
34
+ *
35
+ * If you want to see what kind of changes the script does, set the `dryRun` flag to `true`. Instead of committing anything, the script
36
+ * will display changes on the screen.
37
+ *
38
+ * The following packages will not be touched:
39
+ * * Integrations (@ckeditor/ckeditor5-@(vue|angular|react)
40
+ * * Dev-tools (@ckeditor/ckeditor5-dev-*)
41
+ * * Inspector (@ckeditor/ckeditor5-inspector)
42
+ *
43
+ * @param {Object} options
44
+ * @param {String} options.cwd Current working directory (packages) from which all paths will be resolved.
45
+ * @param {String} options.version Target version, all of the eligible dependencies will be updated to.
46
+ * @param {Array.<CKEditor5EntryItem>} options.packages An array containing paths where to look for packages to update.
47
+ * @param {Boolean} [options.dryRun=false] If set on true, all changes will be printed on the screen instead of committed.
48
+ */
49
+ module.exports = function updateCKEditor5Dependencies( options ) {
50
+ const totalResult = { found: 0, updated: 0, toCommit: 0, differences: [] };
51
+ const pathsToCommit = [];
52
+
53
+ console.log( '\n📍 ' + chalk.blue( 'Updating CKEditor 5 dependencies...\n' ) );
54
+
55
+ if ( options.dryRun ) {
56
+ console.log( `⚠️ ${ chalk.yellow( 'DRY RUN mode' ) } ⚠️` );
57
+ console.log( chalk.yellow( 'The script WILL NOT modify anything but instead show differences that would be made.\n' ) );
58
+ }
59
+
60
+ for ( const entryItem of options.packages ) {
61
+ console.log( `Looking for packages in the '${ chalk.underline( entryItem.directory ) }/' directory...` );
62
+
63
+ // An absolute path to a directory where to look for packages.
64
+ const absolutePath = path.join( options.cwd, entryItem.directory );
65
+ const results = updatePackagesInDirectory( options.version, absolutePath, options.dryRun );
66
+
67
+ totalResult.found += results.found;
68
+ totalResult.updated += results.updated;
69
+ totalResult.differences.push( ...results.differences );
70
+
71
+ if ( !results.found ) {
72
+ console.log( 'No files were found.\n' );
73
+ } else if ( !results.updated ) {
74
+ console.log( `${ chalk.bold( results.found ) } files were found, but none needed to be updated.\n` );
75
+ } else {
76
+ console.log( `Out of ${ chalk.bold( results.found ) } files found, ${ chalk.bold( results.updated ) } were updated.\n` );
77
+
78
+ if ( entryItem.commit ) {
79
+ totalResult.toCommit += results.updated;
80
+ pathsToCommit.push( absolutePath );
81
+ }
82
+ }
83
+ }
84
+
85
+ if ( options.dryRun ) {
86
+ console.log( chalk.yellow( `${ chalk.bold( 'Enter' ) } / ${ chalk.bold( 'Space' ) } - Display next file diff` ) );
87
+ console.log( chalk.yellow( ` ${ chalk.bold( 'A' ) } - Display diff from all files` ) );
88
+ console.log( chalk.yellow( ` ${ chalk.bold( 'Esc' ) } / ${ chalk.bold( 'Q' ) } - Exit` ) );
89
+
90
+ if ( !totalResult.differences.length ) {
91
+ console.log( chalk.yellow( 'The script has not changed any files.' ) );
92
+ process.exit();
93
+ }
94
+
95
+ readline.emitKeypressEvents( process.stdin );
96
+ process.stdin.setRawMode( true );
97
+
98
+ // Instead of a lambda function, this `process.stdin.on` has to take in a named function and have `differences` assigned to it.
99
+ // This is done so that it can be tested properly, as it is otherwise impossible to pass this array inside tests.
100
+ processInput.differences = totalResult.differences;
101
+ process.stdin.on( 'keypress', processInput );
102
+ } else {
103
+ if ( pathsToCommit.length ) {
104
+ console.log( '\n📍 ' + chalk.blue( 'Committing the changes...\n' ) );
105
+
106
+ // First, add changes from specified paths to stage...
107
+ for ( const absoluteDirectoryPath of pathsToCommit ) {
108
+ console.log( `${ chalk.green( '+' ) } '${ chalk.underline( absoluteDirectoryPath ) }'` );
109
+
110
+ execSync( 'git add "*/package.json"', {
111
+ stdio: 'inherit',
112
+ cwd: absoluteDirectoryPath
113
+ } );
114
+ }
115
+
116
+ // ...then, commit all changes in a single commit.
117
+ execSync( 'git commit -m "Internal: Updated CKEditor 5 packages to the latest version. [skip ci]"', {
118
+ stdio: 'inherit',
119
+ cwd: options.cwd
120
+ } );
121
+
122
+ console.log( '\n📍 ' + chalk.green( `Successfully committed ${ totalResult.toCommit } files!\n` ) );
123
+ }
124
+
125
+ if ( totalResult.updated ) {
126
+ console.log( '\n📍 ' + chalk.green( `Updated total of ${ totalResult.updated } files!\n` ) );
127
+ } else {
128
+ console.log( '\n📍 ' + chalk.green( 'No files needed an update.\n' ) );
129
+ }
130
+ }
131
+ };
132
+
133
+ /**
134
+ * Updates `@ckeditor/ckeditor5-*` and `ckeditor5` dependencies. The following packages will be ignored:
135
+ * * Integrations (@ckeditor/ckeditor5-@(vue|angular|react)
136
+ * * Dev-tools (@ckeditor/ckeditor5-dev-*)
137
+ * * Inspector (@ckeditor/ckeditor5-inspector)
138
+ *
139
+ * @param {String} version Target version, all of the eligible dependencies will be updated to.
140
+ * @param {String} packagesDirectory Directory containing packages to update.
141
+ * @param {Boolean} dryRun If set to true, diff of changes that would be made is calculated, and included in the returned object.
142
+ * @returns {UpdateResult}
143
+ */
144
+ function updatePackagesInDirectory( version, packagesDirectory, dryRun ) {
145
+ const packageJsonArray = glob.sync( '*/package.json', {
146
+ cwd: packagesDirectory,
147
+ absolute: true
148
+ } );
149
+
150
+ let updatedFiles = 0;
151
+ const differences = [];
152
+
153
+ for ( const packageJsonPath of packageJsonArray ) {
154
+ const currentContent = fs.readFileSync( packageJsonPath, 'utf-8' );
155
+ const contentAsJson = JSON.parse( currentContent );
156
+
157
+ updateObjectProperty( contentAsJson, 'dependencies', version );
158
+ updateObjectProperty( contentAsJson, 'devDependencies', version );
159
+
160
+ const newContent = JSON.stringify( contentAsJson, null, 2 ) + '\n';
161
+
162
+ // No changes.
163
+ if ( currentContent === newContent ) {
164
+ continue;
165
+ }
166
+
167
+ if ( dryRun ) {
168
+ differences.push( {
169
+ file: packageJsonPath,
170
+ content: diff( currentContent, newContent, { newlineIsToken: true } )
171
+ } );
172
+ } else {
173
+ fs.writeFileSync( packageJsonPath, newContent, 'utf-8' );
174
+ }
175
+
176
+ updatedFiles++;
177
+ }
178
+
179
+ return {
180
+ found: packageJsonArray.length,
181
+ updated: updatedFiles,
182
+ differences
183
+ };
184
+ }
185
+
186
+ /**
187
+ * Updates the CKEditor 5 dependencies.
188
+ *
189
+ * @param {Object} packageJson `package.json` as JSON object to update.
190
+ * @param {'dependencies'|'devDependencies'} propertyName Name of the property to update.
191
+ * @param {String} version Target version, all of the eligible dependencies will be updated to.
192
+ */
193
+ function updateObjectProperty( packageJson, propertyName, version ) {
194
+ // "dependencies" or "devDependencies" are not specified. There is nothing to update.
195
+ if ( !packageJson[ propertyName ] ) {
196
+ return;
197
+ }
198
+
199
+ for ( const packageName of Object.keys( packageJson[ propertyName ] ) ) {
200
+ const match = packageName.match( CKEDITOR5_DEPENDENCY_PATTERN );
201
+ const shouldSkip = PATTERNS_TO_SKIP.some( pattern => packageName.match( pattern ) );
202
+
203
+ if ( match && !shouldSkip ) {
204
+ packageJson[ propertyName ][ packageName ] = `^${ version }`;
205
+ }
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Takes in raw changelog for a single file generated by `diff` library, and returns formatted array of strings, containing
211
+ * human-readable, line by line changelog of a file, with removals and additions colored. If the file has any longer parts without changes,
212
+ * these will be partially hidden.
213
+ *
214
+ * @param {Array.<Object>} diff Array of changes for a single file generated by `diff` library.
215
+ * @returns {Array.<String>} Formatted changelog split into single lines.
216
+ */
217
+ function formatDiff( diff ) {
218
+ const formattedDiff = [];
219
+
220
+ // Searches for a space in the following expression: `"dependency": "version"`.
221
+ // It is used for displaying pretty diff.
222
+ const keyValueSpaceRegexp = /(?<=":) (?=")/;
223
+
224
+ for ( let i = 0; i < diff.length; i++ ) {
225
+ const previous = diff[ i - 1 ];
226
+ const current = diff[ i ];
227
+ const next = diff[ i + 1 ];
228
+ const currentLines = current.value.split( '\n' );
229
+
230
+ if ( shouldFormatDifference( current, next, keyValueSpaceRegexp ) ) {
231
+ // Adding removals followed by additions (formatted).
232
+ formattedDiff.push( [
233
+ current.value.split( keyValueSpaceRegexp )[ 0 ],
234
+ ' ',
235
+ chalk.red( current.value.split( keyValueSpaceRegexp )[ 1 ] ),
236
+ chalk.green( next.value.split( keyValueSpaceRegexp )[ 1 ] )
237
+ ].join( '' ) );
238
+
239
+ i++;
240
+ } else if ( current.added ) {
241
+ // Other additions (trimming whitespaces in replacements)
242
+ formattedDiff.push( chalk.green( previous && previous.removed ? current.value.trim() : current.value ) );
243
+ } else if ( current.removed ) {
244
+ // Other removals
245
+ formattedDiff.push( chalk.red( current.value ) );
246
+ } else if ( currentLines.length > 8 ) {
247
+ // Cutting out the middle of a long streak of unchanged lines.
248
+ const shortenedLines = [
249
+ ...currentLines.slice( 0, 3 ),
250
+ chalk.gray( `[...${ currentLines.length - 7 } lines without changes...]` ),
251
+ ...currentLines.slice( -4 )
252
+ ].join( '\n' );
253
+
254
+ formattedDiff.push( shortenedLines );
255
+ } else {
256
+ // Unchanged lines
257
+ formattedDiff.push( current.value );
258
+ }
259
+ }
260
+
261
+ // This turns the array from random chunks containing newlines, to uniform set of single lines.
262
+ return formattedDiff.join( '' ).split( '\n' );
263
+ }
264
+
265
+ /**
266
+ * Displays and removes first file changelog from a given array of file changelogs.
267
+ *
268
+ * @param {Array.<Object>} differences Array of objects, where each element has `content` value that is an array of strings, and `file`
269
+ * value that is a string with file name.
270
+ */
271
+ function printNextFile( differences ) {
272
+ const nextDiff = differences.shift();
273
+ const formattedDiff = formatDiff( nextDiff.content );
274
+
275
+ console.log( `File: '${ chalk.underline( nextDiff.file ) }'` );
276
+
277
+ for ( const line of formattedDiff ) {
278
+ console.log( line );
279
+ }
280
+ }
281
+
282
+ /**
283
+ * The difference between passed `currentDiff` and `nextDiff` objects should be formatted only if the `currentDiff` removed a line, then
284
+ * `nextDiff` added a new one, and both updated a CKEditor 5 package.
285
+ *
286
+ * @param {Object} currentDiff
287
+ * @param {Object} nextDiff
288
+ * @param {String} regex
289
+ * @returns {Boolean}
290
+ */
291
+ function shouldFormatDifference( currentDiff, nextDiff, regex ) {
292
+ if ( !nextDiff ) {
293
+ return false;
294
+ }
295
+
296
+ if ( !currentDiff.removed ) {
297
+ return false;
298
+ }
299
+
300
+ if ( !nextDiff.added ) {
301
+ return false;
302
+ }
303
+
304
+ if ( !regex.test( currentDiff.value ) ) {
305
+ return false;
306
+ }
307
+
308
+ if ( !regex.test( nextDiff.value ) ) {
309
+ return false;
310
+ }
311
+
312
+ return true;
313
+ }
314
+
315
+ /**
316
+ * Takes data about the pressed key, and produces appropriate result:
317
+ *
318
+ * - "Enter" / "Space": Prints next file diff, and ends the process if it was the last file or displays controls if not.
319
+ * - "A": Prints all the remaining file diffs, and ends the process.
320
+ * - "Q" / "Esc": Ends the process.
321
+ *
322
+ * This function is passed as a callback in `process.stdin.on( 'keypress', processInput )`. In order for this function to be tested
323
+ * properly, it needs to be a named function that can have attached values, as otherwise passing the `differences` array would be impossible
324
+ * in the tests.
325
+ *
326
+ * @param {String} chunk Streak of keyboard inputs.
327
+ * @param {Object} key Contains information about what button was pressed, and whether or not modifiers
328
+ * such as `ctrl` were held at the same time.
329
+ */
330
+ function processInput( chunk, key ) {
331
+ // Differences array should be attached to the function itself.
332
+ const differences = processInput.differences;
333
+
334
+ const inputs = {
335
+ next: [ 'space', 'return' /* 'return' means enter */ ],
336
+ all: [ 'a' ],
337
+ exit: [ 'q', 'escape' ]
338
+ };
339
+
340
+ if ( inputs.next.includes( key.name ) ) {
341
+ console.log( chalk.yellow( 'Displaying next file.' ) );
342
+
343
+ printNextFile( differences );
344
+
345
+ if ( !differences.length ) {
346
+ console.log( chalk.yellow( 'No more files.' ) );
347
+
348
+ process.exit();
349
+ } else {
350
+ console.log( [
351
+ chalk.yellow( `${ chalk.bold( 'Enter' ) } / ${ chalk.bold( 'Space' ) } - Next` ),
352
+ chalk.yellow( `${ chalk.bold( 'A' ) } - All` ),
353
+ chalk.yellow( `${ chalk.bold( 'Esc' ) } / ${ chalk.bold( 'Q' ) } - Exit` )
354
+ ].join( ' ' ) );
355
+ }
356
+ }
357
+
358
+ if ( inputs.all.includes( key.name ) ) {
359
+ console.log( chalk.yellow( 'Displaying all files.' ) );
360
+
361
+ while ( differences.length ) {
362
+ printNextFile( differences );
363
+ }
364
+
365
+ process.exit();
366
+ }
367
+
368
+ if ( inputs.exit.includes( key.name ) ) {
369
+ console.log( chalk.yellow( 'Manual exit.' ) );
370
+
371
+ process.exit();
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Contains information about the way in which the files were processed.
377
+ *
378
+ * @typedef {Object} UpdateResult
379
+ * @property {Number} found Number of files found.
380
+ * @property {Number} updated Number of files updated.
381
+ * @property {Array.<Object>} differences Array of objects, where each object has string `file` containing path to the file, as well as
382
+ * array of objects `content` returned by the `diff` library, that describes changes made to each file.
383
+ */
384
+
385
+ /**
386
+ * A definition of a directory where to look for packages.
387
+ *
388
+ * @typedef {Object} CKEditor5EntryItem
389
+ *
390
+ * @property {String} directory A relative path (to root directory) where to look for packages.
391
+ * @property {Boolean} [commit=false] Whether changes should be committed.
392
+ */
@@ -0,0 +1,29 @@
1
+ * {{#if scope}}**{{~scope.[0]}}**: {{/if}}{{subject}}
2
+
3
+ {{~#unless @root.skipCommitsLink }}
4
+ {{~#unless skipCommitsLink }}
5
+ {{~!-- commit link --}} {{#if @root.linkReferences~}}
6
+ ([commit](
7
+ {{~#if @root.repository}}
8
+ {{~#if @root.host}}
9
+ {{~@root.host}}/
10
+ {{~/if}}
11
+ {{~#if @root.owner}}
12
+ {{~@root.owner}}/
13
+ {{~/if}}
14
+ {{~@root.repository}}
15
+ {{~else}}
16
+ {{~@root.repoUrl}}
17
+ {{~/if}}/
18
+ {{~@root.commit}}/{{hash}}))
19
+ {{~else}}
20
+ {{~hash}}
21
+ {{~/if}}
22
+ {{/unless}}
23
+ {{/unless}}
24
+ {{#if body}}
25
+
26
+
27
+ {{body}}
28
+ {{~/if}}
29
+
@@ -0,0 +1,10 @@
1
+ {{#if noteGroups}}
2
+ {{#each noteGroups}}
3
+ ### {{title}}
4
+
5
+ {{#each notes}}
6
+ * {{#if scope}}**{{~scope.[0]}}**: {{/if}}{{text}}
7
+ {{/each}}
8
+
9
+ {{/each}}
10
+ {{/if}}
@@ -0,0 +1,23 @@
1
+ ## {{#unless @root.skipCompareLink~}}
2
+ [{{version}}](
3
+ {{~#if @root.repository~}}
4
+ {{~#if @root.host}}
5
+ {{~@root.host}}/
6
+ {{~/if}}
7
+ {{~#if @root.owner}}
8
+ {{~@root.owner}}/
9
+ {{~/if}}
10
+ {{~@root.repository}}
11
+ {{~else}}
12
+ {{~@root.repoUrl}}
13
+ {{~/if~}}
14
+ {{~#if previousTag~}}
15
+ /compare/{{previousTag}}...{{currentTag}}
16
+ {{~else~}}
17
+ /tree/{{currentTag}}
18
+ {{~/if}})
19
+ {{~else}}
20
+ {{~version}}
21
+ {{~/unless}}
22
+ {{~#if date}} ({{date}})
23
+ {{/if}}
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": null,
3
+ "version": null,
4
+ "description": null,
5
+ "keywords": null,
6
+ "engines": null,
7
+ "author": null,
8
+ "license": null,
9
+ "homepage": null,
10
+ "bugs": null,
11
+ "repository": null
12
+ }
@@ -0,0 +1,37 @@
1
+ {{> header}}
2
+
3
+ {{#if isInternalRelease}}
4
+ Internal changes only (updated dependencies, documentation, etc.).
5
+
6
+
7
+ {{else}}
8
+ {{#if highlightsPlaceholder}}
9
+ ### Release highlights
10
+
11
+ <!-- TODO: Add a link to the blog post. -->
12
+
13
+ {{/if}}
14
+ {{#if collaborationFeatures}}
15
+ ### Collaboration features
16
+
17
+ The CKEditor 5 Collaboration features changelog can be found here: https://ckeditor.com/collaboration/changelog.
18
+
19
+ {{/if}}
20
+ {{> footer}}
21
+ {{#each commitGroups}}
22
+ ### {{title}}
23
+
24
+ {{#if (lookup @root.additionalNotes title)}}
25
+ {{ lookup @root.additionalNotes title }}
26
+
27
+ {{/if}}
28
+ {{#each commits}}
29
+ {{> commit root=@root}}
30
+ {{/each}}
31
+
32
+ {{else}}
33
+ Internal changes only (updated dependencies, documentation, etc.).
34
+
35
+ {{/each}}
36
+
37
+ {{/if}}
@@ -0,0 +1,67 @@
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
+
11
+ const utils = {
12
+ /**
13
+ * Changelog file name.
14
+ */
15
+ changelogFile: 'CHANGELOG.md',
16
+
17
+ /**
18
+ * Changelog header.
19
+ */
20
+ changelogHeader: 'Changelog\n=========\n\n',
21
+
22
+ /**
23
+ * Retrieves changes from the changelog for the given version (tag).
24
+ *
25
+ * @param {String} version
26
+ * @param {String} [cwd=process.cwd()] Where to look for the changelog file.
27
+ * @returns {String|null}
28
+ */
29
+ getChangesForVersion( version, cwd = process.cwd() ) {
30
+ version = version.replace( /^v/, '' );
31
+
32
+ const changelog = utils.getChangelog( cwd ).replace( utils.changelogHeader, '\n' );
33
+ const match = changelog.match( new RegExp( `\\n(## \\[?${ version }\\]?[\\s\\S]+?)(?:\\n## \\[?|$)` ) );
34
+
35
+ if ( !match || !match[ 1 ] ) {
36
+ return null;
37
+ }
38
+
39
+ return match[ 1 ].replace( /##[^\n]+\n/, '' ).trim();
40
+ },
41
+
42
+ /**
43
+ * @param {String} [cwd=process.cwd()] Where to look for the changelog file.
44
+ * @returns {String|null}
45
+ */
46
+ getChangelog( cwd = process.cwd() ) {
47
+ const changelogFile = path.join( cwd, utils.changelogFile );
48
+
49
+ if ( !fs.existsSync( changelogFile ) ) {
50
+ return null;
51
+ }
52
+
53
+ return fs.readFileSync( changelogFile, 'utf-8' );
54
+ },
55
+
56
+ /**
57
+ * @param {String} content
58
+ * @param {String} [cwd=process.cwd()] Where to look for the changelog file.
59
+ */
60
+ saveChangelog( content, cwd = process.cwd() ) {
61
+ const changelogFile = path.join( cwd, utils.changelogFile );
62
+
63
+ fs.writeFileSync( changelogFile, content, 'utf-8' );
64
+ }
65
+ };
66
+
67
+ module.exports = utils;