From fc7bdd16451d23f47e57c329720483808ddb6363 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 13 Nov 2018 15:38:15 -0800 Subject: [PATCH 1/4] Download Circle CI artifact and prepare canary release --- .../print-post-build-summary.js | 6 +- scripts/release/build.js | 19 +----- .../download-build-artifacts.js | 60 +++++++++++++++++ .../prepare-canary-commands/parse-params.js | 65 +++++++++++++++++++ .../prepare-canary-commands/print-summary.js | 40 ++++++++++++ scripts/release/prepare-canary.js | 24 +++++++ scripts/release/publish.js | 17 +---- .../check-environment-variables.js | 0 scripts/release/utils.js | 16 +++++ 9 files changed, 213 insertions(+), 34 deletions(-) create mode 100644 scripts/release/prepare-canary-commands/download-build-artifacts.js create mode 100644 scripts/release/prepare-canary-commands/parse-params.js create mode 100644 scripts/release/prepare-canary-commands/print-summary.js create mode 100755 scripts/release/prepare-canary.js rename scripts/release/{build-commands => shared-commands}/check-environment-variables.js (100%) diff --git a/scripts/release/build-commands/print-post-build-summary.js b/scripts/release/build-commands/print-post-build-summary.js index a9c660d0c58bf..688159c19b622 100644 --- a/scripts/release/build-commands/print-post-build-summary.js +++ b/scripts/release/build-commands/print-post-build-summary.js @@ -43,9 +43,9 @@ module.exports = ({cwd, dry, path, version}) => { 1. Open {yellow.bold ${standaloneFixturePath}} in the browser. 2. It should say {italic "Hello world!"} 3. Next go to {yellow.bold ${packagingFixturesPath}} and run {bold node build-all.js} - 4. Install the "serve" module ({bold npm install -g serve}) - 5. Go to the repo root and {bold serve -s .} - 6. Open {blue.bold http://localhost:5000/fixtures/packaging} + 4. Install the "pushstate-server" module ({bold npm install -g pushstate-server}) + 5. Go to the repo root and {bold pushstate-server -s .} + 6. Open {blue.bold http://localhost:9000/fixtures/packaging} 7. Verify every iframe shows {italic "Hello world!"} After completing the above steps, resume the release process by running: diff --git a/scripts/release/build.js b/scripts/release/build.js index 9288af14bf27d..f69a2c3d95703 100755 --- a/scripts/release/build.js +++ b/scripts/release/build.js @@ -6,14 +6,12 @@ const {exec} = require('child_process'); // Follows the steps outlined in github.com/facebook/react/issues/10620 const run = async () => { - const chalk = require('chalk'); - const logUpdate = require('log-update'); - const {getPublicPackages, getPackages} = require('./utils'); + const {getPublicPackages, getPackages, handleError} = require('./utils'); const addGitTag = require('./build-commands/add-git-tag'); const buildArtifacts = require('./build-commands/build-artifacts'); const checkCircleCiStatus = require('./build-commands/check-circle-ci-status'); - const checkEnvironmentVariables = require('./build-commands/check-environment-variables'); + const checkEnvironmentVariables = require('./shared-commands/check-environment-variables'); const checkNpmPermissions = require('./build-commands/check-npm-permissions'); const checkPackageDependencies = require('./build-commands/check-package-dependencies'); const checkUncommittedChanges = require('./build-commands/check-uncommitted-changes'); @@ -55,18 +53,7 @@ const run = async () => { await addGitTag(params); await printPostBuildSummary(params); } catch (error) { - logUpdate.clear(); - - const message = error.message.trim().replace(/\n +/g, '\n'); - const stack = error.stack.replace(error.message, ''); - - console.log( - `${chalk.bgRed.white(' ERROR ')} ${chalk.red(message)}\n\n${chalk.gray( - stack - )}` - ); - - process.exit(1); + handleError(error); } }; diff --git a/scripts/release/prepare-canary-commands/download-build-artifacts.js b/scripts/release/prepare-canary-commands/download-build-artifacts.js new file mode 100644 index 0000000000000..e047b59732573 --- /dev/null +++ b/scripts/release/prepare-canary-commands/download-build-artifacts.js @@ -0,0 +1,60 @@ +#!/usr/bin/env node + +'use strict'; + +const chalk = require('chalk'); +const http = require('request-promise-json'); +const {exec} = require('child-process-promise'); +const {readdirSync} = require('fs'); +const {readJsonSync} = require('fs-extra'); +const {logPromise} = require('../utils'); + +const run = async ({build, cwd}) => { + // https://circleci.com/docs/2.0/artifacts/#downloading-all-artifacts-for-a-build-on-circleci + const metadataURL = `https://circleci.com/api/v1.1/project/github/facebook/react/${build}/artifacts?circle-token=${ + process.env.CIRCLE_CI_API_TOKEN + }`; + const metadata = await http.get(metadataURL, true); + const nodeModulesURL = metadata.find( + entry => entry.path === 'home/circleci/project/node_modules.tgz' + ).url; + + // Download and extract artifact + await exec(`rm -rf ${cwd}/build/node_modules*`); + await exec(`curl ${nodeModulesURL} --output ${cwd}/build/node_modules.tgz`); + await exec(`mkdir ${cwd}/build/node_modules`); + await exec( + `tar zxvf ${cwd}/build/node_modules.tgz -C ${cwd}/build/node_modules/` + ); + + // Unpack packages and parepare to publish + const compressedPackages = readdirSync('build/node_modules/'); + for (let i = 0; i < compressedPackages.length; i++) { + await exec( + `tar zxvf ${cwd}/build/node_modules/${ + compressedPackages[i] + } -C ${cwd}/build/node_modules/` + ); + const packageJSON = readJsonSync( + `${cwd}/build/node_modules/package/package.json` + ); + await exec( + `mv ${cwd}/build/node_modules/package ${cwd}/build/node_modules/${ + packageJSON.name + }` + ); + } + + // Cleanup + await exec(`rm ${cwd}/build/node_modules.tgz`); + await exec(`rm ${cwd}/build/node_modules/*.tgz`); +}; + +module.exports = async ({build, cwd}) => { + return logPromise( + run({build, cwd}), + `Downloading artifacts from Circle CI for build ${chalk.yellow.bold( + `${build}` + )}` + ); +}; diff --git a/scripts/release/prepare-canary-commands/parse-params.js b/scripts/release/prepare-canary-commands/parse-params.js new file mode 100644 index 0000000000000..1e27fe95dd372 --- /dev/null +++ b/scripts/release/prepare-canary-commands/parse-params.js @@ -0,0 +1,65 @@ +#!/usr/bin/env node + +'use strict'; + +const chalk = require('chalk'); +const commandLineArgs = require('command-line-args'); +const commandLineUsage = require('command-line-usage'); +const figlet = require('figlet'); + +const paramDefinitions = [ + { + name: 'build', + type: Number, + description: + 'Circle CI build identifier (e.g. https://circleci.com/gh/facebook/react/)', + defaultValue: false, + }, + { + name: 'path', + type: String, + alias: 'p', + description: + 'Location of React repository to release; defaults to [bold]{cwd}', + defaultValue: '.', + }, +]; + +module.exports = () => { + const params = commandLineArgs(paramDefinitions); + + if (!params.build) { + const usage = commandLineUsage([ + { + content: chalk + .hex('#61dafb') + .bold(figlet.textSync('react', {font: 'Graffiti'})), + raw: true, + }, + { + content: + 'Prepare a Circle CI build to be published to NPM as a canary.', + }, + { + header: 'Options', + optionList: paramDefinitions, + }, + { + header: 'Examples', + content: [ + { + desc: 'Example:', + example: '$ ./prepare-canary.js [bold]{--build=}[underline]{12639}', + }, + ], + }, + ]); + console.log(usage); + process.exit(1); + } + + return { + ...params, + cwd: params.path, // For script convenience + }; +}; diff --git a/scripts/release/prepare-canary-commands/print-summary.js b/scripts/release/prepare-canary-commands/print-summary.js new file mode 100644 index 0000000000000..950f2e1a300da --- /dev/null +++ b/scripts/release/prepare-canary-commands/print-summary.js @@ -0,0 +1,40 @@ +#!/usr/bin/env node + +'use strict'; + +const chalk = require('chalk'); +const {join, relative} = require('path'); + +module.exports = ({cwd, build, path}) => { + const publishPath = relative( + process.env.PWD, + join(__dirname, '../publish.js') + ); + const command = `${publishPath}` + (path ? ` -p ${path}` : ''); + + const packagingFixturesPath = join(cwd, 'fixtures/packaging'); + const standaloneFixturePath = join( + cwd, + 'fixtures/packaging/babel-standalone/dev.html' + ); + + console.log( + chalk` + {green.bold A potential canary has been prepared!} + Next there are a couple of manual steps: + + {bold.underline Smoke test the packages} + + 1. Open {yellow.bold ${standaloneFixturePath}} in the browser. + 2. It should say {italic "Hello world!"} + 3. Next go to {yellow.bold ${packagingFixturesPath}} and run {bold node build-all.js} + 4. Install the "pushstate-server" module ({bold npm install -g pushstate-server}) + 5. Go to the repo root and {bold pushstate-server -s .} + 6. Open {blue.bold http://localhost:9000/fixtures/packaging} + 7. Verify every iframe shows {italic "Hello world!"} + + After completing the above steps, you can publish this canary by running: + {yellow.bold ${command}} + `.replace(/\n +/g, '\n') + ); +}; diff --git a/scripts/release/prepare-canary.js b/scripts/release/prepare-canary.js new file mode 100755 index 0000000000000..e078021bd5252 --- /dev/null +++ b/scripts/release/prepare-canary.js @@ -0,0 +1,24 @@ +#!/usr/bin/env node + +'use strict'; + +const {handleError} = require('./utils'); + +const checkEnvironmentVariables = require('./shared-commands/check-environment-variables'); +const downloadBuildArtifacts = require('./prepare-canary-commands/download-build-artifacts'); +const parseParams = require('./prepare-canary-commands/parse-params'); +const printSummary = require('./prepare-canary-commands/print-summary'); + +const run = async () => { + try { + const params = parseParams(); + + await checkEnvironmentVariables(params); + await downloadBuildArtifacts(params); + await printSummary(params); + } catch (error) { + handleError(error); + } +}; + +run(); diff --git a/scripts/release/publish.js b/scripts/release/publish.js index c3d980b0f248c..4f242041488f8 100755 --- a/scripts/release/publish.js +++ b/scripts/release/publish.js @@ -2,9 +2,7 @@ 'use strict'; -const chalk = require('chalk'); -const logUpdate = require('log-update'); -const {getPublicPackages} = require('./utils'); +const {getPublicPackages, handleError} = require('./utils'); const checkBuildStatus = require('./publish-commands/check-build-status'); const commitChangelog = require('./publish-commands/commit-changelog'); @@ -27,18 +25,7 @@ const run = async () => { await publishToNpm(params); await printPostPublishSummary(params); } catch (error) { - logUpdate.clear(); - - const message = error.message.trim().replace(/\n +/g, '\n'); - const stack = error.stack.replace(error.message, ''); - - console.log( - `${chalk.bgRed.white(' ERROR ')} ${chalk.red(message)}\n\n${chalk.gray( - stack - )}` - ); - - process.exit(1); + handleError(error); } }; diff --git a/scripts/release/build-commands/check-environment-variables.js b/scripts/release/shared-commands/check-environment-variables.js similarity index 100% rename from scripts/release/build-commands/check-environment-variables.js rename to scripts/release/shared-commands/check-environment-variables.js diff --git a/scripts/release/utils.js b/scripts/release/utils.js index 2a9ecff5e895c..6f3f5500253bd 100644 --- a/scripts/release/utils.js +++ b/scripts/release/utils.js @@ -67,6 +67,21 @@ const getUnexecutedCommands = () => { } }; +const handleError = error => { + logUpdate.clear(); + + const message = error.message.trim().replace(/\n +/g, '\n'); + const stack = error.stack.replace(error.message, ''); + + console.log( + `${chalk.bgRed.white(' ERROR ')} ${chalk.red(message)}\n\n${chalk.gray( + stack + )}` + ); + + process.exit(1); +}; + const logPromise = async (promise, text, isLongRunningTask = false) => { const {frames, interval} = dots; @@ -119,6 +134,7 @@ module.exports = { getPackages, getPublicPackages, getUnexecutedCommands, + handleError, logPromise, runYarnTask, }; From c5209cd01a5cb3007e3f3de222d19230d2554b42 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Thu, 15 Nov 2018 12:52:57 -0800 Subject: [PATCH 2/4] New create-canary script Also updated Circle CI build script to generate and add a build-info.json file to artifacts --- packages/create-subscription/package.json | 1 + .../eslint-plugin-react-hooks/package.json | 1 + packages/jest-react/package.json | 1 + packages/react-art/package.json | 1 + packages/react-cache/package.json | 1 + packages/react-debug-tools/package.json | 1 + packages/react-dom/package.json | 1 + packages/react-is/package.json | 1 + packages/react-noop-renderer/package.json | 1 + packages/react-reconciler/package.json | 1 + packages/react-test-renderer/package.json | 1 + packages/react/package.json | 1 + packages/scheduler/package.json | 1 + scripts/circleci/add_build_info_json.sh | 5 + scripts/circleci/test_entry_point.sh | 2 + scripts/circleci/update_package_versions.sh | 5 + .../release/build-commands/build-artifacts.js | 32 ------- scripts/release/build.js | 2 +- scripts/release/ci-add-build-info-json.js | 48 ++++++++++ scripts/release/ci-update-package-versions.js | 27 ++++++ .../add-build-info-json.js | 35 +++++++ .../create-build-commands/build-artifacts.js | 25 +++++ .../copy-repo-to-temp-directory.js | 34 +++++++ .../npm-pack-and-unpack.js | 54 +++++++++++ .../update-version-numbers.js | 13 +++ scripts/release/create-canary.js | 40 ++++++++ scripts/release/package.json | 1 + scripts/release/prepare-canary.js | 4 +- .../print-canary-summary.js} | 0 scripts/release/utils.js | 96 ++++++++++++++++++- scripts/release/yarn.lock | 51 ++++++++++ 31 files changed, 448 insertions(+), 39 deletions(-) create mode 100755 scripts/circleci/add_build_info_json.sh create mode 100755 scripts/circleci/update_package_versions.sh delete mode 100644 scripts/release/build-commands/build-artifacts.js create mode 100755 scripts/release/ci-add-build-info-json.js create mode 100755 scripts/release/ci-update-package-versions.js create mode 100644 scripts/release/create-build-commands/add-build-info-json.js create mode 100644 scripts/release/create-build-commands/build-artifacts.js create mode 100644 scripts/release/create-build-commands/copy-repo-to-temp-directory.js create mode 100644 scripts/release/create-build-commands/npm-pack-and-unpack.js create mode 100644 scripts/release/create-build-commands/update-version-numbers.js create mode 100755 scripts/release/create-canary.js rename scripts/release/{prepare-canary-commands/print-summary.js => shared-commands/print-canary-summary.js} (100%) diff --git a/packages/create-subscription/package.json b/packages/create-subscription/package.json index b28cd18dd8397..31f4c2b7d8046 100644 --- a/packages/create-subscription/package.json +++ b/packages/create-subscription/package.json @@ -6,6 +6,7 @@ "files": [ "LICENSE", "README.md", + "build-info.json", "index.js", "cjs/" ], diff --git a/packages/eslint-plugin-react-hooks/package.json b/packages/eslint-plugin-react-hooks/package.json index 598758798faa8..efac100e7edbf 100644 --- a/packages/eslint-plugin-react-hooks/package.json +++ b/packages/eslint-plugin-react-hooks/package.json @@ -7,6 +7,7 @@ "files": [ "LICENSE", "README.md", + "build-info.json", "index.js", "cjs" ], diff --git a/packages/jest-react/package.json b/packages/jest-react/package.json index dc5cf76d0d7b9..d0604366855cd 100644 --- a/packages/jest-react/package.json +++ b/packages/jest-react/package.json @@ -22,6 +22,7 @@ "files": [ "LICENSE", "README.md", + "build-info.json", "index.js", "cjs/" ] diff --git a/packages/react-art/package.json b/packages/react-art/package.json index 02a608546335c..32db7ca763780 100644 --- a/packages/react-art/package.json +++ b/packages/react-art/package.json @@ -31,6 +31,7 @@ "files": [ "LICENSE", "README.md", + "build-info.json", "index.js", "cjs/", "umd/", diff --git a/packages/react-cache/package.json b/packages/react-cache/package.json index b608fb3128694..d31f140c42c0d 100644 --- a/packages/react-cache/package.json +++ b/packages/react-cache/package.json @@ -7,6 +7,7 @@ "files": [ "LICENSE", "README.md", + "build-info.json", "index.js", "cjs/", "umd/" diff --git a/packages/react-debug-tools/package.json b/packages/react-debug-tools/package.json index ea013d1aba2bd..405705dde59d5 100644 --- a/packages/react-debug-tools/package.json +++ b/packages/react-debug-tools/package.json @@ -12,6 +12,7 @@ "files": [ "LICENSE", "README.md", + "build-info.json", "index.js", "cjs/" ], diff --git a/packages/react-dom/package.json b/packages/react-dom/package.json index 0900718323f57..d92c95f7d3bc2 100644 --- a/packages/react-dom/package.json +++ b/packages/react-dom/package.json @@ -24,6 +24,7 @@ "files": [ "LICENSE", "README.md", + "build-info.json", "index.js", "profiling.js", "server.js", diff --git a/packages/react-is/package.json b/packages/react-is/package.json index 5ff1d68029541..70db5a95add56 100644 --- a/packages/react-is/package.json +++ b/packages/react-is/package.json @@ -15,6 +15,7 @@ "files": [ "LICENSE", "README.md", + "build-info.json", "index.js", "cjs/", "umd/" diff --git a/packages/react-noop-renderer/package.json b/packages/react-noop-renderer/package.json index 22f5d5d8f9ed9..51b668a366b76 100644 --- a/packages/react-noop-renderer/package.json +++ b/packages/react-noop-renderer/package.json @@ -18,6 +18,7 @@ "files": [ "LICENSE", "README.md", + "build-info.json", "index.js", "persistent.js", "cjs/" diff --git a/packages/react-reconciler/package.json b/packages/react-reconciler/package.json index 1d4bbe9416be0..906a58ed40006 100644 --- a/packages/react-reconciler/package.json +++ b/packages/react-reconciler/package.json @@ -11,6 +11,7 @@ "files": [ "LICENSE", "README.md", + "build-info.json", "index.js", "persistent.js", "reflection.js", diff --git a/packages/react-test-renderer/package.json b/packages/react-test-renderer/package.json index 2525b17a43097..6012653435e84 100644 --- a/packages/react-test-renderer/package.json +++ b/packages/react-test-renderer/package.json @@ -26,6 +26,7 @@ "files": [ "LICENSE", "README.md", + "build-info.json", "index.js", "shallow.js", "cjs/", diff --git a/packages/react/package.json b/packages/react/package.json index 7c5990406298f..623b1d1808133 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -11,6 +11,7 @@ "files": [ "LICENSE", "README.md", + "build-info.json", "index.js", "cjs/", "umd/" diff --git a/packages/scheduler/package.json b/packages/scheduler/package.json index 380a3c8e92a6f..cbba2e887964e 100644 --- a/packages/scheduler/package.json +++ b/packages/scheduler/package.json @@ -19,6 +19,7 @@ "files": [ "LICENSE", "README.md", + "build-info.json", "index.js", "tracing.js", "tracing-profiling.js", diff --git a/scripts/circleci/add_build_info_json.sh b/scripts/circleci/add_build_info_json.sh new file mode 100755 index 0000000000000..122d859535b4a --- /dev/null +++ b/scripts/circleci/add_build_info_json.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +set -e + +node ./scripts/release/ci-add-build-info-json.js diff --git a/scripts/circleci/test_entry_point.sh b/scripts/circleci/test_entry_point.sh index c452eebc00c45..5b46ec45d3b6e 100755 --- a/scripts/circleci/test_entry_point.sh +++ b/scripts/circleci/test_entry_point.sh @@ -25,6 +25,8 @@ if [ $((1 % CIRCLE_NODE_TOTAL)) -eq "$CIRCLE_NODE_INDEX" ]; then fi if [ $((2 % CIRCLE_NODE_TOTAL)) -eq "$CIRCLE_NODE_INDEX" ]; then + COMMANDS_TO_RUN+=('./scripts/circleci/add_build_info_json.sh') + COMMANDS_TO_RUN+=('./scripts/circleci/update_package_versions.sh') COMMANDS_TO_RUN+=('./scripts/circleci/build.sh') COMMANDS_TO_RUN+=('yarn test-build --maxWorkers=2') COMMANDS_TO_RUN+=('yarn test-build-prod --maxWorkers=2') diff --git a/scripts/circleci/update_package_versions.sh b/scripts/circleci/update_package_versions.sh new file mode 100755 index 0000000000000..3661e80cdef77 --- /dev/null +++ b/scripts/circleci/update_package_versions.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +set -e + +node ./scripts/release/ci-update-package-versions.js diff --git a/scripts/release/build-commands/build-artifacts.js b/scripts/release/build-commands/build-artifacts.js deleted file mode 100644 index 8687c90d40113..0000000000000 --- a/scripts/release/build-commands/build-artifacts.js +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env node - -'use strict'; - -const {exec} = require('child-process-promise'); -const {execRead, execUnlessDry, logPromise} = require('../utils'); - -const run = async ({cwd, dry, version}) => { - await exec('yarn build -- --extract-errors', {cwd}); - - const modifiedFiles = await execRead('git ls-files -m', {cwd}); - - if (modifiedFiles.includes('scripts/error-codes/codes.json')) { - await execUnlessDry('git add scripts/error-codes/codes.json', {cwd, dry}); - await execUnlessDry( - `git commit -m "Update error codes for ${version} release"`, - {cwd, dry} - ); - } - - if (modifiedFiles.includes('scripts/rollup/results.json')) { - await execUnlessDry('git add scripts/rollup/results.json', {cwd, dry}); - await execUnlessDry( - `git commit -m "Update bundle sizes for ${version} release"`, - {cwd, dry} - ); - } -}; - -module.exports = async params => { - return logPromise(run(params), 'Building artifacts', true); -}; diff --git a/scripts/release/build.js b/scripts/release/build.js index f69a2c3d95703..721770778a218 100755 --- a/scripts/release/build.js +++ b/scripts/release/build.js @@ -9,7 +9,7 @@ const run = async () => { const {getPublicPackages, getPackages, handleError} = require('./utils'); const addGitTag = require('./build-commands/add-git-tag'); - const buildArtifacts = require('./build-commands/build-artifacts'); + const buildArtifacts = require('./create-release-commands/build-artifacts'); const checkCircleCiStatus = require('./build-commands/check-circle-ci-status'); const checkEnvironmentVariables = require('./shared-commands/check-environment-variables'); const checkNpmPermissions = require('./build-commands/check-npm-permissions'); diff --git a/scripts/release/ci-add-build-info-json.js b/scripts/release/ci-add-build-info-json.js new file mode 100755 index 0000000000000..53444d2ecba1d --- /dev/null +++ b/scripts/release/ci-add-build-info-json.js @@ -0,0 +1,48 @@ +#!/usr/bin/env node + +'use strict'; + +const {exec} = require('child_process'); +const {existsSync} = require('fs'); +const {join} = require('path'); + +const run = async () => { + const {writeJson} = require('fs-extra'); + const {getBuildInfo, getPackages} = require('./utils'); + + const cwd = join(__dirname, '..', '..'); + + const {checksum, commit, branch} = await getBuildInfo(); + + const packages = getPackages(join(cwd, 'packages')); + const packagesDir = join(cwd, 'packages'); + + const buildInfoJSON = { + branch, + checksum, + commit, + environment: 'ci', + }; + + for (let i = 0; i < packages.length; i++) { + const packageName = packages[i]; + const packagePath = join(packagesDir, packageName); + + // Add build info JSON to package + if (existsSync(join(packagePath, 'npm'))) { + const buildInfoJSONPath = join(packagePath, 'npm', 'build-info.json'); + await writeJson(buildInfoJSONPath, buildInfoJSON, {spaces: 2}); + } + } +}; + +// Install (or update) release script dependencies before proceeding. +// This needs to be done before we require() the first NPM module. +exec('yarn install', {cwd: __dirname}, (error, stdout, stderr) => { + if (error) { + console.error(error); + process.exit(1); + } else { + run(); + } +}); diff --git a/scripts/release/ci-update-package-versions.js b/scripts/release/ci-update-package-versions.js new file mode 100755 index 0000000000000..b4441f3f39c33 --- /dev/null +++ b/scripts/release/ci-update-package-versions.js @@ -0,0 +1,27 @@ +#!/usr/bin/env node + +'use strict'; + +const {exec} = require('child_process'); +const {join} = require('path'); + +const run = async () => { + const {getBuildInfo, updateVersionsForCanary} = require('./utils'); + + const cwd = join(__dirname, '..', '..'); + + const {version} = await getBuildInfo(); + + await updateVersionsForCanary(cwd, version); +}; + +// Install (or update) release script dependencies before proceeding. +// This needs to be done before we require() the first NPM module. +exec('yarn install', {cwd: __dirname}, (error, stdout, stderr) => { + if (error) { + console.error(error); + process.exit(1); + } else { + run(); + } +}); diff --git a/scripts/release/create-build-commands/add-build-info-json.js b/scripts/release/create-build-commands/add-build-info-json.js new file mode 100644 index 0000000000000..816b2a146eb18 --- /dev/null +++ b/scripts/release/create-build-commands/add-build-info-json.js @@ -0,0 +1,35 @@ +#!/usr/bin/env node + +'use strict'; + +const {existsSync} = require('fs'); +const {writeJson} = require('fs-extra'); +const {join} = require('path'); +const {getPackages, logPromise} = require('../utils'); + +const run = async ({branch, checksum, commit, tempDirectory}) => { + const packages = getPackages(join(tempDirectory, 'packages')); + const packagesDir = join(tempDirectory, 'packages'); + + const buildInfoJSON = { + branch, + checksum, + commit, + environment: 'local', + }; + + for (let i = 0; i < packages.length; i++) { + const packageName = packages[i]; + const packagePath = join(packagesDir, packageName); + + // Add build info JSON to package + if (existsSync(join(packagePath, 'npm'))) { + const buildInfoJSONPath = join(packagePath, 'npm', 'build-info.json'); + await writeJson(buildInfoJSONPath, buildInfoJSON, {spaces: 2}); + } + } +}; + +module.exports = async params => { + return logPromise(run(params), 'Adding build metadata to packages'); +}; diff --git a/scripts/release/create-build-commands/build-artifacts.js b/scripts/release/create-build-commands/build-artifacts.js new file mode 100644 index 0000000000000..3435a625b584d --- /dev/null +++ b/scripts/release/create-build-commands/build-artifacts.js @@ -0,0 +1,25 @@ +#!/usr/bin/env node + +'use strict'; + +const {exec} = require('child-process-promise'); +const {join} = require('path'); +const {logPromise} = require('../utils'); + +const run = async ({cwd, dry, tempDirectory}) => { + const defaultOptions = { + cwd: tempDirectory, + }; + + await exec('yarn install', defaultOptions); + await exec('yarn build -- --extract-errors', defaultOptions); + + const tempNodeModulesPath = join(tempDirectory, 'build', 'node_modules'); + const buildPath = join(cwd, 'build'); + + await exec(`cp -r ${tempNodeModulesPath} ${buildPath}`); +}; + +module.exports = async params => { + return logPromise(run(params), 'Building artifacts', true); +}; diff --git a/scripts/release/create-build-commands/copy-repo-to-temp-directory.js b/scripts/release/create-build-commands/copy-repo-to-temp-directory.js new file mode 100644 index 0000000000000..0490efbe67d17 --- /dev/null +++ b/scripts/release/create-build-commands/copy-repo-to-temp-directory.js @@ -0,0 +1,34 @@ +#!/usr/bin/env node + +'use strict'; + +const chalk = require('chalk'); +const {exec} = require('child-process-promise'); +const {join} = require('path'); +const {tmpdir} = require('os'); +const {logPromise} = require('../utils'); + +const run = async ({commit, cwd, tempDirectory}) => { + const directory = `react-${commit}`; + const temp = tmpdir(); + + if (tempDirectory !== join(tmpdir(), directory)) { + throw Error(`Unexpected temporary directory "${tempDirectory}"`); + } + + await exec(`rm -rf ${directory}`, {cwd: temp}); + await exec(`git archive --format=tar --output=${temp}/react.tgz ${commit}`, { + cwd, + }); + await exec(`mkdir ${directory}`, {cwd: temp}); + await exec(`tar -xf ./react.tgz -C ./${directory}`, {cwd: temp}); +}; + +module.exports = async params => { + return logPromise( + run(params), + `Copying React repo to temporary directory (${chalk.gray( + params.tempDirectory + )})` + ); +}; diff --git a/scripts/release/create-build-commands/npm-pack-and-unpack.js b/scripts/release/create-build-commands/npm-pack-and-unpack.js new file mode 100644 index 0000000000000..76f3cf85cf882 --- /dev/null +++ b/scripts/release/create-build-commands/npm-pack-and-unpack.js @@ -0,0 +1,54 @@ +#!/usr/bin/env node + +'use strict'; + +const {join} = require('path'); +const {exec} = require('child-process-promise'); +const {readdirSync} = require('fs'); +const {readJsonSync} = require('fs-extra'); +const {logPromise} = require('../utils'); + +const run = async ({cwd, dry, tempDirectory}) => { + // Cleanup from previous build. + await exec(`rm -rf ${cwd}/build`); + + // NPM pack all built packages. + // We do this to ensure that the package.json files array is correct. + const builtPackages = readdirSync(join(tempDirectory, 'build/node_modules/')); + for (let i = 0; i < builtPackages.length; i++) { + await exec(`npm pack ./${builtPackages[i]}`, { + cwd: `${tempDirectory}/build/node_modules/`, + }); + } + + await exec('mkdir build'); + await exec('mkdir build/node_modules'); + await exec( + `cp -r ${tempDirectory}/build/node_modules/*.tgz ${cwd}/build/node_modules/` + ); + + // Unpack packages and parepare to publish. + const compressedPackages = readdirSync('build/node_modules/'); + for (let i = 0; i < compressedPackages.length; i++) { + await exec( + `tar -zxvf ${cwd}/build/node_modules/${ + compressedPackages[i] + } -C ${cwd}/build/node_modules/` + ); + const packageJSON = readJsonSync( + `${cwd}/build/node_modules/package/package.json` + ); + await exec( + `mv ${cwd}/build/node_modules/package ${cwd}/build/node_modules/${ + packageJSON.name + }` + ); + } + + // Cleanup. + await exec(`rm ${cwd}/build/node_modules/*.tgz`); +}; + +module.exports = async params => { + return logPromise(run(params), 'Packing artifacts'); +}; diff --git a/scripts/release/create-build-commands/update-version-numbers.js b/scripts/release/create-build-commands/update-version-numbers.js new file mode 100644 index 0000000000000..f0adc9ae77237 --- /dev/null +++ b/scripts/release/create-build-commands/update-version-numbers.js @@ -0,0 +1,13 @@ +#!/usr/bin/env node + +'use strict'; + +const chalk = require('chalk'); +const {logPromise, updateVersionsForCanary} = require('../utils'); + +module.exports = async ({tempDirectory, version}) => { + return logPromise( + updateVersionsForCanary(tempDirectory, version), + `Updating version numbers (${chalk.yellow.bold(version)})` + ); +}; diff --git a/scripts/release/create-canary.js b/scripts/release/create-canary.js new file mode 100755 index 0000000000000..0c9652d84fa3e --- /dev/null +++ b/scripts/release/create-canary.js @@ -0,0 +1,40 @@ +#!/usr/bin/env node + +'use strict'; + +const {tmpdir} = require('os'); +const {join} = require('path'); +const {getBuildInfo, handleError} = require('./utils'); + +// This local build script exists for special case, manual builds. +// The typical suggesgted release process is to create a canary from a CI artifact. +// This build script is optimized for speed and simplicity. +// It doesn't run all of the tests that the CI environment runs. +// You're expected to run those manually before publishing a release. + +const addBuildInfoJSON = require('./create-build-commands/add-build-info-json'); +const buildArtifacts = require('./create-build-commands/build-artifacts'); +const copyRepoToTempDirectory = require('./create-build-commands/copy-repo-to-temp-directory'); +const npmPackAndUnpack = require('./create-build-commands/npm-pack-and-unpack'); +const printCanarySummary = require('./shared-commands/print-canary-summary'); +const updateVersionNumbers = require('./create-build-commands/update-version-numbers'); + +const run = async () => { + try { + const cwd = join(__dirname, '..', '..'); + const {branch, checksum, commit, version} = await getBuildInfo(); + const tempDirectory = join(tmpdir(), `react-${commit}`); + const params = {branch, checksum, commit, cwd, tempDirectory, version}; + + await copyRepoToTempDirectory(params); + await updateVersionNumbers(params); + await addBuildInfoJSON(params); + await buildArtifacts(params); + await npmPackAndUnpack(params); + await printCanarySummary(params); + } catch (error) { + handleError(error); + } +}; + +run(); diff --git a/scripts/release/package.json b/scripts/release/package.json index c5f9f99af3207..21e2d3b2ddc56 100644 --- a/scripts/release/package.json +++ b/scripts/release/package.json @@ -11,6 +11,7 @@ "command-line-args": "^4.0.7", "command-line-usage": "^4.0.1", "figlet": "^1.2.0", + "folder-hash": "^2.1.2", "fs-extra": "^4.0.2", "log-update": "^2.1.0", "prompt-promise": "^1.0.3", diff --git a/scripts/release/prepare-canary.js b/scripts/release/prepare-canary.js index e078021bd5252..2a459722d360b 100755 --- a/scripts/release/prepare-canary.js +++ b/scripts/release/prepare-canary.js @@ -7,7 +7,7 @@ const {handleError} = require('./utils'); const checkEnvironmentVariables = require('./shared-commands/check-environment-variables'); const downloadBuildArtifacts = require('./prepare-canary-commands/download-build-artifacts'); const parseParams = require('./prepare-canary-commands/parse-params'); -const printSummary = require('./prepare-canary-commands/print-summary'); +const printCanarySummary = require('./shared-commands/print-canary-summary'); const run = async () => { try { @@ -15,7 +15,7 @@ const run = async () => { await checkEnvironmentVariables(params); await downloadBuildArtifacts(params); - await printSummary(params); + await printCanarySummary(params); } catch (error) { handleError(error); } diff --git a/scripts/release/prepare-canary-commands/print-summary.js b/scripts/release/shared-commands/print-canary-summary.js similarity index 100% rename from scripts/release/prepare-canary-commands/print-summary.js rename to scripts/release/shared-commands/print-canary-summary.js diff --git a/scripts/release/utils.js b/scripts/release/utils.js index 6f3f5500253bd..c909c8679b818 100644 --- a/scripts/release/utils.js +++ b/scripts/release/utils.js @@ -2,8 +2,10 @@ const chalk = require('chalk'); const {dots} = require('cli-spinners'); -const {exec} = require('child-process-promise'); -const {readdirSync, readFileSync, statSync} = require('fs'); +const {exec, spawn} = require('child-process-promise'); +const {hashElement} = require('folder-hash'); +const {readdirSync, readFileSync, statSync, writeFileSync} = require('fs'); +const {readJson, writeJson} = require('fs-extra'); const logUpdate = require('log-update'); const {join} = require('path'); @@ -23,9 +25,31 @@ const execUnlessDry = async (command, {cwd, dry}) => { } }; -const getPackages = () => { - const packagesRoot = join(__dirname, '..', '..', 'packages'); +const getBuildInfo = async () => { + const cwd = join(__dirname, '..', '..'); + + const branch = await execRead('git branch | grep \\* | cut -d " " -f2', { + cwd, + }); + const commit = await execRead('git show -s --format=%h', {cwd}); + const checksum = await getChecksumForCurrentRevision(cwd); + const version = `0.0.0-${commit}`; + + return {branch, checksum, commit, version}; +}; +const getChecksumForCurrentRevision = async cwd => { + const packagesDir = join(cwd, 'packages'); + const hashedPackages = await hashElement(packagesDir, { + encoding: 'hex', + files: {exclude: ['.DS_Store']}, + }); + return hashedPackages.hash.slice(0, 7); +}; + +const getPackages = ( + packagesRoot = join(__dirname, '..', '..', 'packages') +) => { return readdirSync(packagesRoot).filter(dir => { const packagePath = join(packagesRoot, dir, 'package.json'); @@ -128,13 +152,77 @@ const runYarnTask = async (cwd, task, errorMessage) => { } }; +const spawnCommand = (command, options) => + spawn(command, { + cwd: join(__dirname, '..', '..'), + encoding: 'utf-8', + env: process.env, + shell: true, + stdio: [process.stdin, process.stdout, process.stderr], + ...options, + }); + +const updateVersionsForCanary = async (cwd, version) => { + const packages = getPackages(join(cwd, 'packages')); + const packagesDir = join(cwd, 'packages'); + + // Update the shared React version source file. + // This is bundled into built renderers. + // The promote script will replace this with a final version later. + const reactVersionPath = join(cwd, 'packages/shared/ReactVersion.js'); + const reactVersion = readFileSync(reactVersionPath, 'utf8').replace( + /module\.exports = '[^']+';/, + `module.exports = '${version}';` + ); + writeFileSync(reactVersionPath, reactVersion); + + // Update the root package.json. + // This is required to pass a later version check script. + { + const packageJSONPath = join(cwd, 'package.json'); + const packageJSON = await readJson(packageJSONPath); + packageJSON.version = version; + await writeJson(packageJSONPath, packageJSON, {spaces: 2}); + } + + for (let i = 0; i < packages.length; i++) { + const packageName = packages[i]; + const packagePath = join(packagesDir, packageName); + + // Update version numbers in package JSONs + const packageJSONPath = join(packagePath, 'package.json'); + const packageJSON = await readJson(packageJSONPath); + packageJSON.version = version; + + // Also update inter-package dependencies. + // Canary releases always have exact version matches. + // The promote script may later relax these (e.g. "^x.x.x") based on source package JSONs. + const {dependencies, peerDependencies} = packageJSON; + for (let j = 0; j < packages.length; j++) { + const dependencyName = packages[j]; + if (dependencies && dependencies[dependencyName]) { + dependencies[dependencyName] = version; + } + if (peerDependencies && peerDependencies[dependencyName]) { + peerDependencies[dependencyName] = version; + } + } + + await writeJson(packageJSONPath, packageJSON, {spaces: 2}); + } +}; + module.exports = { execRead, execUnlessDry, + getBuildInfo, + getChecksumForCurrentRevision, getPackages, getPublicPackages, getUnexecutedCommands, handleError, logPromise, runYarnTask, + spawnCommand, + updateVersionsForCanary, }; diff --git a/scripts/release/yarn.lock b/scripts/release/yarn.lock index 44ea4e5703a2f..240210db6c7e4 100644 --- a/scripts/release/yarn.lock +++ b/scripts/release/yarn.lock @@ -63,6 +63,11 @@ aws4@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + bcrypt-pbkdf@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" @@ -81,6 +86,14 @@ boom@5.x.x: dependencies: hoek "4.x.x" +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" @@ -148,6 +161,11 @@ command-line-usage@^4.0.1: table-layout "^0.4.1" typical "^2.6.1" +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + core-util-is@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -171,6 +189,13 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +debug@^3.1.0: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + deep-extend@~0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.5.0.tgz#6ef4a09b05f98b0e358d6d93d4ca3caec6672803" @@ -212,6 +237,15 @@ find-replace@^1.0.3: array-back "^1.0.4" test-value "^2.1.0" +folder-hash@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/folder-hash/-/folder-hash-2.1.2.tgz#7109f9cd0cbca271936d1b5544b156d6571e6cfd" + integrity sha512-PmMwEZyNN96EMshf7sek4OIB7ADNsHOJ7VIw7pO0PBI0BNfEsi7U8U56TBjjqqwQ0WuBv8se0HEfmbw5b/Rk+w== + dependencies: + debug "^3.1.0" + graceful-fs "~4.1.11" + minimatch "~3.0.4" + forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" @@ -242,6 +276,11 @@ graceful-fs@^4.1.2, graceful-fs@^4.1.6: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" +graceful-fs@~4.1.11: + version "4.1.15" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" + integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA== + har-schema@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" @@ -372,6 +411,18 @@ mimic-fn@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18" +minimatch@~3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +ms@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + native-or-another@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/native-or-another/-/native-or-another-2.0.0.tgz#17a567f92beea9cd71acff96a7681a735eca3bff" From 5bf84d2929a3b1d8a71fca91a48c61df75145f3d Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Thu, 15 Nov 2018 12:54:46 -0800 Subject: [PATCH 3/4] Empty commit to trigger Circle CI From f80452849d334cddb24a7f42726c17f5eaf11c4d Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Fri, 16 Nov 2018 10:37:10 -0800 Subject: [PATCH 4/4] New prepare-canary release script --- scripts/release/ci-add-build-info-json.js | 4 + scripts/release/ci-update-package-versions.js | 4 + scripts/release/create-canary.js | 4 +- scripts/release/package.json | 1 + scripts/release/prepare-canary.js | 4 +- .../check-out-packages.js | 29 ++++ .../confirm-stable-version-numbers.js | 56 +++++++ .../guess-stable-version-numbers.js | 34 +++++ .../prepare-stable-commands/parse-params.js | 52 +++++++ .../update-stable-version-numbers.js | 144 ++++++++++++++++++ scripts/release/prepare-stable.js | 36 +++++ ...summary.js => print-prerelease-summary.js} | 14 +- scripts/release/utils.js | 11 ++ scripts/release/yarn.lock | 12 ++ 14 files changed, 394 insertions(+), 11 deletions(-) create mode 100644 scripts/release/prepare-stable-commands/check-out-packages.js create mode 100644 scripts/release/prepare-stable-commands/confirm-stable-version-numbers.js create mode 100644 scripts/release/prepare-stable-commands/guess-stable-version-numbers.js create mode 100644 scripts/release/prepare-stable-commands/parse-params.js create mode 100644 scripts/release/prepare-stable-commands/update-stable-version-numbers.js create mode 100755 scripts/release/prepare-stable.js rename scripts/release/shared-commands/{print-canary-summary.js => print-prerelease-summary.js} (71%) diff --git a/scripts/release/ci-add-build-info-json.js b/scripts/release/ci-add-build-info-json.js index 53444d2ecba1d..a2f30fc4492f7 100755 --- a/scripts/release/ci-add-build-info-json.js +++ b/scripts/release/ci-add-build-info-json.js @@ -2,6 +2,10 @@ 'use strict'; +// This script is run by Circle CI (see ../scripts/circleci). +// It is not meant to be run as part of the local build or publish process. +// It exists to share code between the Node release scripts and CI bash scripts. + const {exec} = require('child_process'); const {existsSync} = require('fs'); const {join} = require('path'); diff --git a/scripts/release/ci-update-package-versions.js b/scripts/release/ci-update-package-versions.js index b4441f3f39c33..0d5e65ec04a7e 100755 --- a/scripts/release/ci-update-package-versions.js +++ b/scripts/release/ci-update-package-versions.js @@ -2,6 +2,10 @@ 'use strict'; +// This script is run by Circle CI (see ../scripts/circleci). +// It is not meant to be run as part of the local build or publish process. +// It exists to share code between the Node release scripts and CI bash scripts. + const {exec} = require('child_process'); const {join} = require('path'); diff --git a/scripts/release/create-canary.js b/scripts/release/create-canary.js index 0c9652d84fa3e..3a3584276e853 100755 --- a/scripts/release/create-canary.js +++ b/scripts/release/create-canary.js @@ -16,7 +16,7 @@ const addBuildInfoJSON = require('./create-build-commands/add-build-info-json'); const buildArtifacts = require('./create-build-commands/build-artifacts'); const copyRepoToTempDirectory = require('./create-build-commands/copy-repo-to-temp-directory'); const npmPackAndUnpack = require('./create-build-commands/npm-pack-and-unpack'); -const printCanarySummary = require('./shared-commands/print-canary-summary'); +const printPrereleaseSummary = require('./shared-commands/print-prerelease-summary'); const updateVersionNumbers = require('./create-build-commands/update-version-numbers'); const run = async () => { @@ -31,7 +31,7 @@ const run = async () => { await addBuildInfoJSON(params); await buildArtifacts(params); await npmPackAndUnpack(params); - await printCanarySummary(params); + await printPrereleaseSummary(params); } catch (error) { handleError(error); } diff --git a/scripts/release/package.json b/scripts/release/package.json index 21e2d3b2ddc56..dd40d3cfdbedc 100644 --- a/scripts/release/package.json +++ b/scripts/release/package.json @@ -14,6 +14,7 @@ "folder-hash": "^2.1.2", "fs-extra": "^4.0.2", "log-update": "^2.1.0", + "print-diff": "^0.1.1", "prompt-promise": "^1.0.3", "request-promise-json": "^1.0.4", "semver": "^5.4.1" diff --git a/scripts/release/prepare-canary.js b/scripts/release/prepare-canary.js index 2a459722d360b..5302bd95f251f 100755 --- a/scripts/release/prepare-canary.js +++ b/scripts/release/prepare-canary.js @@ -7,7 +7,7 @@ const {handleError} = require('./utils'); const checkEnvironmentVariables = require('./shared-commands/check-environment-variables'); const downloadBuildArtifacts = require('./prepare-canary-commands/download-build-artifacts'); const parseParams = require('./prepare-canary-commands/parse-params'); -const printCanarySummary = require('./shared-commands/print-canary-summary'); +const printPrereleaseSummary = require('./shared-commands/print-prerelease-summary'); const run = async () => { try { @@ -15,7 +15,7 @@ const run = async () => { await checkEnvironmentVariables(params); await downloadBuildArtifacts(params); - await printCanarySummary(params); + await printPrereleaseSummary(params); } catch (error) { handleError(error); } diff --git a/scripts/release/prepare-stable-commands/check-out-packages.js b/scripts/release/prepare-stable-commands/check-out-packages.js new file mode 100644 index 0000000000000..a49a37377fbcd --- /dev/null +++ b/scripts/release/prepare-stable-commands/check-out-packages.js @@ -0,0 +1,29 @@ +#!/usr/bin/env node + +'use strict'; + +const chalk = require('chalk'); +const {exec} = require('child-process-promise'); +const {join} = require('path'); +const {logPromise} = require('../utils'); + +const run = async ({cwd, packages, version}) => { + // Cleanup from previous builds + await exec(`rm -rf ./build/node_modules*`, {cwd}); + await exec(`mkdir ./build/node_modules`, {cwd}); + + const nodeModulesPath = join(cwd, 'build/node_modules'); + + // Checkout canary release from NPM for all local packages + for (let i = 0; i < packages.length; i++) { + const packageName = packages[i]; + await exec(`npm i ${packageName}@${version}`, {cwd: nodeModulesPath}); + } +}; + +module.exports = async params => { + return logPromise( + run(params), + `Checking out canary from NPM ${chalk.yellow(params.version)}` + ); +}; diff --git a/scripts/release/prepare-stable-commands/confirm-stable-version-numbers.js b/scripts/release/prepare-stable-commands/confirm-stable-version-numbers.js new file mode 100644 index 0000000000000..a5b2e67c7684e --- /dev/null +++ b/scripts/release/prepare-stable-commands/confirm-stable-version-numbers.js @@ -0,0 +1,56 @@ +#!/usr/bin/env node + +'use strict'; + +const chalk = require('chalk'); +const prompt = require('prompt-promise'); +const semver = require('semver'); + +const run = async (params, versionsMap) => { + const groupedVersionsMap = new Map(); + + // Group packages with the same source versions. + // We want these to stay lock-synced anyway. + // This will require less redundant input from the user later, + // and reduce the likelihood of human error (entering the wrong version). + versionsMap.forEach((version, packageName) => { + if (!groupedVersionsMap.has(version)) { + groupedVersionsMap.set(version, [packageName]); + } else { + groupedVersionsMap.get(version).push(packageName); + } + }); + + // Prompt user to confirm or override each version group. + const entries = [...groupedVersionsMap.entries()]; + for (let i = 0; i < entries.length; i++) { + const [bestGuessVersion, packages] = entries[i]; + const packageNames = chalk.green(packages.join(', ')); + const defaultVersion = bestGuessVersion + ? chalk.yellow(` (default ${bestGuessVersion})`) + : ''; + const version = + (await prompt( + chalk`{green ✓} Version for ${packageNames}${defaultVersion}: ` + )) || bestGuessVersion; + prompt.done(); + + // Verify a valid version has been supplied. + try { + semver(version); + + packages.forEach(packageName => { + versionsMap.set(packageName, version); + }); + } catch (error) { + console.log(chalk`{red ✘ Version {white ${version}} is invalid.}`); + + // Prompt again + i--; + } + } +}; + +// Run this directly because it's fast, +// and logPromise would interfere with console prompting. +module.exports = run; diff --git a/scripts/release/prepare-stable-commands/guess-stable-version-numbers.js b/scripts/release/prepare-stable-commands/guess-stable-version-numbers.js new file mode 100644 index 0000000000000..11508489e4074 --- /dev/null +++ b/scripts/release/prepare-stable-commands/guess-stable-version-numbers.js @@ -0,0 +1,34 @@ +#!/usr/bin/env node + +'use strict'; + +const semver = require('semver'); +const {execRead, logPromise} = require('../utils'); + +const run = async ({cwd, packages}, versionsMap) => { + for (let i = 0; i < packages.length; i++) { + const packageName = packages[i]; + + try { + // In case local package JSONs are outdated, + // guess the next version based on the latest NPM release. + const version = await execRead(`npm show ${packageName} version`); + const {major, minor, patch} = semver(version); + + // Guess the next version by incrementing patch. + // The script will confirm this later. + versionsMap.set(packageName, `${major}.${minor}.${patch + 1}`); + } catch (error) { + // If the package has not yet been published, + // we'll require a version number to be entered later. + versionsMap.set(packageName, null); + } + } +}; + +module.exports = async (params, versionsMap) => { + return logPromise( + run(params, versionsMap), + 'Guessing stable version numbers' + ); +}; diff --git a/scripts/release/prepare-stable-commands/parse-params.js b/scripts/release/prepare-stable-commands/parse-params.js new file mode 100644 index 0000000000000..462b69bbae209 --- /dev/null +++ b/scripts/release/prepare-stable-commands/parse-params.js @@ -0,0 +1,52 @@ +#!/usr/bin/env node + +'use strict'; + +const chalk = require('chalk'); +const commandLineArgs = require('command-line-args'); +const commandLineUsage = require('command-line-usage'); +const figlet = require('figlet'); + +const paramDefinitions = [ + { + name: 'version', + type: String, + description: 'Version of published canary release (e.g. 0.0.0-ddaf2b07c)', + }, +]; + +module.exports = () => { + const params = commandLineArgs(paramDefinitions); + + if (!params.version) { + const usage = commandLineUsage([ + { + content: chalk + .hex('#61dafb') + .bold(figlet.textSync('react', {font: 'Graffiti'})), + raw: true, + }, + { + content: 'Prepare a published canary release to be promoted to stable.', + }, + { + header: 'Options', + optionList: paramDefinitions, + }, + { + header: 'Examples', + content: [ + { + desc: 'Example:', + example: + '$ ./prepare-stable.js [bold]{--version=}[underline]{0.0.0-ddaf2b07c}', + }, + ], + }, + ]); + console.log(usage); + process.exit(1); + } + + return params; +}; diff --git a/scripts/release/prepare-stable-commands/update-stable-version-numbers.js b/scripts/release/prepare-stable-commands/update-stable-version-numbers.js new file mode 100644 index 0000000000000..b088b504786ee --- /dev/null +++ b/scripts/release/prepare-stable-commands/update-stable-version-numbers.js @@ -0,0 +1,144 @@ +#!/usr/bin/env node + +'use strict'; + +const chalk = require('chalk'); +const {readFileSync, writeFileSync} = require('fs'); +const {readJson, writeJson} = require('fs-extra'); +const {join} = require('path'); +const printDiff = require('print-diff'); +const {confirm, execRead} = require('../utils'); + +const run = async ({cwd, packages, version}, versionsMap) => { + const nodeModulesPath = join(cwd, 'build/node_modules'); + + // Cache all package JSONs for easy lookup below. + const sourcePackageJSONs = new Map(); + const targetPackageJSONs = new Map(); + for (let i = 0; i < packages.length; i++) { + const packageName = packages[i]; + const sourcePackageJSON = await readJson( + join(cwd, 'packages', packageName, 'package.json') + ); + sourcePackageJSONs.set(packageName, sourcePackageJSON); + const targetPackageJSON = await readJson( + join(nodeModulesPath, packageName, 'package.json') + ); + targetPackageJSONs.set(packageName, targetPackageJSON); + } + + const updateDependencies = async (targetPackageJSON, key) => { + const targetDependencies = targetPackageJSON[key]; + if (targetDependencies) { + const sourceDependencies = sourcePackageJSONs.get(targetPackageJSON.name)[ + key + ]; + + for (let i = 0; i < packages.length; i++) { + const dependencyName = packages[i]; + const targetDependency = targetDependencies[dependencyName]; + + if (targetDependency) { + // For example, say we're updating react-dom's dependency on scheduler. + // We compare source packages to determine what the new scheduler dependency constraint should be. + // To do this, we look at both the local version of the scheduler (e.g. 0.11.0), + // and the dependency constraint in the local version of react-dom (e.g. scheduler@^0.11.0). + const sourceDependencyVersion = sourcePackageJSONs.get(dependencyName) + .version; + const sourceDependencyConstraint = sourceDependencies[dependencyName]; + + // If the source dependency's version and the constraint match, + // we will need to update the constraint to point at the dependency's new release version, + // (e.g. scheduler@^0.11.0 becomes scheduler@^0.12.0 when we release scheduler 0.12.0). + // Othewise we leave the constraint alone (e.g. react@^16.0.0 doesn't change between releases). + // Note that in both cases, we must update the target package JSON, + // since canary releases are all locked to the canary version (e.g. 0.0.0-ddaf2b07c). + if ( + sourceDependencyVersion === + sourceDependencyConstraint.replace(/^[\^\~]/, '') + ) { + targetDependencies[ + dependencyName + ] = sourceDependencyConstraint.replace( + sourceDependencyVersion, + versionsMap.get(dependencyName) + ); + } else { + targetDependencies[dependencyName] = sourceDependencyConstraint; + } + } + } + } + }; + + // Update all package JSON versions and their dependencies/peerDependencies. + // This must be done in a way that respects semver constraints (e.g. 16.7.0, ^16.7.0, ^16.0.0). + // To do this, we use the dependencies defined in the source package JSONs, + // because the canary dependencies have already been falttened to an exact match (e.g. 0.0.0-ddaf2b07c). + for (let i = 0; i < packages.length; i++) { + const packageName = packages[i]; + const packageJSONPath = join(nodeModulesPath, packageName, 'package.json'); + const packageJSON = await readJson(packageJSONPath); + packageJSON.version = versionsMap.get(packageName); + + await updateDependencies(packageJSON, 'dependencies'); + await updateDependencies(packageJSON, 'peerDependencies'); + + await writeJson(packageJSONPath, packageJSON, {spaces: 2}); + } + + // Print the map of versions and their dependencies for confirmation. + const printDependencies = (maybeDependency, label) => { + if (maybeDependency) { + for (let dependencyName in maybeDependency) { + if (packages.includes(dependencyName)) { + console.log( + chalk`• {green ${dependencyName}} @ {yellow ${ + maybeDependency[dependencyName] + }} (${label})` + ); + } + } + } + }; + for (let i = 0; i < packages.length; i++) { + const packageName = packages[i]; + const packageJSONPath = join(nodeModulesPath, packageName, 'package.json'); + const packageJSON = await readJson(packageJSONPath); + console.log( + chalk`\n{green ${packageName}} @ {yellow ${chalk.yellow( + versionsMap.get(packageName) + )}}` + ); + printDependencies(packageJSON.dependencies, 'dependency'); + printDependencies(packageJSON.peerDependencies, 'peer'); + } + await confirm('Do the versions above look correct?'); + + // Find-and-replace hard coded version (in built JS) for renderers. + for (let i = 0; i < packages.length; i++) { + const packageName = packages[i]; + const packagePath = join(nodeModulesPath, packageName); + + let files = await execRead( + `find ${packagePath} -name '*.js' -exec echo {} \\;`, + {cwd} + ); + files = files.split('\n'); + files.forEach(path => { + const beforeContents = readFileSync(path, 'utf8', {cwd}); + const afterContents = beforeContents.replace( + new RegExp(version, 'g'), + versionsMap.get(packageName) + ); + if (beforeContents !== afterContents) { + printDiff(beforeContents, afterContents); + writeFileSync(path, afterContents, {cwd}); + } + }); + } + await confirm('Do the replacements above look correct?'); +}; + +// Run this directly because logPromise would interfere with printing package dependencies. +module.exports = run; diff --git a/scripts/release/prepare-stable.js b/scripts/release/prepare-stable.js new file mode 100755 index 0000000000000..733cb109e9bf5 --- /dev/null +++ b/scripts/release/prepare-stable.js @@ -0,0 +1,36 @@ +#!/usr/bin/env node + +'use strict'; + +const {join} = require('path'); +const {getPublicPackages, handleError} = require('./utils'); + +const checkOutPackages = require('./prepare-stable-commands/check-out-packages'); +const confirmStableVersionNumbers = require('./prepare-stable-commands/confirm-stable-version-numbers'); +const guessStableVersionNumbers = require('./prepare-stable-commands/guess-stable-version-numbers'); +const parseParams = require('./prepare-stable-commands/parse-params'); +const printPrereleaseSummary = require('./shared-commands/print-prerelease-summary'); +const updateStableVersionNumbers = require('./prepare-stable-commands/update-stable-version-numbers'); + +const run = async () => { + try { + const params = parseParams(); + params.cwd = join(__dirname, '..', '..'); + params.packages = await getPublicPackages(); + + // Map of package name to upcoming stable version. + // This Map is initially populated with guesses based on local versions. + // The developer running the release later confirms or overrides each version. + const versionsMap = new Map(); + + await checkOutPackages(params); + await guessStableVersionNumbers(params, versionsMap); + await confirmStableVersionNumbers(params, versionsMap); + await updateStableVersionNumbers(params, versionsMap); + await printPrereleaseSummary(params); + } catch (error) { + handleError(error); + } +}; + +run(); diff --git a/scripts/release/shared-commands/print-canary-summary.js b/scripts/release/shared-commands/print-prerelease-summary.js similarity index 71% rename from scripts/release/shared-commands/print-canary-summary.js rename to scripts/release/shared-commands/print-prerelease-summary.js index 950f2e1a300da..b7a1691c5efb3 100644 --- a/scripts/release/shared-commands/print-canary-summary.js +++ b/scripts/release/shared-commands/print-prerelease-summary.js @@ -5,12 +5,11 @@ const chalk = require('chalk'); const {join, relative} = require('path'); -module.exports = ({cwd, build, path}) => { +module.exports = ({cwd}) => { const publishPath = relative( process.env.PWD, join(__dirname, '../publish.js') ); - const command = `${publishPath}` + (path ? ` -p ${path}` : ''); const packagingFixturesPath = join(cwd, 'fixtures/packaging'); const standaloneFixturePath = join( @@ -20,10 +19,11 @@ module.exports = ({cwd, build, path}) => { console.log( chalk` - {green.bold A potential canary has been prepared!} - Next there are a couple of manual steps: + {green.bold A potential release has been prepared!} - {bold.underline Smoke test the packages} + You can review the contents of this release in {yellow.bold ./build/node_modules/} + + {bold.underline Before publishing, please smoke test the packages} 1. Open {yellow.bold ${standaloneFixturePath}} in the browser. 2. It should say {italic "Hello world!"} @@ -33,8 +33,8 @@ module.exports = ({cwd, build, path}) => { 6. Open {blue.bold http://localhost:9000/fixtures/packaging} 7. Verify every iframe shows {italic "Hello world!"} - After completing the above steps, you can publish this canary by running: - {yellow.bold ${command}} + After completing the above steps, you can publish this release by running: + {yellow.bold ${publishPath}} `.replace(/\n +/g, '\n') ); }; diff --git a/scripts/release/utils.js b/scripts/release/utils.js index c909c8679b818..261df04764bb1 100644 --- a/scripts/release/utils.js +++ b/scripts/release/utils.js @@ -8,6 +8,16 @@ const {readdirSync, readFileSync, statSync, writeFileSync} = require('fs'); const {readJson, writeJson} = require('fs-extra'); const logUpdate = require('log-update'); const {join} = require('path'); +const prompt = require('prompt-promise'); + +const confirm = async message => { + const confirmation = await prompt(chalk`\n${message} {yellow (y/N)} `); + prompt.done(); + if (confirmation !== 'y' && confirmation !== 'Y') { + console.log(chalk.red('Release cancelled.')); + process.exit(0); + } +}; const execRead = async (command, options) => { const {stdout} = await exec(command, options); @@ -213,6 +223,7 @@ const updateVersionsForCanary = async (cwd, version) => { }; module.exports = { + confirm, execRead, execUnlessDry, getBuildInfo, diff --git a/scripts/release/yarn.lock b/scripts/release/yarn.lock index 240210db6c7e4..f3eecb747cc1e 100644 --- a/scripts/release/yarn.lock +++ b/scripts/release/yarn.lock @@ -204,6 +204,11 @@ delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" +diff@^1.2.1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-1.4.0.tgz#7f28d2eb9ee7b15a97efd89ce63dcfdaa3ccbabf" + integrity sha1-fyjS657nsVqX79ic5j3P2qPMur8= + ecc-jsbn@~0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" @@ -451,6 +456,13 @@ performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" +print-diff@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/print-diff/-/print-diff-0.1.1.tgz#e32e59d89f753208629ff74d9a7430df4f3cc899" + integrity sha512-dp36GezMEivgKH/zcLB4eBhJmQM3ewAa1UAqEPXMzU69NQ5wCP8puVBZlDFyt1WEtR5k2UC+3ahg+T5BfmRVGw== + dependencies: + diff "^1.2.1" + promise-polyfill@^6.0.1: version "6.0.2" resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-6.0.2.tgz#d9c86d3dc4dc2df9016e88946defd69b49b41162"