From 58b3ee7a889b62fefb9cc64962819795bebce7b8 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 31 Jul 2020 19:18:37 +0100 Subject: [PATCH] Support untagged releases (#19507) * Support untagged releases * Fix --- scripts/release/README.md | 22 +++++--- .../confirm-skipped-packages.js | 2 +- ...and-tags.js => confirm-version-and-tag.js} | 24 +++----- .../release/publish-commands/parse-params.js | 19 +++++-- .../print-follow-up-instructions.js | 6 +- .../publish-commands/publish-to-npm.js | 23 +++++--- .../update-stable-version-numbers.js | 4 +- .../release/publish-commands/validate-tag.js | 55 +++++++++++++++++++ .../release/publish-commands/validate-tags.js | 38 ------------- scripts/release/publish.js | 8 +-- 10 files changed, 117 insertions(+), 84 deletions(-) rename scripts/release/publish-commands/{confirm-version-and-tags.js => confirm-version-and-tag.js} (58%) create mode 100644 scripts/release/publish-commands/validate-tag.js delete mode 100644 scripts/release/publish-commands/validate-tags.js diff --git a/scripts/release/README.md b/scripts/release/README.md index dac8bb5240dc..da04c04656eb 100644 --- a/scripts/release/README.md +++ b/scripts/release/README.md @@ -20,9 +20,13 @@ The high level process of creating releases is [documented below](#process). Ind If this is your first time running the release scripts, go to the `scripts/release` directory and run `yarn` to install the dependencies. -## Publishing Without Tags +## Publishing Untagged -The sections bekow include meaningful `--tags` in the instructions. However, keep in mind that **the `--tags` arguments is optional**, and you can omit it if you don't want to tag the release on npm at all. This can be useful when preparing breaking changes. +The sections bekow include meaningful `--tag` in the instructions. + +However, keep in mind that **the `--tag` arguments is optional**, and you can omit it if you don't want to tag the release on npm at all. This can be useful when preparing breaking changes. + +Because npm requires a tag on publish, the script does it by creating a temporary tag and deleting it afterwards. ## Publishing Next @@ -42,7 +46,7 @@ scripts/release/prepare-release-from-ci.js --build=124756 Once the build has been checked out and tested locally, you're ready to publish it: ```sh -scripts/release/publish.js --tags next +scripts/release/publish.js --tag next ``` If the OTP code expires while publishing, re-run this command and answer "y" to the questions about whether it was expected for already published packages. @@ -64,7 +68,7 @@ scripts/release/prepare-release-from-ci.js --build=124763 Once the build has been checked out and tested locally, you're ready to publish it. When publishing an experimental release, use the `experimental` tag: ```sh -scripts/release/publish.js --tags experimental +scripts/release/publish.js --tag experimental ``` If the OTP code expires while publishing, re-run this command and answer "y" to the questions about whether it was expected for already published packages. @@ -86,11 +90,13 @@ This script will prompt you to select stable version numbers for each of the pac Once this step is complete, you're ready to publish the release: ```sh -scripts/release/publish.js --tags latest +scripts/release/publish.js --tag latest ``` If the OTP code expires while publishing, re-run this command and answer "y" to the questions about whether it was expected for already published packages. +Note that publishing the `latest` tag will always update the `next` tag automatically as well so they're in sync. + After successfully publishing the release, follow the on-screen instructions to ensure that all of the appropriate post-release steps are executed. 1: You can omit the `version` param if you just want to promote the latest "next" candidate to stable. @@ -170,7 +176,9 @@ Upon completion, this script provides instructions for tagging the Git commit th **Specify a `--dry` flag when running this script if you want to skip the NPM-publish step.** In this event, the script will print the NPM commands but it will not actually run them. #### Example usage -To publish a release to NPM as both `next` and `latest`: +To publish a release to NPM as `latest`: ```sh -scripts/release/publish.js --tags latest +scripts/release/publish.js --tag latest ``` + +Note that publishing the `latest` tag will always update the `next` tag automatically as well so they're in sync. diff --git a/scripts/release/publish-commands/confirm-skipped-packages.js b/scripts/release/publish-commands/confirm-skipped-packages.js index 3e7bc03d6854..be27df70f639 100644 --- a/scripts/release/publish-commands/confirm-skipped-packages.js +++ b/scripts/release/publish-commands/confirm-skipped-packages.js @@ -6,7 +6,7 @@ const clear = require('clear'); const {confirm} = require('../utils'); const theme = require('../theme'); -const run = async ({cwd, packages, skipPackages, tags}) => { +const run = async ({cwd, packages, skipPackages}) => { if (skipPackages.length === 0) { return; } diff --git a/scripts/release/publish-commands/confirm-version-and-tags.js b/scripts/release/publish-commands/confirm-version-and-tag.js similarity index 58% rename from scripts/release/publish-commands/confirm-version-and-tags.js rename to scripts/release/publish-commands/confirm-version-and-tag.js index 2538f280a477..ed51892dd341 100644 --- a/scripts/release/publish-commands/confirm-version-and-tags.js +++ b/scripts/release/publish-commands/confirm-version-and-tag.js @@ -8,24 +8,16 @@ const {join} = require('path'); const {confirm} = require('../utils'); const theme = require('../theme'); -const run = async ({cwd, packages, tags}) => { +const run = async ({cwd, packages, tag}) => { clear(); - if (tags.length === 0) { - console.log( - theme`{spinnerSuccess ✓} You are about the publish the following packages without any tags:` - ); - } else if (tags.length === 1) { - console.log( - theme`{spinnerSuccess ✓} You are about the publish the following packages under the tag {tag ${tags}}:` - ); - } else { - console.log( - theme`{spinnerSuccess ✓} You are about the publish the following packages under the tags {tag ${tags.join( - ', ' - )}}:` - ); - } + // All latest releases are auto-tagged as next too by the script. + let tags = tag === 'latest' ? ['latest', 'next'] : [tag]; + console.log( + theme`{spinnerSuccess ✓} You are about the publish the following packages under the tag {tag ${tags.join( + ', ' + )}}:` + ); for (let i = 0; i < packages.length; i++) { const packageName = packages[i]; diff --git a/scripts/release/publish-commands/parse-params.js b/scripts/release/publish-commands/parse-params.js index 202d3570b463..2c2ae3ee8254 100644 --- a/scripts/release/publish-commands/parse-params.js +++ b/scripts/release/publish-commands/parse-params.js @@ -13,10 +13,10 @@ const paramDefinitions = [ defaultValue: false, }, { - name: 'tags', + name: 'tag', type: String, - multiple: true, - description: 'NPM tags to point to the new release.', + description: 'NPM tag to point to the new release.', + defaultValue: 'untagged', }, { name: 'skipPackages', @@ -29,10 +29,17 @@ const paramDefinitions = [ module.exports = () => { const params = commandLineArgs(paramDefinitions); - if (!params.tags || !params.tags.length) { - params.tags = []; + switch (params.tag) { + case 'latest': + case 'next': + case 'experimental': + case 'untagged': + break; + default: + console.error('Unknown tag: "' + params.tag + '"'); + process.exit(1); + break; } splitCommaParams(params.skipPackages); - splitCommaParams(params.tags); return params; }; diff --git a/scripts/release/publish-commands/print-follow-up-instructions.js b/scripts/release/publish-commands/print-follow-up-instructions.js index 614b5875ec4f..b43e679e74bb 100644 --- a/scripts/release/publish-commands/print-follow-up-instructions.js +++ b/scripts/release/publish-commands/print-follow-up-instructions.js @@ -9,7 +9,7 @@ const {join} = require('path'); const theme = require('../theme'); const {execRead} = require('../utils'); -const run = async ({cwd, packages, tags}) => { +const run = async ({cwd, packages, tag}) => { // All packages are built from a single source revision, // so it is safe to read build info from any one of them. const arbitraryPackageName = packages[0]; @@ -24,7 +24,7 @@ const run = async ({cwd, packages, tags}) => { clear(); - if (tags.length === 1 && tags[0] === 'next') { + if (tag === 'next') { console.log( theme`{header A "next" release} {version ${version}} {header has been published!}` ); @@ -35,7 +35,7 @@ const run = async ({cwd, packages, tags}) => { theme.caution`The release has been published but you're not done yet!` ); - if (tags.includes('latest')) { + if (tag === 'latest') { console.log(); console.log( theme.header`Please review and commit all local, staged changes.` diff --git a/scripts/release/publish-commands/publish-to-npm.js b/scripts/release/publish-commands/publish-to-npm.js index cc925bcc6639..97b7d7fd8daf 100644 --- a/scripts/release/publish-commands/publish-to-npm.js +++ b/scripts/release/publish-commands/publish-to-npm.js @@ -9,7 +9,7 @@ const {join} = require('path'); const {confirm, execRead} = require('../utils'); const theme = require('../theme'); -const run = async ({cwd, dry, packages, tags}, otp) => { +const run = async ({cwd, dry, packages, tag}, otp) => { clear(); for (let i = 0; i < packages.length; i++) { @@ -34,25 +34,34 @@ const run = async ({cwd, dry, packages, tags}, otp) => { // Publish the package and tag it. if (!dry) { - await exec(`npm publish --tag=${tags[0]} --otp=${otp}`, { + await exec(`npm publish --tag=${tag} --otp=${otp}`, { cwd: packagePath, }); } console.log(theme.command(` cd ${packagePath}`)); - console.log(theme.command(` npm publish --tag=${tags[0]} --otp=${otp}`)); + console.log(theme.command(` npm publish --tag=${tag} --otp=${otp}`)); - for (let j = 1; j < tags.length; j++) { + if (tag === 'latest') { + // Whenever we publish latest, also tag "next" automatically so they're in sync. if (!dry) { await exec( - `npm dist-tag add ${packageName}@${version} ${tags[j]} --otp=${otp}`, - {cwd: packagePath} + `npm dist-tag add ${packageName}@${version} next --otp=${otp}` ); } console.log( theme.command( - ` npm dist-tag add ${packageName}@${version} ${tags[j]} --otp=${otp}` + ` npm dist-tag add ${packageName}@${version} next --otp=${otp}` ) ); + } else if (tag === 'untagged') { + // npm doesn't let us publish without a tag at all, + // so for one-off publishes we clean it up ourselves. + if (!dry) { + await exec(`npm dist-tag rm ${packageName}@untagged --otp=${otp}`); + } + console.log( + theme.command(`npm dist-tag rm ${packageName}@untagged --otp=${otp}`) + ); } } } diff --git a/scripts/release/publish-commands/update-stable-version-numbers.js b/scripts/release/publish-commands/update-stable-version-numbers.js index 76a6d6f1622c..649cbbd56ef7 100644 --- a/scripts/release/publish-commands/update-stable-version-numbers.js +++ b/scripts/release/publish-commands/update-stable-version-numbers.js @@ -6,8 +6,8 @@ const {readFileSync, writeFileSync} = require('fs'); const {readJson, writeJson} = require('fs-extra'); const {join} = require('path'); -const run = async ({cwd, packages, skipPackages, tags}) => { - if (!tags.includes('latest')) { +const run = async ({cwd, packages, skipPackages, tag}) => { + if (tag !== 'latest') { // Don't update version numbers for alphas. return; } diff --git a/scripts/release/publish-commands/validate-tag.js b/scripts/release/publish-commands/validate-tag.js new file mode 100644 index 000000000000..1fa76745299f --- /dev/null +++ b/scripts/release/publish-commands/validate-tag.js @@ -0,0 +1,55 @@ +#!/usr/bin/env node + +'use strict'; + +const {readJson} = require('fs-extra'); +const {join} = require('path'); +const theme = require('../theme'); + +const run = async ({cwd, packages, tag}) => { + // Prevent a "next" release from ever being published as @latest + // All canaries share a version number, so it's okay to check any of them. + const arbitraryPackageName = packages[0]; + const packageJSONPath = join( + cwd, + 'build', + 'node_modules', + arbitraryPackageName, + 'package.json' + ); + const {version} = await readJson(packageJSONPath); + const isExperimentalVersion = version.indexOf('experimental') !== -1; + if (version.indexOf('0.0.0') === 0) { + if (tag === 'latest') { + if (isExperimentalVersion) { + console.log( + theme`{error Experimental release} {version ${version}} {error cannot be tagged as} {tag latest}` + ); + } else { + console.log( + theme`{error Next release} {version ${version}} {error cannot be tagged as} {tag latest}` + ); + } + process.exit(1); + } else if (tag === 'next' && isExperimentalVersion) { + console.log( + theme`{error Experimental release} {version ${version}} {error cannot be tagged as} {tag next}` + ); + process.exit(1); + } else if (tag === 'experimental' && !isExperimentalVersion) { + console.log( + theme`{error Next release} {version ${version}} {error cannot be tagged as} {tag experimental}` + ); + process.exit(1); + } + } else { + if (tag !== 'latest') { + console.log( + theme`{error Stable release} {version ${version}} {error cannot be tagged as} {tag ${tag}}` + ); + process.exit(1); + } + } +}; + +module.exports = run; diff --git a/scripts/release/publish-commands/validate-tags.js b/scripts/release/publish-commands/validate-tags.js deleted file mode 100644 index 3d5dcf0567a9..000000000000 --- a/scripts/release/publish-commands/validate-tags.js +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env node - -'use strict'; - -const {readJson} = require('fs-extra'); -const {join} = require('path'); -const theme = require('../theme'); - -const run = async ({cwd, packages, tags}) => { - // Prevent a "next" release from ever being published as @latest - // All canaries share a version number, so it's okay to check any of them. - const arbitraryPackageName = packages[0]; - const packageJSONPath = join( - cwd, - 'build', - 'node_modules', - arbitraryPackageName, - 'package.json' - ); - const {version} = await readJson(packageJSONPath); - if (version.indexOf('0.0.0') === 0) { - if (tags.includes('latest')) { - console.log( - theme`{error Next release} {version ${version}} {error cannot be tagged as} {tag latest}` - ); - process.exit(1); - } - } else { - if (tags.includes('next')) { - console.log( - theme`{error Stable release} {version ${version}} {error cannot be tagged as} {tag next}` - ); - process.exit(1); - } - } -}; - -module.exports = run; diff --git a/scripts/release/publish.js b/scripts/release/publish.js index 7df86c7b5019..4825af038301 100755 --- a/scripts/release/publish.js +++ b/scripts/release/publish.js @@ -8,13 +8,13 @@ const theme = require('./theme'); const checkNPMPermissions = require('./publish-commands/check-npm-permissions'); const confirmSkippedPackages = require('./publish-commands/confirm-skipped-packages'); -const confirmVersionAndTags = require('./publish-commands/confirm-version-and-tags'); +const confirmVersionAndTag = require('./publish-commands/confirm-version-and-tag'); const parseParams = require('./publish-commands/parse-params'); const printFollowUpInstructions = require('./publish-commands/print-follow-up-instructions'); const promptForOTP = require('./publish-commands/prompt-for-otp'); const publishToNPM = require('./publish-commands/publish-to-npm'); const updateStableVersionNumbers = require('./publish-commands/update-stable-version-numbers'); -const validateTags = require('./publish-commands/validate-tags'); +const validateTag = require('./publish-commands/validate-tag'); const validateSkipPackages = require('./publish-commands/validate-skip-packages'); const run = async () => { @@ -37,9 +37,9 @@ const run = async () => { } }); - await validateTags(params); + await validateTag(params); await confirmSkippedPackages(params); - await confirmVersionAndTags(params); + await confirmVersionAndTag(params); await validateSkipPackages(params); await checkNPMPermissions(params); const otp = await promptForOTP(params);