diff --git a/bin/commander.js b/bin/commander.js index e81f6ce236af3..7662f65ae8c01 100755 --- a/bin/commander.js +++ b/bin/commander.js @@ -9,6 +9,8 @@ const inquirer = require( 'inquirer' ); const semver = require( 'semver' ); const chalk = require( 'chalk' ); const fs = require( 'fs' ); +const glob = require( 'fast-glob' ); +const readline = require( 'readline' ); const rimraf = require( 'rimraf' ); const SimpleGit = require( 'simple-git/promise' ); const childProcess = require( 'child_process' ); @@ -428,7 +430,7 @@ async function runBumpPluginVersionAndCommitStep( version, changelog, abortMessa ] ); const commitData = await simpleGit.commit( 'Bump plugin version to ' + version ); commitHash = commitData.commit; - console.log( '>> The plugin version bump has been commited successfully.' ); + console.log( '>> The plugin version bump has been committed successfully.' ); } ); return commitHash; @@ -730,12 +732,114 @@ async function runWordPressReleaseBranchSyncStep( abortMessage ) { }; } +/** + * Update CHANGELOG files with the new version number for those packages that + * contain new entries. + * + * @param {string} minimumVersionBump Minimum version bump for the packages. + * @param {string} abortMessage Abort Message. + */ +async function updatePackageChangelogs( minimumVersionBump, abortMessage ) { + const changelogFiles = await glob( path.resolve( gitWorkingDirectoryPath, 'packages/*/CHANGELOG.md' ) ); + const processedPackages = await Promise.all( changelogFiles.map( async ( changelogFile ) => { + const fileStream = fs.createReadStream( changelogFile ); + + const lines = readline.createInterface( { + input: fileStream, + } ); + + let changesDetected = false; + let versionBump = null; + for await ( const line of lines ) { + // Detect unpublished changes first. + if ( line.startsWith( '## Master' ) ) { + changesDetected = true; + continue; + } + + // Skip all lines until unpublished changes found. + if ( ! changesDetected ) { + continue; + } + + // A previous published version detected. Stop processing. + if ( line.startsWith( '## ' ) ) { + break; + } + + // A major version bump required. Stop processing. + if ( line.startsWith( '### Breaking Change' ) ) { + versionBump = 'major'; + break; + } + + // A minor version bump required. Proceed to the next line. + if ( line.startsWith( '### New Feature' ) || line.startsWith( '### Deprecation' ) ) { + versionBump = 'minor'; + continue; + } + + // A version bump required. Found new changelog section. + if ( versionBump !== 'minor' && line.startsWith( '### ' ) ) { + versionBump = minimumVersionBump; + } + } + const packageName = `@wordpress/${ changelogFile.split( '/' ).reverse()[ 1 ] }`; + const { version } = readJSONFile( changelogFile.replace( 'CHANGELOG.md', 'package.json' ) ); + const nextVersion = ( versionBump !== null ) ? + semver.inc( version, versionBump ) : + null; + + return { + changelogFile, + packageName, + version, + nextVersion, + }; + } ) ); + + const changelogsToUpdate = processedPackages. + filter( ( { nextVersion } ) => nextVersion ); + + if ( changelogsToUpdate.length === 0 ) { + console.log( '>> No changes in CHANGELOG files detected.' ); + return; + } + + console.log( '>> Recommended version bumps based on the changes detected in CHANGELOG files:' ); + + const publishDate = new Date().toISOString().split( 'T' )[ 0 ]; + await Promise.all( + changelogsToUpdate + .map( async ( { changelogFile, packageName, nextVersion, version } ) => { + const content = await fs.promises.readFile( changelogFile, 'utf8' ); + await fs.promises.writeFile( changelogFile, content.replace( + '## Master', + `## Master\n\n## ${ nextVersion } (${ publishDate })` + ) ); + console.log( ` - ${ packageName }: ${ version } -> ${ nextVersion }` ); + } ) + ); + + await askForConfirmationToContinue( + `All corresponding files were updated. Commit the changes?`, + true, + abortMessage + ); + const simpleGit = SimpleGit( gitWorkingDirectoryPath ); + await simpleGit.add( './*' ); + await simpleGit.commit( 'Update changelog files' ); + console.log( '>> Changelog files changes have been committed successfully.' ); +} + /** * Prepublish to npm steps for WordPress packages. * + * @param {string} minimumVersionBump Minimum version bump for the packages. + * * @return {Object} Github release object. */ -async function prepublishPackages() { +async function prepublishPackages( minimumVersionBump ) { // This is a variable that contains the abort message shown when the script is aborted. let abortMessage = 'Aborting!'; await askForConfirmationToContinue( 'Ready to go? ' ); @@ -746,6 +850,8 @@ async function prepublishPackages() { // Checking out the WordPress release branch and doing sync with the last plugin release. const { releaseBranch } = await runWordPressReleaseBranchSyncStep( abortMessage ); + await updatePackageChangelogs( minimumVersionBump, abortMessage ); + // Push the local changes abortMessage = `Aborting! Make sure to push changes applied to WordPress release branch "${ releaseBranch }" manually.`; await runPushGitChangesStep( releaseBranch, abortMessage ); @@ -757,15 +863,15 @@ async function prepublishPackages() { program .command( 'prepublish-packages-stable' ) .alias( 'npm-stable' ) - .description( 'Prepublish to npm steps for a stable version of WordPress packages' ) + .description( 'Prepublish to npm steps for the next stable version of WordPress packages' ) .action( async () => { console.log( chalk.bold( '💃 Time to publish WordPress packages to npm 🕺\n\n' ), - 'Welcome! This tool is going to help you with prepublish to npm steps for a new stable version of WordPress packages.\n', + 'Welcome! This tool is going to help you with prepublish to npm steps for the next stable version of WordPress packages.\n', 'To perform a release you\'ll have to be a member of the WordPress Team on npm.\n' ); - await prepublishPackages(); + await prepublishPackages( 'minor' ); console.log( '\n>> 🎉 WordPress packages are ready to publish.\n', diff --git a/docs/contributors/release.md b/docs/contributors/release.md index 09c812df8f3ec..4b91573ee602e 100644 --- a/docs/contributors/release.md +++ b/docs/contributors/release.md @@ -209,7 +209,7 @@ The Gutenberg repository mirrors the [WordPress SVN repository](https://make.wor ### Synchronizing WordPress Trunk -For each Gutenberg plugin release, WordPress trunk should be synchronized with this release. This involves the following steps: +For each Gutenberg plugin release, WordPress trunk should be synchronized with this release. This involves the following steps that are automated with `./bin/commander npm-stable` command: **Note:** The WordPress `trunk` branch can be closed or in "feature-freeze" mode. Usually, this happens between the first `beta` and the first `RC` of the WordPress release cycle. During this period, the Gutenberg plugin releases should not be synchronized with WordPress Core. @@ -219,12 +219,13 @@ For each Gutenberg plugin release, WordPress trunk should be synchronized with t 3. Remove all files from the current branch: `git rm -r .`. 4. Check out all the files from the release branch: `git checkout release/x.x -- .`. 5. Commit all changes to the `wp/trunk` branch with `git commit -m "Merge changes published in the Gutenberg plugin vX.X release"` and push to the repository. +6. Update the `CHANGELOG.md` files of the packages with the new publish version calculated and commit to the `wp/trunk` branch. +Aassuming the package versions are written using this format `major.minor.patch`, make sure to bump at least the `minor` version number. For example, if the CHANGELOG of the package to be released indicates that the next unreleased version is `5.6.1`, choose `5.7.0` as a version in case of `minor` version. Now, the branch is ready to be used to publish the npm packages. -1. Run the [package release process] but when asked for the version numbers to choose for each package, (assuming the package versions are written using this format `major.minor.patch`) make sure to bump at least the `minor` version number. For example, if the CHANGELOG of the package to be released indicates that the next unreleased version is `5.6.1`, choose `5.7.0` as a version. -2. Update the `CHANGELOG.md` files of the published packages with the new released versions and commit to the `wp/trunk` branch. -3. Cherry-pick the "Publish" (created by Lerna) and the CHANGELOG update commits into the `master` branch of Gutenberg. +1. Run the [package release process] but when asked for the version numbers to choose for each package pick values based on the updated CHANGELOG files. +2. Cherry-pick the "Publish" (created by Lerna) and the CHANGELOG update commits into the `master` branch of Gutenberg. Now, the npm packages should be ready and a patch can be created and committed into WordPress `trunk`. @@ -254,7 +255,7 @@ Now, the branch is ready to be used to publish the npm packages. Now, the npm packages should be ready and a patch can be created and committed into the corresponding WordPress SVN branch. -### Standalone Package Releases +### Standalone Package Releases The following workflow is needed when packages require bug fixes or security releases to be published to _npm_ outside of a WordPress release cycle. @@ -276,7 +277,7 @@ Before porting commits check that the `wp/trunk` branch does not have any outsta Now _cherry-pick_ the commits from `master` to `wp/trunk`, use `-m 1 commithash` if the commit was a pull request merge commit: 1. `git cherry-pick -m 1 cb150a2` -2. `git push` +2. `git push` Whilst waiting for the Travis CI build for `wp/trunk` [branch to pass](https://travis-ci.com/WordPress/gutenberg/branches) identify and begin updating the `CHANGELOG.md` files: 1. `git checkout wp/trunk` @@ -317,23 +318,23 @@ Now that the changes have been committed to the `wp/trunk` branch and the Travis > @wordpress/scripts > lerna success found 3 packages ready to publish > ``` -2. Run the [package release process](https://github.com/WordPress/gutenberg/blob/master/packages/README.md#releasing-packages) but when asked for the version numbers to choose for each package use the versions you made note of above when updating each packages `CHANGELOG.md` file. +2. Run the [package release process] but when asked for the version numbers to choose for each package use the versions you made note of above when updating each packages `CHANGELOG.md` file. > Truncated example of publishing process output > ``` > npm run publish:prod -> +> > Build Progress: [==============================] 100% > lerna notice cli v3.18.2 > lerna info versioning independent > ? Select a new version for @wordpress/e2e-tests (currently 1.9.0) Patch (1.9.1) > ? Select a new version for @wordpress/jest-preset-default (currently 5.3.0) Patch (5.3.1) > ? Select a new version for @wordpress/scripts (currently 6.1.0) Patch (6.1.1) -> +> > Changes: > - @wordpress/e2e-tests: 1.9.0 => 1.9.1 > - @wordpress/jest-preset-default: 5.3.0 => 5.3.1 > - @wordpress/scripts: 6.1.0 => 6.1.1 -> +> > ? Are you sure you want to publish these packages? Yes > lerna info execute Skipping releases > lerna info git Pushing tags... @@ -359,21 +360,21 @@ Now that the packages have been published the _"chore(release): publish"_ and _" 5. Get the commit hash from the the lerna publish commit either from the terminal or [wp/trunk commits](https://github.com/WordPress/gutenberg/commits/wp/trunk) 6. Cherry-pick the `fe6ae0d` "chore(release): publish"_ commit made to `wp/trunk` 7. `git cherry-pick fe6ae0d` -8. `git push` +8. `git push` Confirm the packages dependancies do not contain `file://` links in the `dependencies` or `devdependencies` section of the packages released, e.g: > https://unpkg.com/browse/@wordpress/jest-preset-default@5.3.1/package.json > https://unpkg.com/browse/@wordpress/scripts@6.1.1/package.json > https://unpkg.com/browse/@wordpress/jest-preset-default@5.3.1/package.json -Time to announce the published changes in the #core-js and #core-editor Slack channels +Time to announce the published changes in the #core-js and #core-editor Slack channels > ``` > 📣 Successfully published: > • @wordpress/e2e-tests@1.9.1 > • @wordpress/jest-preset-default@5.3.1 > • @wordpress/scripts@6.1.1 > Lerna success published 3 packages -``` +> ``` ---------