diff --git a/.github/workflows/build-plugin-zip.yml b/.github/workflows/build-plugin-zip.yml index 7b4c161ff3b5e..2bce11a3c0f93 100644 --- a/.github/workflows/build-plugin-zip.yml +++ b/.github/workflows/build-plugin-zip.yml @@ -6,18 +6,111 @@ on: - '**.md' push: branches: [trunk] - tags: - - 'v*' paths-ignore: - '**.md' + workflow_dispatch: + inputs: + version: + description: 'rc or stable?' + required: true jobs: + bump-version: + name: Bump version + runs-on: ubuntu-latest + if: | + github.event_name == 'workflow_dispatch' && + github.ref == 'refs/heads/trunk' && ( + github.event.inputs.version == 'rc' || + github.event.inputs.version == 'stable' + ) + outputs: + old_version: ${{ steps.get_version.outputs.old_version }} + new_version: ${{ steps.get_version.outputs.new_version }} + release_branch: ${{ steps.get_version.outputs.release_branch }} + steps: + - name: Checkout code + uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 + + - name: Compute old and new version + id: get_version + run: | + OLD_VERSION=$(jq --raw-output '.version' package.json) + echo "::set-output name=old_version::$(echo $OLD_VERSION)" + if [[ ${{ github.event.inputs.version }} == 'stable' ]]; then + NEW_VERSION=$(npx semver $OLD_VERSION -i patch) + else + if [[ $OLD_VERSION == *"rc"* ]]; then + NEW_VERSION=$(npx semver $OLD_VERSION -i prerelease) + else + # WordPress version guidelines: If minor is 9, bump major instead. + IFS='.' read -r -a OLD_VERSION_ARRAY <<< "$OLD_VERSION" + if [[ ${OLD_VERSION_ARRAY[1]} == "9" ]]; then + NEW_VERSION="$(npx semver $OLD_VERSION -i major)-rc.1" + else + NEW_VERSION="$(npx semver $OLD_VERSION -i minor)-rc.1" + fi + fi + fi + echo "::set-output name=new_version::$(echo $NEW_VERSION)" + IFS='.' read -r -a NEW_VERSION_ARRAY <<< "$NEW_VERSION" + RELEASE_BRANCH="release/${NEW_VERSION_ARRAY[0]}.${NEW_VERSION_ARRAY[1]}" + echo "::set-output name=release_branch::$(echo $RELEASE_BRANCH)" + + - name: Configure git user name and email + run: | + git config user.name "Gutenberg Repository Automation" + git config user.email gutenberg@wordpress.org + + - name: Create and switch to release branch + if: | + github.event.inputs.version == 'rc' && + ! contains( steps.get_version.outputs.old_version, 'rc' ) + run: git checkout -b "${{ steps.get_version.outputs.release_branch }}" + + - name: Switch to release branch + if: | + github.event.inputs.version == 'stable' || + contains( steps.get_version.outputs.old_version, 'rc' ) + run: | + git fetch --depth=1 origin "${{ steps.get_version.outputs.release_branch }}" + git checkout "${{ steps.get_version.outputs.release_branch }}" + + - name: Update plugin version + env: + VERSION: ${{ steps.get_version.outputs.new_version }} + run: | + cat <<< $(jq --tab --arg version "${VERSION}" '.version = $version' package.json) > package.json + cat <<< $(jq --tab --arg version "${VERSION}" '.version = $version' package-lock.json) > package-lock.json + sed -i "s/${{ steps.get_version.outputs.old_version }}/${VERSION}/g" gutenberg.php + sed -i "s/${{ steps.get_version.outputs.old_version }}/${VERSION}/g" readme.txt + + - name: Commit the version bump + run: | + git add gutenberg.php package.json package-lock.json readme.txt + git commit -m "Bump plugin version to ${{ steps.get_version.outputs.new_version }}" + git push --set-upstream origin "${{ steps.get_version.outputs.release_branch }}" + + - name: Cherry-pick to trunk + run: | + git checkout trunk + TRUNK_VERSION=$(jq --raw-output '.version' package.json) + if [[ ${{ steps.get_version.outputs.old_version }} == "$TRUNK_VERSION" ]]; then + git cherry-pick "${{ steps.get_version.outputs.release_branch }}" + git push + fi + + build: name: Build Release Artifact runs-on: ubuntu-latest + needs: bump-version + if: always() steps: - name: Checkout code uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 + with: + ref: ${{ needs.bump-version.outputs.release_branch || github.ref }} - name: Cache node modules uses: actions/cache@26968a09c0ea4f3e233fdddbafd1166051a095f6 # v2.1.4 @@ -48,26 +141,49 @@ jobs: name: gutenberg-plugin path: ./gutenberg.zip + - name: Build release notes draft + if: ${{ needs.bump-version.outputs.new_version }} + env: + VERSION: ${{ needs.bump-version.outputs.new_version }} + run: | + IFS='.' read -r -a VERSION_ARRAY <<< "${VERSION}" + MILESTONE="Gutenberg ${VERSION_ARRAY[0]}.${VERSION_ARRAY[1]}" + npm run changelog -- --milestone="$MILESTONE" --unreleased > release-notes.txt + sed -ie '1,6d' release-notes.txt + if [[ ${{ needs.bump-version.outputs.new_version }} != *"rc"* ]]; then + # Include previous RCs' release notes, if any + CHANGELOG_REGEX="=\s[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?\s=" + RC_REGEX="=\s${VERSION}(-rc\.[0-9]+)?\s=" + awk "/${RC_REGEX}/ {found=1;print;next} /${CHANGELOG_REGEX}/ {found=0} found" changelog.txt >> release-notes.txt + fi + + - name: Upload release notes artifact + if: ${{ needs.bump-version.outputs.new_version }} + uses: actions/upload-artifact@e448a9b857ee2131e752b06002bf0e093c65e571 # v2.2.2 + with: + name: release-notes + path: ./release-notes.txt + create-release: name: Create Release Draft and Attach Asset - needs: build + needs: [bump-version, build] runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/v') steps: - name: Set Release Version id: get_release_version - run: echo ::set-output name=version::$(echo $GITHUB_REF | cut -d / -f 3 | sed s/^v// | sed 's/-rc./ RC/' ) + env: + VERSION: ${{ needs.bump-version.outputs.new_version }} + run: echo ::set-output name=version::$(echo $VERSION | cut -d / -f 3 | sed 's/-rc./ RC/' ) - name: Download Plugin Zip Artifact uses: actions/download-artifact@4a7a711286f30c025902c28b541c10e147a9b843 # v2.0.8 with: name: gutenberg-plugin - - name: Extract Changelog for Release - run: | - unzip gutenberg.zip changelog.txt - CHANGELOG_REGEX="/=\s[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?\s=/" - awk -i inplace "$CHANGELOG_REGEX"'{p++;next} p==2{exit} p>=1' changelog.txt + - name: Download Release Notes Artifact + uses: actions/download-artifact@4a7a711286f30c025902c28b541c10e147a9b843 # v2.0.8 + with: + name: release-notes - name: Create Release Draft id: create_release @@ -75,11 +191,12 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - tag_name: ${{ github.ref }} + tag_name: "v${{ needs.bump-version.outputs.new_version }}" release_name: ${{ steps.get_release_version.outputs.version }} + commitish: ${{ needs.bump-version.outputs.release_branch || github.ref }} draft: true - prerelease: ${{ contains(github.ref, 'rc') }} - body_path: changelog.txt + prerelease: ${{ contains(needs.bump-version.outputs.new_version, 'rc') }} + body_path: release-notes.txt - name: Upload Release Asset id: upload-release-asset diff --git a/.github/workflows/upload-release-to-plugin-repo.yml b/.github/workflows/upload-release-to-plugin-repo.yml index 57df6897e9553..06b02347e7237 100644 --- a/.github/workflows/upload-release-to-plugin-repo.yml +++ b/.github/workflows/upload-release-to-plugin-repo.yml @@ -1,18 +1,99 @@ +name: Update Changelog and upload Gutenberg plugin to WordPress.org plugin repo + on: release: - types: [released] - -name: Upload Gutenberg plugin to WordPress.org plugin repo + types: [published] jobs: + get-release-branch: + name: Get release branch name + runs-on: ubuntu-latest + if: github.event.release.assets[0] + outputs: + release_branch: ${{ steps.get_release_branch.outputs.release_branch }} + steps: + - name: Compute release branch name + id: get_release_branch + env: + TAG: ${{ github.event.release.tag_name }} + run: | + IFS='.' read -r -a VERSION_ARRAY <<< "${TAG#v}" + RELEASE_BRANCH="release/${VERSION_ARRAY[0]}.${VERSION_ARRAY[1]}" + echo "::set-output name=release_branch::$(echo $RELEASE_BRANCH)" + + update-changelog: + name: Update Changelog on ${{ matrix.branch }} branch + runs-on: ubuntu-latest + if: github.event.release.assets[0] + needs: get-release-branch + env: + TAG: ${{ github.event.release.tag_name }} + strategy: + matrix: + include: + - branch: trunk + label: trunk + - branch: ${{ needs.get-release-branch.outputs.release_branch }} + label: release + steps: + - name: Checkout code + uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 + with: + ref: ${{ matrix.branch }} + + - name: Update the Changelog to include the release notes + run: | + # First, determine where to insert the new Changelog entry. + SERIES="${RELEASE_BRANCH#release/}" + SERIES_REGEX="=\s${SERIES}\.[0-9]+\s=" + CUT_MARKS=$( grep -nP -m 1 "${SERIES_REGEX}" changelog.txt | cut -d: -f1 ) + if [[ -z "${CUT_MARKS}" ]]; then + CHANGELOG_REGEX="=\s[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?\s=" + RC_REGEX="=\s${TAG#v}(-rc\.[0-9]+)?\s=" + CUT_MARKS=$( awk "/${RC_REGEX}/ {print NR; next}; /${CHANGELOG_REGEX}/ {print NR; exit}" changelog.txt ) + fi + BEFORE=$( echo "$CUT_MARKS" | head -n 1 ) + AFTER=$( echo "$CUT_MARKS" | tail -n 1 ) + # Okay, we have all we need to build the new Changelog. + head -n $(( "${BEFORE}" - 1 )) changelog.txt > new_changelog.txt + printf '= %s =\n\n' "${TAG#v}" >> new_changelog.txt + # Need to use a heredoc in order to preserve special characters. + cat <<- "EOF" > release_notes.txt + ${{ github.event.release.body }} + EOF + # Normalize empty lines: Trim them from beginning and end of file... + awk 'NF {p=1} p' <<< "$(< release_notes.txt)" >> new_changelog.txt + # ...then add two empty lines at the end. + printf '\n\n' >> new_changelog.txt + tail -n +"${AFTER}" changelog.txt >> new_changelog.txt + mv new_changelog.txt changelog.txt + + - name: Configure git user name and email + run: | + git config user.name "Gutenberg Repository Automation" + git config user.email gutenberg@wordpress.org + + - name: Commit the Changelog update + run: | + git add changelog.txt + git commit -m "Update Changelog for ${TAG#v}" + git push --set-upstream origin "${{ matrix.branch }}" + + - name: Upload Changelog artifact + uses: actions/upload-artifact@e448a9b857ee2131e752b06002bf0e093c65e571 # v2.2.2 + with: + name: changelog ${{ matrix.label }} + path: ./changelog.txt + upload: name: Upload Gutenberg Plugin runs-on: ubuntu-latest environment: wp.org plugin - if: github.event.release.assets[0] + needs: update-changelog + if: ${{ !github.event.release.prerelease && github.event.release.assets[0] }} env: PLUGIN_REPO_URL: 'https://plugins.svn.wordpress.org/gutenberg' - STABLE_TAG_REGEX: 'Stable tag: [0-9]\+\.[0-9]\+\.[0-9]\+\s*' + STABLE_VERSION_REGEX: '[0-9]\+\.[0-9]\+\.[0-9]\+\s*' SVN_USERNAME: ${{ secrets.svn_username }} SVN_PASSWORD: ${{ secrets.svn_password }} VERSION: ${{ github.event.release.name }} @@ -20,9 +101,11 @@ jobs: - name: Check out Gutenberg trunk from WP.org plugin repo run: svn checkout "$PLUGIN_REPO_URL/trunk" - - name: Get previous stable tag - id: get_previous_stable_tag - run: echo ::set-output name=stable_tag::$(grep "$STABLE_TAG_REGEX" ./trunk/readme.txt) + - name: Get previous stable version + id: get_previous_stable_version + env: + STABLE_TAG_REGEX: 'Stable tag: \K${{ env.STABLE_VERSION_REGEX }}' + run: echo ::set-output name=stable_version::$(grep -oP "${STABLE_TAG_REGEX}" ./trunk/readme.txt) - name: Delete everything working-directory: ./trunk @@ -39,9 +122,15 @@ jobs: - name: Replace the stable tag placeholder with the existing stable tag on the SVN repository env: STABLE_TAG_PLACEHOLDER: 'Stable tag: V\.V\.V' - STABLE_TAG: ${{ steps.get_previous_stable_tag.outputs.stable_tag }} + STABLE_TAG: 'Stable tag: ${{ steps.get_previous_stable_version.outputs.stable_version }}' run: sed -i "s/${STABLE_TAG_PLACEHOLDER}/${STABLE_TAG}/g" ./trunk/readme.txt + - name: Download Changelog Artifact + uses: actions/download-artifact@4a7a711286f30c025902c28b541c10e147a9b843 # v2.0.8 + with: + name: changelog trunk + path: trunk + - name: Commit the content changes working-directory: ./trunk run: | @@ -59,6 +148,6 @@ jobs: - name: Update the plugin's stable version working-directory: ./trunk run: | - sed -i "s/${STABLE_TAG_REGEX}/Stable tag: ${VERSION}/g" ./readme.txt + sed -i "s/Stable tag: ${STABLE_VERSION_REGEX}/Stable tag: ${VERSION}/g" ./readme.txt svn commit -m "Releasing version $VERSION" \ --no-auth-cache --non-interactive --username "$SVN_USERNAME" --password "$SVN_PASSWORD" diff --git a/bin/plugin/cli.js b/bin/plugin/cli.js index c1a978c33b131..e3a22ebd3d603 100755 --- a/bin/plugin/cli.js +++ b/bin/plugin/cli.js @@ -19,7 +19,6 @@ const catchException = ( command ) => { /** * Internal dependencies */ -const { releaseRC, releaseStable } = require( './commands/release' ); const { publishNpmLatestDistTag, publishNpmNextDistTag, @@ -27,18 +26,6 @@ const { const { getReleaseChangelog } = require( './commands/changelog' ); const { runPerformanceTests } = require( './commands/performance' ); -program - .command( 'release-plugin-rc' ) - .alias( 'rc' ) - .description( 'Release an RC version of the plugin' ) - .action( catchException( releaseRC ) ); - -program - .command( 'release-plugin-stable' ) - .alias( 'stable' ) - .description( 'Release a stable version of the plugin' ) - .action( catchException( releaseStable ) ); - program .command( 'publish-npm-packages-latest' ) .alias( 'npm-latest' ) diff --git a/bin/plugin/commands/release.js b/bin/plugin/commands/release.js deleted file mode 100644 index a554ac42f1288..0000000000000 --- a/bin/plugin/commands/release.js +++ /dev/null @@ -1,542 +0,0 @@ -/** - * External dependencies - */ -const inquirer = require( 'inquirer' ); -const fs = require( 'fs' ); -const semver = require( 'semver' ); -const Octokit = require( '@octokit/rest' ); -const { sprintf } = require( 'sprintf-js' ); - -/** - * Internal dependencies - */ -const { log, formats } = require( '../lib/logger' ); -const { askForConfirmation, runStep, readJSONFile } = require( '../lib/utils' ); -const git = require( '../lib/git' ); -const { getNextMajorVersion } = require( '../lib/version' ); -const { - getIssuesByMilestone, - getMilestoneByTitle, -} = require( '../lib/milestone' ); -const config = require( '../config' ); -const { - runGitRepositoryCloneStep, - runCleanLocalFoldersStep, - findReleaseBranchName, -} = require( './common' ); - -/** - * Creates a new release branch based on the last package.json version - * and chooses the next version number. - * - * @param {string} gitWorkingDirectoryPath Git Working Directory Path. - * @param {string} abortMessage Abort Message. - * @param {string} version the new version. - * @param {string} releaseBranch The release branch to push to. - */ -async function runReleaseBranchCreationStep( - gitWorkingDirectoryPath, - abortMessage, - version, - releaseBranch -) { - await runStep( 'Creating the release branch', abortMessage, async () => { - await askForConfirmation( - 'The Plugin version to be used is ' + - formats.success( version ) + - '. Proceed with the creation of the release branch?', - true, - abortMessage - ); - - // Creates the release branch - await git.createLocalBranch( gitWorkingDirectoryPath, releaseBranch ); - - log( - '>> The local release branch ' + - formats.success( releaseBranch ) + - ' has been successfully created.' - ); - } ); -} - -/** - * Checkouts out the release branch. - * - * @param {string} gitWorkingDirectoryPath Git Working Directory Path. - * @param {string} abortMessage Abort Message. - * @param {string} version The new version. - * @param {string} releaseBranch The release branch to checkout. - */ -async function runReleaseBranchCheckoutStep( - gitWorkingDirectoryPath, - abortMessage, - version, - releaseBranch -) { - await runStep( - 'Getting into the release branch', - abortMessage, - async () => { - await git.checkoutRemoteBranch( - gitWorkingDirectoryPath, - releaseBranch - ); - - log( - '>> The local release branch ' + - formats.success( releaseBranch ) + - ' has been successfully checked out.' - ); - - await askForConfirmation( - 'The Version to release is ' + - formats.success( version ) + - '. Proceed?', - true, - abortMessage - ); - } - ); -} - -/** - * Bump the version in the different files (package.json, package-lock.json...) - * and commit the changes. - * - * @param {string} gitWorkingDirectoryPath Git Working Directory Path. - * @param {string} version Version to use. - * @param {string} changelog Changelog to use. - * @param {string} abortMessage Abort message. - * - * @return {Promise} hash of the version bump commit. - */ -async function runBumpPluginVersionUpdateChangelogAndCommitStep( - gitWorkingDirectoryPath, - version, - changelog, - abortMessage -) { - let commitHash; - await runStep( 'Updating the plugin version', abortMessage, async () => { - const packageJsonPath = gitWorkingDirectoryPath + '/package.json'; - const packageLockPath = gitWorkingDirectoryPath + '/package-lock.json'; - const pluginFilePath = - gitWorkingDirectoryPath + '/' + config.pluginEntryPoint; - const packageJson = readJSONFile( packageJsonPath ); - const packageLock = readJSONFile( packageLockPath ); - const newPackageJson = { - ...packageJson, - version, - }; - fs.writeFileSync( - packageJsonPath, - JSON.stringify( newPackageJson, null, '\t' ) + '\n' - ); - const newPackageLock = { - ...packageLock, - version, - }; - fs.writeFileSync( - packageLockPath, - JSON.stringify( newPackageLock, null, '\t' ) + '\n' - ); - const content = fs.readFileSync( pluginFilePath, 'utf8' ); - fs.writeFileSync( - pluginFilePath, - content.replace( - ' * Version: ' + packageJson.version, - ' * Version: ' + version - ) - ); - - // Update the content of the readme.txt file - const readmePath = gitWorkingDirectoryPath + '/readme.txt'; - const readmeFileContent = fs.readFileSync( readmePath, 'utf8' ); - const newReadmeContent = - readmeFileContent.substr( - 0, - readmeFileContent.indexOf( '== Changelog ==' ) - ) + - '== Changelog ==\n\n' + - `To read the changelog for ${ config.name } ${ version }, please navigate to the release page.` + - '\n'; - fs.writeFileSync( readmePath, newReadmeContent ); - - // Update the content of the changelog.txt file - const stableVersion = version.match( /[0-9]+\.[0-9]+\.[0-9]+/ )[ 0 ]; - const changelogPath = gitWorkingDirectoryPath + '/changelog.txt'; - const changelogFileContent = fs.readFileSync( changelogPath, 'utf8' ); - const versionHeader = '= ' + version + ' =\n\n'; - const regexToSearch = /=\s([0-9]+\.[0-9]+\.[0-9]+)(-rc\.[0-9]+)?\s=\n\n/g; - let lastDifferentVersionMatch = regexToSearch.exec( - changelogFileContent - ); - if ( lastDifferentVersionMatch[ 1 ] === stableVersion ) { - lastDifferentVersionMatch = regexToSearch.exec( - changelogFileContent - ); - } - const newChangelogContent = - '== Changelog ==\n\n' + - versionHeader + - changelog + - '\n\n' + - changelogFileContent.substr( lastDifferentVersionMatch.index ); - fs.writeFileSync( changelogPath, newChangelogContent ); - - log( - '>> The plugin version and changelog have been updated successfully.' - ); - - // Commit the version bump - await askForConfirmation( - 'Please check the diff. Proceed with the version bump commit?', - true, - abortMessage - ); - commitHash = await git.commit( - gitWorkingDirectoryPath, - 'Bump plugin version to ' + version, - [ - packageJsonPath, - packageLockPath, - pluginFilePath, - readmePath, - changelogPath, - ] - ); - log( - '>> The plugin version bump and changelog update have been committed successfully.' - ); - } ); - - return commitHash; -} - -/** - * Create a local Git Tag. - * - * @param {string} gitWorkingDirectoryPath Git Working Directory Path. - * @param {string} version Version to use. - * @param {string} abortMessage Abort message. - */ -async function runCreateGitTagStep( - gitWorkingDirectoryPath, - version, - abortMessage -) { - await runStep( 'Creating the git tag', abortMessage, async () => { - await askForConfirmation( - 'Proceed with the creation of the git tag?', - true, - abortMessage - ); - - await git.createLocalTag( gitWorkingDirectoryPath, 'v' + version ); - - log( - '>> The ' + - formats.success( 'v' + version ) + - ' tag has been created successfully.' - ); - } ); -} - -/** - * Push the local Git Changes and Tags to the remote repository. - * - * @param {string} gitWorkingDirectoryPath Git Working Directory Path. - * @param {string} releaseBranch Release branch name. - * @param {string} abortMessage Abort message. - */ -async function runPushGitChangesStep( - gitWorkingDirectoryPath, - releaseBranch, - abortMessage -) { - await runStep( - 'Pushing the release branch and the tag', - abortMessage, - async () => { - await askForConfirmation( - 'The release branch and the tag are going to be pushed to the remote repository. Continue?', - true, - abortMessage - ); - - await git.pushBranchToOrigin( - gitWorkingDirectoryPath, - releaseBranch - ); - - await git.pushTagsToOrigin( gitWorkingDirectoryPath ); - } - ); -} - -/** - * Cherry-picks the version bump commit into trunk. - * - * @param {string} gitWorkingDirectoryPath Git Working Directory Path. - * @param {string} commitHash Commit to cherry-pick. - * @param {string} abortMessage Abort message. - */ -async function runCherrypickBumpCommitIntoTrunkStep( - gitWorkingDirectoryPath, - commitHash, - abortMessage -) { - await runStep( - 'Cherry-picking the bump commit into trunk', - abortMessage, - async () => { - await askForConfirmation( - 'The plugin is now released. Proceed with the version bump in the trunk branch?', - true, - abortMessage - ); - await git.discardLocalChanges( gitWorkingDirectoryPath ); - await git.resetLocalBranchAgainstOrigin( - gitWorkingDirectoryPath, - 'trunk' - ); - await git.cherrypickCommitIntoBranch( - gitWorkingDirectoryPath, - commitHash, - 'trunk' - ); - await git.pushBranchToOrigin( gitWorkingDirectoryPath, 'trunk' ); - } - ); -} - -/** - * Checks whether the milestone associated with the release has open issues or - * pull requests. Returns a promise resolving to true if there are no open issue - * or pull requests, or false otherwise. - * - * @param {string} version Release version. - * - * @return {Promise} Promise resolving to boolean indicating whether - * the milestone is clear of open issues. - */ -async function isMilestoneClear( version ) { - const { githubRepositoryOwner: owner, githubRepositoryName: name } = config; - const octokit = new Octokit(); - const milestone = await getMilestoneByTitle( - octokit, - owner, - name, - // Disable reason: valid-sprintf applies to `@wordpress/i18n` where - // strings are expected to need to be extracted, and thus variables are - // not allowed. This string will not need to be extracted. - // eslint-disable-next-line @wordpress/valid-sprintf - sprintf( config.versionMilestoneFormat, { - ...config, - ...semver.parse( version ), - } ) - ); - - if ( ! milestone ) { - // If milestone can't be found, it's not especially actionable to warn - // that the milestone isn't clear. `true` is the sensible fallback. - return true; - } - - const issues = await getIssuesByMilestone( - new Octokit(), - owner, - name, - milestone.number, - 'open' - ); - - return issues.length === 0; -} - -/** - * Release a new version. - * - * @param {boolean} isRC Whether it's an RC release or not. - * - * @return {string} The new version. - */ -async function releasePlugin( isRC = true ) { - // This is a variable that contains the abort message shown when the script is aborted. - let abortMessage = 'Aborting!'; - let version, releaseBranch; - - const temporaryFolders = []; - - await askForConfirmation( 'Ready to go? ' ); - - const { changelog } = await inquirer.prompt( [ - { - type: 'editor', - name: 'changelog', - message: 'Please provide the CHANGELOG of the release (markdown)', - }, - ] ); - - // Cloning the Git repository - const gitWorkingDirectory = await runGitRepositoryCloneStep( abortMessage ); - temporaryFolders.push( gitWorkingDirectory ); - - const packageJsonPath = gitWorkingDirectory + '/package.json'; - const packageJson = readJSONFile( packageJsonPath ); - const packageVersion = packageJson.version; - const parsedPackagedVersion = semver.parse( packageJson.version ); - const isPackageVersionRC = parsedPackagedVersion.prerelease.length > 0; - - // Are we going to release an RC? - if ( isRC ) { - // We are releasing an RC. - // If packageVersion is stable, then generate new branch and RC1. - // If packageVersion is RC, then checkout branch and inc RC. - if ( ! isPackageVersionRC ) { - version = getNextMajorVersion( packageVersion ) + '-rc.1'; - const parsedVersion = semver.parse( version ); - - releaseBranch = - 'release/' + parsedVersion.major + '.' + parsedVersion.minor; - - await runReleaseBranchCreationStep( - gitWorkingDirectory, - abortMessage, - version, - releaseBranch - ); - } else { - version = semver.inc( packageVersion, 'prerelease', 'rc' ); - releaseBranch = findReleaseBranchName( packageJsonPath ); - - await runReleaseBranchCheckoutStep( - gitWorkingDirectory, - abortMessage, - version, - releaseBranch - ); - } - } else { - // We are releasing a stable version. - // If packageVersion is stable, then checkout the branch and inc patch. - // If packageVersion is RC, then checkout the branch and inc patch, effectively removing the RC. - version = semver.inc( packageVersion, 'patch' ); - releaseBranch = findReleaseBranchName( packageJsonPath ); - - await runReleaseBranchCheckoutStep( - gitWorkingDirectory, - abortMessage, - version, - findReleaseBranchName( packageJsonPath ) - ); - } - - if ( ! ( await isMilestoneClear( version ) ) ) { - await askForConfirmation( - 'There are still open issues in the milestone. Are you sure you want to proceed?', - false, - abortMessage - ); - } - - // Bumping the version and commit. - const commitHash = await runBumpPluginVersionUpdateChangelogAndCommitStep( - gitWorkingDirectory, - version, - changelog, - abortMessage - ); - - // Creating the git tag - await runCreateGitTagStep( gitWorkingDirectory, version, abortMessage ); - - // Push the local changes - await runPushGitChangesStep( - gitWorkingDirectory, - releaseBranch, - abortMessage - ); - - abortMessage = - 'Aborting! Make sure to manually cherry-pick the ' + - formats.success( commitHash ) + - ' commit to the trunk branch.'; - - // Cherry-picking the bump commit into trunk - await runCherrypickBumpCommitIntoTrunkStep( - gitWorkingDirectory, - commitHash, - abortMessage - ); - - abortMessage = 'Aborting! The release is finished though.'; - await runCleanLocalFoldersStep( temporaryFolders, abortMessage ); - - return version; -} - -async function releaseRC() { - log( - formats.title( '\nπŸ’ƒ Time to release ' + config.name + ' πŸ•Ί\n\n' ), - 'Welcome! This tool is going to help you release a new RC version of the Plugin.\n', - 'It goes through different steps: creating the release branch, bumping the plugin version, tagging the release, and pushing the tag to GitHub.\n', - 'Once the tag is pushed to GitHub, GitHub will build the plugin ZIP, and attach it to a release draft.\n' - ); - - const version = await releasePlugin( true ); - - log( - '\n>> πŸŽ‰ The ' + - config.name + - ' version ' + - formats.success( version ) + - ' has been successfully tagged.\n', - "In a few minutes, you'll be able to find the GitHub release draft here: " + - formats.success( config.wpRepositoryReleasesURL ) + - '\n', - "Don't forget to publish the release once the draft is available!\n", - 'Thanks for performing the release!\n' - ); -} - -async function releaseStable() { - log( - formats.title( '\nπŸ’ƒ Time to release ' + config.name + ' πŸ•Ί\n\n' ), - 'Welcome! This tool is going to help you release a new stable version of the Plugin.\n', - 'It goes through different steps: bumping the plugin version, tagging the release, and pushing the tag to GitHub.\n', - 'Once the tag is pushed to GitHub, GitHub will build the plugin ZIP, and attach it to a release draft.\n', - 'To have the release uploaded to the WP.org plugin repository SVN, you need approval from a member of the ' + - config.team + - ' Team.\n' - ); - - const version = await releasePlugin( false ); - - log( - '\n>> πŸŽ‰ ' + - config.name + - ' ' + - formats.success( version ) + - ' has been successfully tagged.\n', - "In a few minutes, you'll be able to find the GitHub release draft here: " + - formats.success( config.wpRepositoryReleasesURL ) + - '\n', - "Don't forget to publish the release once the draft is available!\n", - 'Once published, the upload to the WP.org plugin repository needs approval from a member of the ' + - config.team + - ' Team at ' + - formats.success( - config.githubRepositoryURL + - 'actions/workflows/upload-release-to-plugin-repo.yml ' - ) + - '.\n', - "Thanks for performing the release! and don't forget to publish the release post.\n" - ); -} - -module.exports = { - releaseRC, - releaseStable, -}; diff --git a/docs/contributors/code/release.md b/docs/contributors/code/release.md index c08bf5cdde81c..aba102136232c 100644 --- a/docs/contributors/code/release.md +++ b/docs/contributors/code/release.md @@ -22,61 +22,35 @@ If critical bugs are discovered on stable versions of the plugin, patch versions > Note that at the time of writing, the tool doesn't support releasing consecutive RC releases. However, it is possible to use the tool for patch releases following the first stable release. -The plugin release process is entirely automated. To release the RC version of the plugin, run the following command and follow the instructions: +The plugin release process is entirely automated and happens solely on GitHub -- i.e. it doesn't require any steps to be run locally on your machine. -```bash -./bin/plugin/cli.js rc -``` - -To release a stable version, run: - -```bash -./bin/plugin/cli.js stable -``` - -During the release process, you'll be asked to provide: - -- A changelog: prepare one beforehand by following the instructions below. -- A [personal access token](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line): have one ready beforehand by visiting [this page](https://github.com/settings/tokens/new?scopes=repo,admin:org,write:packages), if you haven't got one yet. -- User and password for your GitHub account: if 2FA is enabled for your account (it should), you need to provide a personal access token instead of password (you can use the one necessary for the release). - -The release script will create a `git` tag for the release and push it to GitHub. This triggers a GitHub workflow that builds the plugin, creates a release draft (based on the Changelog), and attaches the plugin zip. This will take a couple of minutes. You will then find the release draft at https://github.com/WordPress/gutenberg/releases. You can edit it further (but note that the changes won't be propagated to `changelog.txt`). Once you're happy with it, press the 'Publish' button. +For your convenience, here's an [11-minute video walkthrough](https://youtu.be/TnSgJd3zpJY) that demonstrates the release process. It's recommended to watch this if you're unfamiliar with it. The process is also documented in the following paragraphs. -If you're releasing a stable version (rather than an RC), this will trigger a GitHub action that will upload the plugin to the WordPress.org plugin repository (SVN). This action needs approval by a member of the [`gutenberg-core` team](https://github.com/orgs/WordPress/teams/gutenberg-core). Locate the ["Upload Gutenberg plugin to WordPress.org plugin repo" workflow](https://github.com/WordPress/gutenberg/actions/workflows/upload-release-to-plugin-repo.yml) for the new version, and have it [approved](https://docs.github.com/en/actions/managing-workflow-runs/reviewing-deployments#approving-or-rejecting-a-job). +In order to start the release process, go to Gutenberg's GitHub repository's Actions tab, and locate the ["Build Gutenberg Plugin Zip" action](https://github.com/WordPress/gutenberg/actions/workflows/build-plugin-zip.yml). Note the blue banner that says "This workflow has a `workflow_dispatch` event trigger.", and expand the "Run workflow" dropdown on its right hand side. -### Manual Release Process +![Run workflow dropdown](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/contributors/code/workflow-dispatch-banner.png) -#### Creating the first Release Candidate +To release a release candidate (RC) version of the plugin, enter `rc`. To release a stable version, enter `stable`. In each case, press the green "Run workflow" button. -Releasing the first release candidate for this milestone (`x.x`) involves: +This will trigger a GitHub Actions (GHA) workflow that bumps the plugin version, builds the Gutenberg plugin .zip file, creates a release draft, and attaches the plugin .zip file to it. This part of the process typically takes a little under six minutes. You'll see that workflow appear at the top of the list, right under the blue banner. Once it's finished, it'll change its status icon from a yellow dot to a green checkmark. You can follow along in a more detailed view by clicking on the workflow. -1. writing a release blog post and changelog -2. creating the release branch -3. bumping the version and tagging the release -4. building the plugin -5. publishing the release to GitHub -6. publishing the call for testing +As soon as the workflow has finished, you'll find the release draft under https://github.com/WordPress/gutenberg/releases. The draft is pre-populated with changelog entries based on previous release candidates for this version, and any changes that have since been cherry-picked to the release branch. Thus, when releasing the first stable version of a series, make sure to delete any RC version headers (that are only there for your information), and to move the more recent changes to the corresponding sections below. Furthermore, take some time to edit the release notes into a more legible format, by grouping related entries under common bullet points. Don't rush this part -- it's important to bring the release notes into a nice shape. You don't have to do it all in one go -- you can save the draft and come back to it later. You can find some more tips on writing the release notes and post in the section below. -##### Writing the Release Post and Changelog +Only once you're happy with the shape of the release notes should you press the green "Publish release" button. This will create a `git` tag for the version, publish the release, and trigger [another GHA workflow](https://github.com/WordPress/gutenberg/actions/workflows/upload-release-to-plugin-repo.yml) that has a twofold purpose: -To generate a changelog for a release, use the changelog generator tool: +1. Use the release notes that you just edited to update `changelog.txt`, and +2. upload the new plugin version to the WordPress.org plugin repository (SVN) (only if you're releasing a stable version). -``` -npm run changelog -``` - -By default, this will search for and organize all pull requests associated with the milestone for the next version of the project. +The latter step needs approval by a member of the [`gutenberg-core` team](https://github.com/orgs/WordPress/teams/gutenberg-core). Locate the ["Upload Gutenberg plugin to WordPress.org plugin repo" workflow](https://github.com/WordPress/gutenberg/actions/workflows/upload-release-to-plugin-repo.yml) for the new version, and have it [approved](https://docs.github.com/en/actions/managing-workflow-runs/reviewing-deployments#approving-or-rejecting-a-job). -To override the default behavior, you can pass one or both of the following options. Remember to use `--` to let NPM pass the options to the script. +This will cause the new version to be available to users of WordPress all over the globe! πŸ’ƒ +You should check that folks are able to install the new version from their Dashboard. -- `--milestone `: Provide the title of the milestone for which the changelog should be generated. This should exactly match the title as shown on [the milestones page](https://github.com/WordPress/gutenberg/milestones). - - Example: `npm run changelog -- --milestone="Gutenberg 8.1"` -- `--token `: Provide a [GitHub personal access token](https://github.com/settings/tokens) for authenticating requests. This should only be necessary if you run the script frequently enough to been blocked by [rate limiting](https://developer.github.com/v3/#rate-limiting). - - Example: `npm run changelog -- --token="..."` -- `--unreleased`: Only list PRs that have been closed after the latest release in the milestone's series has been published. In other words, only list PRs that haven't been part of a release yet. - - Example: `npm run changelog -- --milestone="Gutenberg 9.8" --unreleased`. If the latest version in the 9.8 series is 9.8.3, only show PRs for the in the 9.8 series that were closed (merged) after 9.8.3 was published. +Once released, all that's left to do is writing a release post on [make.wordpress.org/core](https://make.wordpress.org/core/). You can find some tips on that below. +### Writing the Release Notes and Post -The script will output a generated changelog, grouped by pull request label. _Note that this is intended to be a starting point for release notes_. You will still want to manually review and curate the changelog entries. +The release notes draft is auto-generated by a script that looks for pull requests for the current milestone, and groups them by pull request label. +This is intended to be a starting point for release notes; you will still want to manually review and curate the changelog entries. Guidelines for proof-reading include: @@ -92,159 +66,17 @@ You should also include a performance audit at the end of the release post. You Compile this to a draft post on [make.wordpress.org/core](https://make.wordpress.org/core/); this post should be published after the actual release. -##### Creating the Release Branch - -For each milestone (let's assume it's `x.x` here), a release branch is used to release all RCs and minor releases. For the first RC of the milestone, a release branch is created from trunk. - -``` -git checkout trunk -git checkout -b release/x.x -git push origin release/x.x -``` - -##### Bumping the Version and Tagging the Release - -1. Checkout the `release/x.x` branch. -2. Create [a commit like this](https://github.com/WordPress/gutenberg/pull/13125/commits/13fa651dadc2472abb9b95f80db9d5f23e63ae9c), bumping the version number in `gutenberg.php`, `package.json`, and `package-lock.json` to `x.x.0-rc.1`. -3. Create a Pull Request from the release branch into `trunk` using the changelog as a description and ensure the tests pass properly. -4. Tag the RC version. `git tag vx.x.0-rc.1` from the release branch. -5. Push the tag `git push --tags`. -6. Merge the version bump pull request and avoid removing the release branch. - -##### Build the Plugin - -1. Run `git fetch --tags`. -2. Check out the tag for this release, you should run `git checkout vx.x.0-rc.1`. -3. Run `npm run build:plugin-zip` from the root of project. This packages a zip file with a release build of `gutenberg.zip`. - -##### Publish the Release on GitHub - -1. [Create a new release on GitHub](https://github.com/WordPress/gutenberg/releases/new). -2. If you were releasing the `x.x.0-rc.1` release candidate, label it `x.x.0-rc.1` and use the `vx.x.x-rc.1` as a tag. -3. Upload the `gutenberg.zip` file into the release. -4. Use the changelog as a description of the release. -5. Publish the release. - -Here's an example [release candidate page](https://github.com/WordPress/gutenberg/releases/tag/v4.6.0-rc.1); yours should look like that when you're finished. +If you don't have access to [make.wordpress.org/core](https://make.wordpress.org/core/), ping [someone on the Gutenberg Core team](https://github.com/orgs/WordPress/teams/gutenberg-core) in the [WordPress #core-editor Slack channel](https://wordpress.slack.com/messages/C02QB2JS7) to publish the post. -#### Creating Release Candidate Patches (done via `git cherry-pick`) +### Creating Release Candidate Patches (done via `git cherry-pick`) -If a bug is found in a release candidate and a fix is committed to `trunk`, we should include that fix in a new release candidate. To do this you'll need to use `git cherry-pick` to add these changes to the milestone's release branch. This way only fixes are added to the release candidate and not all the new code that has landed on `trunk` since tagging: +If a bug is found in a release candidate and a fix is committed to `trunk`, we should include that fix in the stable version (or optionally in another release candidate before that). To do this you'll need to use `git cherry-pick` to add these changes to the milestone's release branch. This way only the desired fixes are added rather than all the new code that has landed on `trunk` since tagging: 1. Checkout the corresponding release branch with: `git checkout release/x.x`. 2. Cherry-pick fix commits (in chronological order) with `git cherry-pick [SHA]`. -3. Create [a commit like this](https://github.com/WordPress/gutenberg/pull/13125/commits/13fa651dadc2472abb9b95f80db9d5f23e63ae9c), bumping the version number in `gutenberg.php`, `package.json`, and `package-lock.json` to `x.x.0-rc.2`. -4. Create a Pull Request from the release branch into `trunk` using the changelog as a description and ensure the tests pass properly. Note that if there there are merge conflicts, Travis CI will not run on the PR. Run tests locally using `npm run test` and `npm run test-e2e` if this happens. -5. Tag the RC version. `git tag vx.x.0-rc.2` from the release branch. -6. Push the tag `git push --tags`. -7. Create a branch for bumping the version number. `git checkout -b bump/x.x`. -8. Create a Pull Request from the `bump/x.x` branch into `trunk` using the - changelog as a description. -9. Merge the version bump pull request. -10. Follow the steps in [build the plugin](#build-the-plugin) and [publish the release on GitHub](#publish-the-release-on-github). - -You can copy the existing changelog from the previous release candidate. Let other contributors know that a new release candidate has been released in the [`#core-editor` channel](https://wordpress.slack.com/messages/C02QB2JS7) and the call for testing post. - -### Official Gutenberg Releasesβ„’ - -The process of releasing Gutenberg is similar to creating a release candidate, except we don't use the `-rc.X` in the `git` tag and we publish a new branch in the subversion repository. This updates the version available in the WordPress plugin repository and will cause WordPress sites around the world to prompt users to update to this new version. - -#### Creating a Release - -Creating a release involves: - -1. verifying the release blog post and changelog -2. bumping the version -3. building the plugin -4. publishing the new release to GitHub -5. committing to the [plugin repository] -6. publishing the release blog post - -##### Verifying the Release Post and Changelog - -1. Check the draft post on [make.wordpress.org/core](https://make.wordpress.org/core/); make sure the changelog reflects what's shipping in the release. - -##### Bumping the Version - -1. Checkout the release branch `git checkout release/x.x`. - -**Note:** This branch should never be removed or rebased. When we want to merge something from it to trunk and conflicts exist/may exist we use a temporary branch `bump/x.x`. - -2. Create [a commit like this](https://github.com/WordPress/gutenberg/commit/00d01049685f11f9bb721ad3437cb928814ab2a2#diff-b9cfc7f2cdf78a7f4b91a753d10865a2), removing the `-rc.X` from the version number in `gutenberg.php`, `package.json`, and `package-lock.json`. -3. Create a new branch called `bump/x.x` from `release/x.x` and switch to it: `git checkout -b bump/x.x`. -4. Create a pull request from `bump/x.x` to `trunk`. Verify the continuous integrations tests pass, before continuing to the next step even if conflicts exist. -5. Rebase `bump/x.x` against `origin/trunk` using `git fetch origin && git rebase origin/trunk`. -6. Force push the branch `bump/x.x` using `git push --force-with-lease`. -7. Switch to the `release/x.x` branch. Tag the version from the release branch `git tag vx.x.0`. -8. Push the tag `git push --tags`. -9. Merge the version bump pull request. - -##### Build the Plugin - -1. Run `git fetch --tags`. -2. Check out the tag for this release, you should run `git checkout vx.x.0`. -3. Run `npm run build:plugin-zip` from the root of project. This packages a zip file with a release build of `gutenberg.zip`. - -##### Publish the Release on GitHub - -1. [Create a new release on GitHub](https://github.com/WordPress/gutenberg/releases/new). -2. If you were releasing the `x.x.0` release candidate, label it `x.x.0` and use the `vx.x.x` as a tag. -3. Upload the a `gutenberg.zip` file into the release. -4. Use the changelog as a description of the release. -5. Publish the release. - -##### Commit to the Plugin Repository - -You'll need to use Subversion to publish the plugin to WordPress.org. - -1. Do an SVN checkout of `https://wordpress.org/plugins/gutenberg/trunk`: - -- If this is your first checkout, run: `svn checkout https://plugins.svn.wordpress.org/gutenberg/trunk` -- If you already have a copy, run: `svn up` - -2. Delete the contents except for the `readme.txt` and `changelog.txt` files (these files don’t exist in the `git` repo, only in Subversion). -3. Extract the contents of the zip file. -4. Edit `readme.txt`, replacing the changelog for the previous version with the current release's changelog. -5. Add the changelog for the current release to `changelog.txt`. -6. Add new files/remove deleted files from the repository: - -```bash -# Add new files: -svn st | grep '^\?' | awk '{print $2}' | xargs svn add # add the -r option to xargs if you use a linux-based OS -# Delete old files: -svn st | grep '^!' | awk '{print $2}' | xargs svn rm # add the -r option to xargs if you use a linux-based OS -``` - -7. Commit the new version: - -```bash -# Replace X.X.X with your version: -svn ci -m "Committing Gutenberg version X.X.X" -``` - -8. Tag the new version: - -```bash -svn cp https://plugins.svn.wordpress.org/gutenberg/trunk https://plugins.svn.wordpress.org/gutenberg/tags/X.X.X -m "Tagging Gutenberg version X.X.X" -``` - -9. Edit `readme.txt` to point to the new tag. The **Stable version** header in `readme.txt` should be updated to match the new release version number. After updating and committing that, the new version should be released: - -```bash -svn ci -m "Releasing Gutenberg version X.X.X" -``` - -This will cause the new version to be available to users of WordPress all over the globe! πŸ’ƒ - -You should check that folks are able to install the new version from their Dashboard. - -### Publish the Release Blog Post - -1. Publish the [make/core](https://make.wordpress.org/core/) release blog post drafted earlier. -2. Pat yourself on the back! πŸ‘ - -If you don't have access to [make.wordpress.org/core](https://make.wordpress.org/core/), ping [someone on the Gutenberg Core team](https://github.com/orgs/WordPress/teams/gutenberg-core) in the [WordPress #core-editor Slack channel](https://wordpress.slack.com/messages/C02QB2JS7) to publish the post. +3. When done, push the changes to GitHub: `git push`. +If you decide that the fixes deserve another release candidate before the stable version is published, create one by following the instructions above. Let other contributors know that a new release candidate has been released in the [`#core-editor` channel](https://wordpress.slack.com/messages/C02QB2JS7). ## Packages Releases to npm and WordPress Core Updates The Gutenberg repository mirrors the [WordPress SVN repository](https://make.wordpress.org/core/handbook/about/release-cycle/) in terms of branching for each SVN branch, a corresponding Gutenberg `wp/*` branch is created: diff --git a/docs/contributors/code/workflow-dispatch-banner.png b/docs/contributors/code/workflow-dispatch-banner.png new file mode 100644 index 0000000000000..d05c88d753f2f Binary files /dev/null and b/docs/contributors/code/workflow-dispatch-banner.png differ