@ckeditor/ckeditor5-dev-release-tools 37.0.1 → 38.0.0-alpha.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.
@@ -1,1001 +0,0 @@
1
- /**
2
- * @license Copyright (c) 2003-2023, 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 chalk = require( 'chalk' );
11
- const glob = require( 'glob' );
12
- const mkdirp = require( 'mkdirp' );
13
- const semver = require( 'semver' );
14
- const Table = require( 'cli-table' );
15
- const { tools, logger } = require( '@ckeditor/ckeditor5-dev-utils' );
16
- const parseGithubUrl = require( 'parse-github-url' );
17
- const cli = require( '../utils/cli' );
18
- const createGithubRelease = require( '../utils/creategithubrelease' );
19
- const displaySkippedPackages = require( '../utils/displayskippedpackages' );
20
- const executeOnPackages = require( '../utils/executeonpackages' );
21
- const { getChangesForVersion } = require( '../utils/changelog' );
22
- const getPackageJson = require( '../utils/getpackagejson' );
23
- const getPackagesPaths = require( '../utils/getpackagespaths' );
24
- const { Octokit } = require( '@octokit/rest' );
25
-
26
- const PACKAGE_JSON_TEMPLATE_PATH = require.resolve( '../templates/release-package.json' );
27
- const BREAK_RELEASE_MESSAGE = 'You aborted publishing the release. Why? Oh why?!';
28
- const NO_RELEASE_MESSAGE = 'No changes for publishing. Why? Oh why?!';
29
- const AUTH_REQUIRED = 'You must be logged to execute this command.';
30
- const MISSING_FILES_MESSAGE =
31
- 'Publishing the release is terminated by you.\n' +
32
- 'Some files were expected to exist, but they were not found in the directory structure of the package.\n\n' +
33
- 'šŸ‘‰ Next step: Take a deep breath and think why the package does not have the necessary files.\n\n' +
34
- 'šŸ‘‰ Workarounds:\n' +
35
- ' (1) Run the script once again and accept the package with missing files.\n' +
36
- ' BE CAREFUL: the package will be published as it is now.\n' +
37
- ' (2) Consider adding the package to exceptions in `options.skipNpmPublish`.\n';
38
-
39
- // That files will be copied from source to the temporary directory and will be released too.
40
- const additionalFiles = [
41
- 'CHANGELOG.md',
42
- 'LICENSE.md',
43
- 'README.md'
44
- ];
45
-
46
- /**
47
- * Releases all sub-repositories (packages) found in specified path.
48
- *
49
- * This task does:
50
- * - finds paths to sub repositories,
51
- * - filters packages which should be released by comparing the latest version published on npm and GitHub,
52
- * - publishes new version on npm,
53
- * - pushes new version to the remote repository,
54
- * - creates a release which is displayed on "Releases" page on GitHub.
55
- *
56
- * If you want to publish an empty repository (it will contain required files for npm), you can specify the package name
57
- * as `options.customReleases`. Packages specified in the array will be published from temporary directory. `package.json` of that package
58
- * will be created based on a template. See `packages/ckeditor5-dev-release-tools/lib/templates/release-package.json` file.
59
- *
60
- * Content of `package.json` can be adjusted using `options.packageJsonForCustomReleases` options. If you need to copy values from
61
- * real `package.json` that are not defined in template, you can add these keys as null. Values will be copied automatically.
62
- *
63
- * If you want to add files from the source package directory to the temporary directory, you can use the
64
- * `options.customReleasesFiles` option.
65
- *
66
- * Example usage:
67
- *
68
- * require( '@ckeditor/ckeditor5-release-tools' )
69
- * .releaseSubRepositories( {
70
- * cwd: process.cwd(),
71
- * packages: 'packages',
72
- * customReleases: [
73
- * 'ckeditor5' // "ckeditor5" will be released as an empty repository.
74
- * ],
75
- * packageJsonForCustomReleases: {
76
- * ckeditor5: { // These properties will overwrite those ones that are defined in package's `package.json`
77
- * description: 'Custom description', // "description" of real package will be overwritten.
78
- * devDependencies: null // The template does not contain "devDependencies" but we want to add it.
79
- * }
80
- * },
81
- * customReleasesFiles: {
82
- * ckeditor5: [ // An array of glob patterns. Files that match to those patterns will be released.
83
- * 'src/*.js' // Copy all JS files from the `src/` directory.
84
- * 'scripts/**' // Copy everything from the `script/` directory.
85
- * ]
86
- * },
87
- * dryRun: process.argv.includes( '--dry-run' )
88
- * } );
89
- *
90
- * Pushes are done at the end of the whole process because of Continues Integration. We need to publish all
91
- * packages on npm before starting the CI testing. If we won't do it, CI will fail because it won't be able
92
- * to install packages which versions will match to specified in `package.json`.
93
- * See {@link https://github.com/ckeditor/ckeditor5-dev/issues/272}.
94
- *
95
- * @param {Object} options
96
- *
97
- * @param {String} options.cwd Current working directory (packages) from which all paths will be resolved.
98
- *
99
- * @param {String|null} options.packages Where to look for other packages (dependencies). If `null`, only repository specified under
100
- * `options.cwd` will be used in the task.
101
- *
102
- * @param {String} [options.npmTag='latest'] Defines an npm tag which the package manager will use when installing the package.
103
- * Read more: https://docs.npmjs.com/cli/v8/commands/npm-publish#tag.
104
- *
105
- * @param {Array.<String>} [options.skipPackages=[]] Name of packages which won't be released.
106
- *
107
- * @param {Boolean} [options.dryRun=false] If set on true, nothing will be published:
108
- * - npm pack will be called instead of npm publish (it packs the whole release to a ZIP archive),
109
- * - "git push" will be replaced with a log on the screen,
110
- * - creating a release on GitHub will be replaced with a log on the screen,
111
- * - every called command will be displayed.
112
- *
113
- * @param {Boolean} [options.skipMainRepository=false] If set on true, package found in "cwd" will be skipped.
114
- *
115
- * @param {Array.<String>>} [options.customReleases=[]] Name of packages that should be published from the temporary directory
116
- * instead of the package directory. It was used for publishing an empty package (with files that are required for npm). By using
117
- * the `options.packageJsonForCustomReleases`, you can specify the content for the `package.json` file. By using
118
- * the `options.customReleasesFiles` option, you can specify which files should be copied to the temporary directory.
119
- *
120
- * @param {Object} [options.packageJsonForCustomReleases={}] Additional fields that will be added to `package.json` for packages which
121
- * will be published using the custom release option. All properties copied from original package's `package.json` file
122
- * will be overwritten by fields specified in this option.
123
- *
124
- * @param {Object} [options.customReleasesFiles={}] Glob patterns of files that will be copied to the temporary for packages which
125
- * will be published using the custom release option.
126
- *
127
- * @param {Array.<String>} [options.skipNpmPublish=[]] Name of packages that should not be published on npm.
128
- *
129
- * @param {String} [options.releaseBranch='master'] A name of the branch that should be used for releasing packages.
130
- *
131
- * @param {Object} [options.optionalFilesAndDirectories=null] By default, for each package that we want to publish,
132
- * the tool checks whether all files specified in the `#files` key (in `package.json`) exist. The option allows defining
133
- * items that does not have to exist, e.g., the `theme/` directory is optional because CKEditor 5 features do not have to define styles.
134
- * The `lang/` directory also a good example, as only some of packages can be localized.
135
- *
136
- * The `options.optionalFilesAndDirectories` object may contain keys that are package names. The `#default` key is used for all packages
137
- * that do not have own key.
138
- *
139
- * @returns {Promise}
140
- */
141
- module.exports = async function releaseSubRepositories( options ) {
142
- const cwd = process.cwd();
143
- const log = logger();
144
-
145
- const dryRun = Boolean( options.dryRun );
146
- const releaseBranch = options.releaseBranch || 'master';
147
- const npmTag = options.npmTag || 'latest';
148
- const customReleases = Array.isArray( options.customReleases ) ? options.customReleases : [ options.customReleases ].filter( Boolean );
149
-
150
- // When preparing packages for release, we check whether there are files in the directory structure of the package, which are
151
- // defined in the `#files` key in the `package.json`. It suffices that at least one file exists for each entry from the `#files`
152
- // key. Some files and directories, although defined in `#files`, are optional, so their absence in the package directory
153
- // should not be treated as an error. The list below defines this optional files and directories in the package.
154
- const optionalFilesAndDirectories = options.optionalFilesAndDirectories || null;
155
-
156
- const pathsCollection = getPackagesPaths( {
157
- cwd: options.cwd,
158
- packages: options.packages,
159
- skipPackages: options.skipPackages || [],
160
- skipMainRepository: options.skipMainRepository
161
- } );
162
-
163
- logDryRun( 'āš ļø DRY RUN mode āš ļø' );
164
- logDryRun( 'The script WILL NOT publish anything but will create some files.' );
165
-
166
- // The variable is set only if "release on github" option has been chosen during configuration the release.
167
- // See `configureRelease()` function.
168
- let github;
169
-
170
- // Collections of paths where different kind of releases should be done.
171
- // - `releasesOnNpm` - the release on npm that contains the entire repository (npm publish is executed inside the repository)
172
- // - `customReleasesOnNpm` - the release on npm that contains a specified files instead of the entire content of the package.
173
- // For this releases, the `npm publish` command is executed from a temporary directory.
174
- // - `releasesOnGithub` - the release on GitHub (there is only one command called - `git push` and creating the release via REST API)
175
- const releasesOnNpm = new Set();
176
- const releasesOnGithub = new Set();
177
- const customReleasesOnNpm = new Map();
178
-
179
- // A list of packages that should not be published on npm.
180
- const skipNpmPublish = new Set( options.skipNpmPublish || [] );
181
-
182
- // List of packages that were released on npm or/and GitHub.
183
- const releasedPackages = new Set();
184
-
185
- // List of files that should be removed in DRY RUN mode. This is a result of command `npm pack`.
186
- const filesToRemove = new Set();
187
-
188
- // List of all details required for releasing packages.
189
- const packages = new Map();
190
-
191
- let releaseOptions;
192
-
193
- return configureRelease()
194
- .then( _releaseOptions => saveReleaseOptions( _releaseOptions ) )
195
- .then( () => authCheck() )
196
- .then( () => confirmNpmTag() )
197
- .then( () => preparePackagesToRelease() )
198
- .then( () => filterPackagesToReleaseOnNpm() )
199
- .then( () => filterPackagesToReleaseOnGitHub() )
200
- .then( () => confirmRelease() )
201
- .then( () => prepareDirectoriesForCustomReleases() )
202
- .then( () => releasePackagesOnNpm() )
203
- .then( () => pushPackages() )
204
- .then( () => createReleasesOnGitHub() )
205
- .then( () => removeTemporaryDirectories() )
206
- .then( () => removeReleaseArchives() )
207
- .then( () => {
208
- process.chdir( cwd );
209
-
210
- logProcess( `Finished releasing ${ chalk.underline( releasedPackages.size ) } package(s).` );
211
- logDryRun( 'Because of the DRY RUN mode, nothing has been changed. All changes were reverted.' );
212
-
213
- // For the real release from non-master branch, show the "merge" tip.
214
- if ( !dryRun && releaseBranch !== 'master' ) {
215
- log.info( 'āš ļø ' + chalk.underline( `Do not forget about merging "${ releaseBranch }" to the "master".` ) );
216
- }
217
- } )
218
- .catch( err => {
219
- process.chdir( cwd );
220
-
221
- if ( err instanceof Error ) {
222
- let message;
223
-
224
- switch ( err.message ) {
225
- case BREAK_RELEASE_MESSAGE:
226
- message = 'Publishing has been aborted.';
227
- break;
228
- case NO_RELEASE_MESSAGE:
229
- message = 'There is nothing to release. The process was aborted.';
230
- break;
231
- case AUTH_REQUIRED:
232
- message = 'Before you starting releasing, you need to login to npm.';
233
- break;
234
- }
235
-
236
- if ( message ) {
237
- logProcess( message );
238
-
239
- return Promise.resolve();
240
- }
241
- }
242
-
243
- log.error( dryRun ? err.stack : err.message );
244
-
245
- process.exitCode = -1;
246
- } );
247
-
248
- // Configures release options.
249
- //
250
- // @returns {Promise.<Object>}
251
- function configureRelease() {
252
- logProcess( 'Configuring the release...' );
253
-
254
- return cli.configureReleaseOptions();
255
- }
256
-
257
- // Saves the options provided by a user.
258
- //
259
- // @param {Object} _releaseOptions
260
- function saveReleaseOptions( _releaseOptions ) {
261
- releaseOptions = _releaseOptions;
262
-
263
- if ( !releaseOptions.npm && !releaseOptions.github ) {
264
- throw new Error( BREAK_RELEASE_MESSAGE );
265
- }
266
-
267
- if ( releaseOptions.github ) {
268
- // Because `octokit.authenticate()` is deprecated, the entire API object is created here.
269
- github = new Octokit( {
270
- version: '3.0.0',
271
- auth: `token ${ releaseOptions.token }`
272
- } );
273
- }
274
- }
275
-
276
- // Verifies if the provided by the user npm tag should be used to release new packages to npm.
277
- //
278
- // @returns {Promise}
279
- function confirmNpmTag() {
280
- if ( !releaseOptions.npm ) {
281
- return Promise.resolve();
282
- }
283
-
284
- const packageJson = getPackageJson( options.cwd );
285
- logProcess( 'Verifying the npm tag...' );
286
-
287
- const versionTag = getVersionTag( packageJson.version );
288
-
289
- if ( versionTag !== npmTag ) {
290
- log.warning( 'āš ļø The version tag is different from the npm tag.' );
291
- } else {
292
- log.info( 'āœ… Release tags are defined correctly.' );
293
- }
294
-
295
- return cli.confirmNpmTag( versionTag, npmTag )
296
- .then( isConfirmed => {
297
- if ( !isConfirmed ) {
298
- throw new Error( BREAK_RELEASE_MESSAGE );
299
- }
300
- } );
301
- }
302
-
303
- // Checks whether to a user is logged to npm.
304
- //
305
- // @returns {Promise}
306
- function authCheck() {
307
- if ( !releaseOptions.npm ) {
308
- return Promise.resolve();
309
- }
310
-
311
- logProcess( 'Checking whether you are logged to npm...' );
312
-
313
- try {
314
- const whoami = exec( 'npm whoami' );
315
-
316
- log.info( `šŸ”‘ Logged as "${ chalk.underline( whoami.trim() ) }".` );
317
-
318
- return Promise.resolve();
319
- } catch ( err ) {
320
- logDryRun( 'ā›”ļø You are not logged to npm. ā›”ļø' );
321
- logDryRun( chalk.italic( 'But this is a DRY RUN so you can continue safely.' ) );
322
-
323
- if ( dryRun ) {
324
- return Promise.resolve();
325
- }
326
-
327
- return Promise.reject( new Error( AUTH_REQUIRED ) );
328
- }
329
- }
330
-
331
- // Prepares a version, a description and other necessary things that must be done before starting
332
- // the entire a releasing process.
333
- //
334
- // @returns {Promise}
335
- function preparePackagesToRelease() {
336
- logProcess( 'Preparing packages that will be released...' );
337
-
338
- displaySkippedPackages( pathsCollection.skipped );
339
-
340
- return Promise.resolve()
341
- .then( () => {
342
- // Prepare the main repository release details.
343
- const packageJson = getPackageJson( options.cwd );
344
- const releaseDetails = {
345
- version: packageJson.version
346
- };
347
-
348
- if ( releaseOptions.github ) {
349
- const repositoryInfo = parseGithubUrl(
350
- exec( 'git remote get-url origin --push' ).trim()
351
- );
352
-
353
- releaseDetails.changes = getChangesForVersion( packageJson.version, options.cwd );
354
- releaseDetails.repositoryOwner = repositoryInfo.owner;
355
- releaseDetails.repositoryName = repositoryInfo.name;
356
- }
357
-
358
- packages.set( packageJson.name, releaseDetails );
359
-
360
- return executeOnPackages( pathsCollection.matched, repositoryPath => {
361
- // The main repository is handled before calling the `executeOnPackages()` function.
362
- if ( repositoryPath === options.cwd ) {
363
- return;
364
- }
365
-
366
- const packageJson = getPackageJson( repositoryPath );
367
-
368
- packages.set( packageJson.name, {
369
- version: packageJson.version
370
- } );
371
-
372
- return Promise.resolve();
373
- } );
374
- } )
375
- .then( () => {
376
- process.chdir( cwd );
377
- } );
378
- }
379
-
380
- // Checks which packages should be published on npm. It compares version defined in `package.json`
381
- // and the latest released on npm.
382
- //
383
- // @returns {Promise}
384
- function filterPackagesToReleaseOnNpm() {
385
- if ( !releaseOptions.npm ) {
386
- return Promise.resolve();
387
- }
388
-
389
- logProcess( 'Collecting the latest versions of packages published on npm...' );
390
-
391
- return executeOnPackages( pathsCollection.matched, repositoryPath => {
392
- process.chdir( repositoryPath );
393
-
394
- const packageJson = getPackageJson( repositoryPath );
395
- const releaseDetails = packages.get( packageJson.name );
396
-
397
- log.info( `\nChecking "${ chalk.underline( packageJson.name ) }"...` );
398
-
399
- if ( skipNpmPublish.has( packageJson.name ) ) {
400
- log.warning( 'āš ļø Skipping because the package was listed as `options.skipNpmPublish`.' );
401
-
402
- releaseDetails.npmVersion = null;
403
- releaseDetails.shouldReleaseOnNpm = false;
404
-
405
- return Promise.resolve();
406
- }
407
-
408
- const matchedFiles = getMatchedFilesToPublish( packageJson, repositoryPath );
409
- const hasAllFilesToPublish = hasAllRequiredFilesToPublish( packageJson, matchedFiles );
410
-
411
- if ( dryRun || !hasAllFilesToPublish ) {
412
- showMatchedFiles( packageJson, matchedFiles );
413
- }
414
-
415
- let promise;
416
-
417
- if ( dryRun ) {
418
- promise = Promise.resolve( true );
419
- } else if ( hasAllFilesToPublish ) {
420
- promise = Promise.resolve( true );
421
- } else {
422
- promise = cli.confirmIncludingPackage();
423
- }
424
-
425
- return promise.then( shouldIncludePackage => {
426
- if ( !shouldIncludePackage ) {
427
- throw new Error( MISSING_FILES_MESSAGE );
428
- }
429
-
430
- const npmVersion = getVersionFromNpm( packageJson.name, npmTag );
431
-
432
- logDryRun( `Versions: package.json: "${ releaseDetails.version }", npm: "${ npmVersion || 'initial release' }".` );
433
-
434
- releaseDetails.npmVersion = npmVersion;
435
- releaseDetails.shouldReleaseOnNpm = npmVersion !== releaseDetails.version;
436
-
437
- if ( releaseDetails.shouldReleaseOnNpm ) {
438
- log.info( 'āœ… Added to release.' );
439
-
440
- releasesOnNpm.add( repositoryPath );
441
- } else {
442
- log.info( 'āŒ Nothing to release.' );
443
- }
444
- } );
445
- } );
446
-
447
- // Scans the patterns provided in the `#files` key from `package.json` and collects number of matched files for each entry.
448
- // The keys in returned map are file patterns, and their values represent number of matched files. If there is no `#files` key
449
- // in `package.json`, then empty map is returned.
450
- function getMatchedFilesToPublish( packageJson, repositoryPath ) {
451
- // TODO: Include the `main` and `types` properties if they are specified.
452
- if ( !packageJson.files ) {
453
- return new Map();
454
- }
455
-
456
- return packageJson.files.reduce( ( result, entry ) => {
457
- const globOptions = {
458
- cwd: repositoryPath,
459
- dot: true,
460
- nodir: true
461
- };
462
-
463
- // An entry in the `#files` key can point either to a file, or to a directory. To test both cases in one `glob` call,
464
- // we use a braced section in the `glob` syntax. A braced section starts with { and ends with } and they are expanded
465
- // into a set of patterns. A braced section may contain any number of comma-delimited sections (path fragments) within.
466
- //
467
- // Example: for entry 'src', the following braced section would expand into 'src' and 'src/**' patterns, both evaluated in
468
- // one `glob` call.
469
- const numberOfMatches = glob.sync( entry + '{,/**}', globOptions ).length;
470
-
471
- return result.set( entry, numberOfMatches );
472
- }, new Map() );
473
- }
474
-
475
- // Checks whether all the required files exist in the package directory. Returns `true` if all required files exist
476
- // and `false` otherwise. It takes into account optional files and directories.
477
- function hasAllRequiredFilesToPublish( packageJson, matchedFiles ) {
478
- // If no `#files` key exist in the `package.json`, assume that the package directory structure is valid.
479
- if ( !packageJson.files ) {
480
- return true;
481
- }
482
-
483
- // Otherwise, check if every entry in the `#files` key matches at least one file.
484
- for ( const [ entry, numberOfMatches ] of matchedFiles ) {
485
- // Some files and directories are optional, so their absence in the package directory structure should not be an error.
486
- if ( isEntryOptional( packageJson.name, entry ) ) {
487
- continue;
488
- }
489
-
490
- if ( numberOfMatches === 0 ) {
491
- return false;
492
- }
493
- }
494
-
495
- return true;
496
- }
497
-
498
- // Displays all entries from the `#files` key from `package.json` with number of matched files.
499
- function showMatchedFiles( packageJson, matchedFiles ) {
500
- if ( !packageJson.files ) {
501
- log.info( 'ā„¹ļø ' + chalk.yellow(
502
- 'No `#files` key in package.json. The package directory has not been checked for the required files for release.'
503
- ) );
504
-
505
- return;
506
- }
507
-
508
- const rows = [ ...matchedFiles ].map( row => {
509
- const [ entry, numberOfMatches ] = row;
510
- const isRequired = !isEntryOptional( packageJson.name, entry );
511
- const color = isRequired && numberOfMatches === 0 ? chalk.red.bold : chalk.white;
512
-
513
- return [
514
- color( entry ),
515
- color( numberOfMatches ),
516
- color( isRequired ? 'Yes' : 'No' )
517
- ];
518
- } );
519
-
520
- const table = new Table( {
521
- head: [ 'Files pattern', 'Number of matches', 'Is required?' ],
522
- rows,
523
- style: {
524
- compact: true,
525
- head: [ 'white' ]
526
- }
527
- } );
528
-
529
- log.info( table.toString() );
530
- }
531
-
532
- // Checks whether the entry from the `#files` key is defined as optional for the package.
533
- function isEntryOptional( packageName, entry ) {
534
- if ( !optionalFilesAndDirectories ) {
535
- return false;
536
- }
537
-
538
- if ( optionalFilesAndDirectories[ packageName ] ) {
539
- return optionalFilesAndDirectories[ packageName ].includes( entry );
540
- }
541
-
542
- return optionalFilesAndDirectories.default.includes( entry );
543
- }
544
-
545
- // Checks whether specified `packageName` has been published on npm.
546
- // If so, returns its version. Otherwise returns `null` which means that
547
- // this package will be published for the first time.
548
- function getVersionFromNpm( packageName, npmTag ) {
549
- try {
550
- return exec( `npm show ${ packageName }@${ npmTag } version` ).trim();
551
- } catch ( err ) {
552
- if ( err.message.match( /npm ERR! 404/ ) ) {
553
- return null;
554
- }
555
-
556
- throw err;
557
- }
558
- }
559
- }
560
-
561
- // Checks for which packages GitHub release should be created. It compares version defined in `package.json`
562
- // and the latest release on GitHub.
563
- //
564
- // @returns {Promise}
565
- function filterPackagesToReleaseOnGitHub() {
566
- if ( !releaseOptions.github ) {
567
- return Promise.resolve();
568
- }
569
-
570
- logProcess( 'Collecting the latest releases published on GitHub...' );
571
-
572
- process.chdir( options.cwd );
573
-
574
- const packageJson = getPackageJson( options.cwd );
575
- const releaseDetails = packages.get( packageJson.name );
576
-
577
- log.info( `\nChecking "${ chalk.underline( packageJson.name ) }"...` );
578
-
579
- return getLastRelease( releaseDetails.repositoryOwner, releaseDetails.repositoryName )
580
- .then( ( { data } ) => {
581
- // It can be `null` if there is no releases on GitHub.
582
- let githubVersion = data.tag_name;
583
-
584
- if ( githubVersion ) {
585
- githubVersion = data.tag_name.replace( /^v/, '' );
586
- }
587
-
588
- logDryRun(
589
- `Versions: package.json: "${ releaseDetails.version }", GitHub: "${ githubVersion || 'initial release' }".`
590
- );
591
-
592
- releaseDetails.githubVersion = githubVersion;
593
- releaseDetails.shouldReleaseOnGithub = githubVersion !== releaseDetails.version;
594
-
595
- if ( releaseDetails.shouldReleaseOnGithub ) {
596
- log.info( 'āœ… Added to release.' );
597
-
598
- releasesOnGithub.add( options.cwd );
599
- } else {
600
- log.info( 'āŒ Nothing to release.' );
601
- }
602
- } )
603
- .catch( err => {
604
- log.warning( err );
605
- } );
606
-
607
- function getLastRelease( repositoryOwner, repositoryName ) {
608
- const requestParams = {
609
- owner: repositoryOwner,
610
- repo: repositoryName
611
- };
612
-
613
- return github.repos.getLatestRelease( requestParams )
614
- .catch( err => {
615
- // If the "last release" returned the 404 error page, it means that this release
616
- // will be the first one for specified `repositoryOwner/repositoryName` package.
617
- if ( err.status == 404 ) {
618
- return Promise.resolve( {
619
- data: {
620
- tag_name: null
621
- }
622
- } );
623
- }
624
-
625
- return Promise.reject( err );
626
- } );
627
- }
628
- }
629
-
630
- // Asks a user whether the process should be continued.
631
- //
632
- // @params {Map.<String, ReleaseDetails>} packages
633
- // @returns {Promise}
634
- function confirmRelease() {
635
- // No packages for releasing...
636
- if ( !releasesOnNpm.size && !releasesOnGithub.size ) {
637
- throw new Error( NO_RELEASE_MESSAGE );
638
- }
639
-
640
- logProcess( 'Should we continue?' );
641
-
642
- return cli.confirmPublishing( packages )
643
- .then( isConfirmed => {
644
- if ( !isConfirmed ) {
645
- throw new Error( BREAK_RELEASE_MESSAGE );
646
- }
647
- } );
648
- }
649
-
650
- // Prepares custom repositories that will be released.
651
- //
652
- // @returns {Promise}
653
- function prepareDirectoriesForCustomReleases() {
654
- if ( !releaseOptions.npm ) {
655
- return Promise.resolve();
656
- }
657
-
658
- logProcess( 'Preparing directories for custom releases...' );
659
-
660
- const customReleasesFiles = options.customReleasesFiles || {};
661
-
662
- return executeOnPackages( releasesOnNpm, repositoryPath => {
663
- let promise = Promise.resolve();
664
-
665
- process.chdir( repositoryPath );
666
-
667
- const packageJson = getPackageJson( repositoryPath );
668
-
669
- if ( !customReleases.includes( packageJson.name ) ) {
670
- return promise;
671
- }
672
-
673
- log.info( `\nPreparing "${ chalk.underline( packageJson.name ) }"...` );
674
-
675
- const tmpDir = fs.mkdtempSync( repositoryPath + path.sep + '.release-directory-' );
676
- const tmpPackageJsonPath = path.join( tmpDir, 'package.json' );
677
-
678
- releasesOnNpm.delete( repositoryPath );
679
- customReleasesOnNpm.set( tmpDir, repositoryPath );
680
-
681
- // Copy `package.json` template.
682
- promise = promise.then( () => copyFile( PACKAGE_JSON_TEMPLATE_PATH, tmpPackageJsonPath ) );
683
-
684
- // Copy files required by npm.
685
- for ( const file of additionalFiles ) {
686
- promise = promise.then( () => copyFile( path.join( repositoryPath, file ), path.join( tmpDir, file ) ) );
687
- }
688
-
689
- // Copy additional files.
690
- const customReleasesFilesForPackage = customReleasesFiles[ packageJson.name ] || [];
691
- const globOptions = {
692
- cwd: repositoryPath,
693
- dot: true,
694
- nodir: true
695
- };
696
-
697
- for ( const globPattern of customReleasesFilesForPackage ) {
698
- for ( const file of glob.sync( globPattern, globOptions ) ) {
699
- promise = promise.then( () => copyFile( path.join( repositoryPath, file ), path.join( tmpDir, file ) ) );
700
- }
701
- }
702
-
703
- return promise.then( () => {
704
- logDryRun( 'Updating package.json...' );
705
-
706
- // Update `package.json` file. It uses values from source `package.json`
707
- // but only these ones which are defined in the template.
708
- // Properties that were passed as `options.packageJsonForCustomReleases` will not be overwritten.
709
- tools.updateJSONFile( tmpPackageJsonPath, jsonFile => {
710
- const additionalPackageJson = options.packageJsonForCustomReleases[ packageJson.name ] || {};
711
-
712
- // Overwrite custom values specified in `options.packageJsonForCustomReleases`.
713
- for ( const property of Object.keys( additionalPackageJson ) ) {
714
- jsonFile[ property ] = additionalPackageJson[ property ];
715
- }
716
-
717
- // Copy values from original package.json file.
718
- for ( const property of Object.keys( jsonFile ) ) {
719
- // If the `property` is set, leave it.
720
- if ( jsonFile[ property ] ) {
721
- continue;
722
- }
723
-
724
- jsonFile[ property ] = packageJson[ property ];
725
- }
726
-
727
- return jsonFile;
728
- } );
729
- } );
730
- } );
731
- }
732
-
733
- // Publishes all packages on npm. In `dry run` mode it will create an archive instead of publishing the package.
734
- //
735
- // @returns {Promise}
736
- function releasePackagesOnNpm() {
737
- if ( !releaseOptions.npm ) {
738
- return Promise.resolve();
739
- }
740
-
741
- logProcess( 'Publishing on npm...' );
742
-
743
- const paths = [
744
- ...customReleasesOnNpm.keys(),
745
- ...releasesOnNpm
746
- ];
747
-
748
- return executeOnPackages( paths, repositoryPath => {
749
- process.chdir( repositoryPath );
750
-
751
- const packageJsonPath = path.join( repositoryPath, 'package.json' );
752
- const packageJson = getPackageJson( repositoryPath );
753
-
754
- log.info( `\nPublishing "${ chalk.underline( packageJson.name ) }" as "v${ packageJson.version }"...` );
755
- logDryRun( 'Do not panic. DRY RUN mode is active. An archive with the release will be created instead.' );
756
-
757
- const repositoryRealPath = customReleasesOnNpm.get( repositoryPath ) || repositoryPath;
758
-
759
- // If a package is written in TypeScript, the `main` field in the `package.json` file contains the path to the `index.ts` file.
760
- // However, on npm we want this field to point to the `index.js` file instead, because we publish only JavaScript files on npm.
761
- // For this reason we have to temporarily replace the extension in the `main` field while the package is being published to npm.
762
- // This change is then reverted.
763
- const hasTypeScriptEntryPoint = packageJson.main && packageJson.main.endsWith( '.ts' );
764
- const hasTypesProperty = !!packageJson.types;
765
-
766
- // TODO: The entire update phase should be done before collecting packages
767
- // TODO: to publish on npm (the `filterPackagesToReleaseOnNpm()` task).
768
- if ( hasTypeScriptEntryPoint ) {
769
- tools.updateJSONFile( packageJsonPath, jsonFile => {
770
- const { main } = jsonFile;
771
-
772
- jsonFile.main = main.replace( /\.ts$/, '.js' );
773
-
774
- if ( !hasTypesProperty ) {
775
- const typesPath = main.replace( /\.ts$/, '.d.ts' );
776
- const absoluteTypesPath = path.join( repositoryPath, typesPath );
777
-
778
- if ( fs.existsSync( absoluteTypesPath ) ) {
779
- jsonFile.types = typesPath;
780
- } else {
781
- log.warning( `āš ļø The "${ typesPath }" file does not exist and cannot be a source of typings.` );
782
- }
783
- }
784
-
785
- return jsonFile;
786
- } );
787
- }
788
-
789
- if ( dryRun ) {
790
- const archiveName = packageJson.name.replace( '@', '' ).replace( '/', '-' ) + `-${ packageJson.version }.tgz`;
791
-
792
- exec( 'npm pack' );
793
-
794
- // Move created archive from temporary directory because the directory will be removed automatically.
795
- if ( customReleasesOnNpm.has( repositoryPath ) ) {
796
- exec( `mv ${ path.join( repositoryPath, archiveName ) } ${ path.resolve( repositoryRealPath ) }` );
797
- }
798
-
799
- // Mark created archive as a file to remove.
800
- filesToRemove.add( path.join( repositoryRealPath, archiveName ) );
801
- } else {
802
- exec( `npm publish --access=public --tag ${ npmTag }` );
803
- }
804
-
805
- // Revert the previous temporary change in the `main` field, if a package is written in TypeScript, so its `main` field points
806
- // again to the `index.ts` file.
807
- if ( hasTypeScriptEntryPoint ) {
808
- tools.updateJSONFile( packageJsonPath, jsonFile => {
809
- jsonFile.main = jsonFile.main.replace( /\.js$/, '.ts' );
810
-
811
- if ( !hasTypesProperty ) {
812
- delete jsonFile.types;
813
- }
814
-
815
- return jsonFile;
816
- } );
817
- }
818
-
819
- releasedPackages.add( repositoryRealPath );
820
- } );
821
- }
822
-
823
- // Pushes all changes to remote.
824
- //
825
- // @params {Map.<String, ReleaseDetails>} packages
826
- // @returns {Promise}
827
- function pushPackages() {
828
- logProcess( 'Pushing packages to the remote...' );
829
-
830
- process.chdir( options.cwd );
831
-
832
- const packageJson = getPackageJson( options.cwd );
833
- const releaseDetails = packages.get( packageJson.name );
834
-
835
- log.info( `\nPushing "${ chalk.underline( packageJson.name ) }" package...` );
836
-
837
- if ( dryRun ) {
838
- logDryRun( `Command: "git push origin ${ releaseBranch } v${ releaseDetails.version }" would be executed.` );
839
- } else {
840
- exec( `git push origin ${ releaseBranch } v${ releaseDetails.version }` );
841
- }
842
-
843
- return Promise.resolve();
844
- }
845
-
846
- // Creates the releases on GitHub. In `dry run` mode it will just print a URL to release.
847
- //
848
- // @returns {Promise}
849
- function createReleasesOnGitHub() {
850
- if ( !releaseOptions.github ) {
851
- return Promise.resolve();
852
- }
853
-
854
- logProcess( 'Creating releases on GitHub...' );
855
-
856
- process.chdir( options.cwd );
857
-
858
- const packageJson = getPackageJson( options.cwd );
859
- const releaseDetails = packages.get( packageJson.name );
860
-
861
- log.info( `\nCreating a GitHub release for "${ packageJson.name }"...` );
862
-
863
- // eslint-disable-next-line max-len
864
- const url = `https://github.com/${ releaseDetails.repositoryOwner }/${ releaseDetails.repositoryName }/releases/tag/v${ releaseDetails.version }`;
865
-
866
- logDryRun( `Created release will be available under: ${ chalk.underline( url ) }` );
867
-
868
- if ( dryRun ) {
869
- return Promise.resolve();
870
- }
871
-
872
- const versionTag = getVersionTag( releaseDetails.version );
873
-
874
- const githubReleaseOptions = {
875
- repositoryOwner: releaseDetails.repositoryOwner,
876
- repositoryName: releaseDetails.repositoryName,
877
- version: `v${ releaseDetails.version }`,
878
- description: releaseDetails.changes,
879
- isPrerelease: versionTag !== 'latest'
880
- };
881
-
882
- return createGithubRelease( releaseOptions.token, githubReleaseOptions )
883
- .then(
884
- () => {
885
- releasedPackages.add( options.cwd );
886
-
887
- log.info( `Created the release: ${ chalk.green( url ) }` );
888
-
889
- return Promise.resolve();
890
- },
891
- err => {
892
- log.info( 'Cannot create a release on GitHub. Skipping that package.' );
893
- log.error( err );
894
-
895
- return Promise.resolve();
896
- }
897
- );
898
- }
899
-
900
- /**
901
- * Returns the version tag for the package.
902
- *
903
- * For the official release, returns the "latest" tag. For a non-official release (pre-release), returns the version tag extracted from
904
- * the package version.
905
- *
906
- * @param {String} version Version of the package to be released.
907
- * @returns {String}
908
- */
909
- function getVersionTag( version ) {
910
- const [ versionTag ] = semver.prerelease( version ) || [ 'latest' ];
911
-
912
- return versionTag;
913
- }
914
-
915
- // Removes all temporary directories that were created for publishing the custom repository.
916
- //
917
- // @returns {Promise}
918
- function removeTemporaryDirectories() {
919
- if ( !releaseOptions.npm ) {
920
- return Promise.resolve();
921
- }
922
-
923
- logProcess( 'Removing temporary directories that were created for publishing on npm...' );
924
-
925
- return executeOnPackages( customReleasesOnNpm.keys(), repositoryPath => {
926
- process.chdir( customReleasesOnNpm.get( repositoryPath ) );
927
-
928
- exec( `rm -rf ${ repositoryPath }` );
929
- } );
930
- }
931
-
932
- // Asks the user whether created archives should be removed. It so, the script will remove them.
933
- //
934
- // @returns {Promise}
935
- function removeReleaseArchives() {
936
- // This step should be skipped if packages won't be released on npm or if dry run mode is disabled.
937
- if ( !releaseOptions.npm || !dryRun ) {
938
- return Promise.resolve();
939
- }
940
-
941
- logProcess( 'Removing archives created by "npm pack" command...' );
942
-
943
- return cli.confirmRemovingFiles()
944
- .then( shouldRemove => {
945
- process.chdir( cwd );
946
-
947
- if ( !shouldRemove ) {
948
- logDryRun( 'You can remove these files manually by calling `git clean -f` command.' );
949
-
950
- return;
951
- }
952
-
953
- for ( const file of filesToRemove ) {
954
- exec( `rm ${ file }` );
955
- }
956
- } );
957
- }
958
-
959
- /**
960
- * Copy a file from the `source` path to the `destination` path.
961
- *
962
- * @param {String} source
963
- * @param {String} destination
964
- * @returns {Promise}
965
- */
966
- function copyFile( source, destination ) {
967
- return new Promise( ( resolve, reject ) => {
968
- if ( dryRun ) {
969
- log.info(
970
- `ā„¹ļø ${ chalk.grey( 'Copy file:' ) } From: "${ chalk.italic( source ) }" to "${ chalk.italic( destination ) }".`
971
- );
972
- }
973
-
974
- mkdirp.sync( path.dirname( destination ) );
975
-
976
- const stream = fs.createReadStream( source )
977
- .pipe( fs.createWriteStream( destination ) );
978
-
979
- stream.on( 'finish', resolve );
980
- stream.on( 'error', reject );
981
- } );
982
- }
983
-
984
- function exec( command ) {
985
- if ( dryRun ) {
986
- log.info( `ā„¹ļø ${ chalk.grey( 'Execute:' ) } "${ chalk.cyan( command ) }" in "${ chalk.grey.italic( process.cwd() ) }".` );
987
- }
988
-
989
- return tools.shExec( command, { verbosity: 'error' } );
990
- }
991
-
992
- function logProcess( message ) {
993
- log.info( '\nšŸ“ ' + chalk.cyan( message ) );
994
- }
995
-
996
- function logDryRun( message ) {
997
- if ( dryRun ) {
998
- log.info( 'ā„¹ļø ' + chalk.yellow( message ) );
999
- }
1000
- }
1001
- };