diff --git a/packages/create-subscription/package.json b/packages/create-subscription/package.json index b28cd18dd83..31f4c2b7d80 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 598758798fa..efac100e7ed 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 dc5cf76d0d7..d0604366855 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 02a60854633..32db7ca7637 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 b608fb31286..d31f140c42c 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 ea013d1aba2..405705dde59 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 0900718323f..d92c95f7d3b 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 5ff1d680295..70db5a95add 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 22f5d5d8f9e..51b668a366b 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 1d4bbe9416b..906a58ed400 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 2525b17a430..6012653435e 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 7c599040629..623b1d18081 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 380a3c8e92a..cbba2e88796 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 00000000000..122d859535b --- /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 c452eebc00c..5b46ec45d3b 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 00000000000..3661e80cdef --- /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 8687c90d401..00000000000 --- 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-commands/print-post-build-summary.js b/scripts/release/build-commands/print-post-build-summary.js index a9c660d0c58..688159c19b6 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 9288af14bf2..721770778a2 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 buildArtifacts = require('./create-release-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/ci-add-build-info-json.js b/scripts/release/ci-add-build-info-json.js new file mode 100755 index 00000000000..53444d2ecba --- /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 00000000000..b4441f3f39c --- /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 00000000000..816b2a146eb --- /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 00000000000..3435a625b58 --- /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 00000000000..0490efbe67d --- /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 00000000000..76f3cf85cf8 --- /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 00000000000..f0adc9ae772 --- /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 00000000000..0c9652d84fa --- /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 c5f9f99af32..21e2d3b2ddc 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-commands/download-build-artifacts.js b/scripts/release/prepare-canary-commands/download-build-artifacts.js new file mode 100644 index 00000000000..e047b597325 --- /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 00000000000..1e27fe95dd3 --- /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.js b/scripts/release/prepare-canary.js new file mode 100755 index 00000000000..2a459722d36 --- /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 printCanarySummary = require('./shared-commands/print-canary-summary'); + +const run = async () => { + try { + const params = parseParams(); + + await checkEnvironmentVariables(params); + await downloadBuildArtifacts(params); + await printCanarySummary(params); + } catch (error) { + handleError(error); + } +}; + +run(); diff --git a/scripts/release/publish.js b/scripts/release/publish.js index c3d980b0f24..4f242041488 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/shared-commands/print-canary-summary.js b/scripts/release/shared-commands/print-canary-summary.js new file mode 100644 index 00000000000..950f2e1a300 --- /dev/null +++ b/scripts/release/shared-commands/print-canary-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/utils.js b/scripts/release/utils.js index 2a9ecff5e89..c909c8679b8 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'); @@ -67,6 +91,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; @@ -113,12 +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 44ea4e5703a..240210db6c7 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"