From f10e0258cb0ca067b43ca60cc6dc0f98792a5537 Mon Sep 17 00:00:00 2001 From: "Michael Bradley, Jr" Date: Mon, 19 Nov 2018 13:11:57 -0600 Subject: [PATCH] build: introduce a `prepare` script in embark's package.json **TL;DR** These changes affect workflow with yarn. To prevent embark's `prepare` script from running undesirably: - If node_modules is in place and you're reinstalling after switching branches: ``` yarn run install_all ``` - If node_modules is missing (fresh clone or deleted): ``` EMBARK_NO_PREPARE=t yarn install && yarn run install_all ``` It's not recommended to set `EMBARK_NO_PREPARE` in your environment (e.g. in `.bashrc`) since that would interfere with embark's `release` script if/when you run it. ----------------- **1.** Specify embark's build-related steps in the `prepare` script of package.json. When embark is installed directly from GitHub the `prepare` script results in a "pre install" phase (handled automatically by npm/yarn) that fetches devDependencies, builds embark (including embark-ui), packs a tarball with the same steps (minus testing and tree-checking) as would happen during an embark release, and finally does a production install from that tarball. Important point: installs from GitHub must be performed with yarn; they're no longer possible with npm since during the "pre install" phase npm will honor embark's `.npmrc` and `"engines"` settings. The following will work correctly after this commit is merged: ``` yarn [global] add git+https://github.com/embark-framework/embark.git ``` Use of "hosted git" shortcuts (e.g. `embark-framework/embark#bracnh`) won't work correctly because yarn doesn't fully support them. See: https://github.com/yarnpkg/yarn/issues/5235. It's important to use `git+https` urls. Following a succesful install with `git+https` it is possible to use a "hosted git" shortcut or `https` url, but that's owing to a subtle and unreliable interaction between yarn's cache and yarn's logic for installing from a url/shortcut. **2.** Adjust the npm configs (`.npmrc`) for embark/-ui so that `yarn run [cmd] [--opt]` can be used in place of `npm run [cmd] -- [--opt]`. Either way is okay for running scripts, they're equivalent, but note the requirement to use `--` before specifying command options with `npm run`. **3.** Introduce yarn configs (`.yarnrc`) for embark/-ui and include the `check-files` directive. H/t to @alaibe for the recommendation. **4.** Ignore embark's `dist/typings` and `scripts` directories when packing a tarball. **5.** Refactor embark/-ui's npm-scripts in relation to the `prepare` script, and make other small improvements. Notably, if the environment variable `EMBARK_NO_PREPARE` is truthy (from JS perspective) then embark's `prepare` script will exit early. This prevents `install_all` and `prepare` from getting stuck in a loop (`install:core` uses cross-env to set `EMBARK_NO_PREPARE`) and provides a mechanism for users to skip the `prepare` script when doing a fresh install: ``` EMBARK_NO_PREPARE=t yarn install ``` **6.** Give `.js` extensions to node scripts in embark's `scripts/`, remove the shebang lines, and have npm-scripts explicitly invoke them with node. This arrangement works for all platforms: Linux, macOS, and Windows. **7.** Adjust travis and appveyor configs. Since at present there aren't any tests or other CI steps that make use of embark-ui's production build, set `EMBARK_NO_PREPARE` in the CI environments and invoke `build:node` directly. Check the working tree after `yarn install` for embark/-ui. This detects situations where changes should have been committed to `yarn.lock` but were not. Check the working tree again at the end to detect situations where ignore files should have been adjusted but were not. Both checks could also detect other surprising behavior that needs to be investigated. Any time the working tree is not clean (there are untracked files or changes) CI will fail. Drop CI runs for node 8.11.3 because that version ships with an older npm that results in unstaged changes to the test apps' `package-lock.json` files, causing the working tree check to fail at the end of the CI run. A simple workaround isn't apparent, but the matter can be revisited. **8.** Refactor embark's `release` script in light of the `prepare` script. Notably, do the push step only after `npm publish` completes successfully. This allows embark's `prepare` and `prepublishOnly` scripts to detect problems before a commit and tag are pushed to GitHub, avoiding a need to rebase/revert the remote release branch; the local branch will still need to have a commit dropped and tag deleted before rerunning the `release` script. Prompt the user if the `release` script is not being run in `--dry-run` mode. Provide additional visual indicators of `--dry-run` mode. Force the user to supply `--repo-branch [branch]` if the intention is to release from a branch other than `master`. --- .npmignore | 2 + .npmrc | 1 + .travis.yml | 6 +- .yarnrc | 1 + appveyor.yml | 6 +- embark-ui/.npmrc | 1 + embark-ui/.yarnrc | 1 + embark-ui/package.json | 2 + embark-ui/yarn.lock | 2 +- package.json | 25 +++-- scripts/check-no-prepare.js | 3 + scripts/check-working-tree.js | 12 +++ scripts/release | 90 ---------------- scripts/release.js | 189 ++++++++++++++++++++++++++++++++++ 14 files changed, 236 insertions(+), 105 deletions(-) create mode 100644 .yarnrc create mode 100644 embark-ui/.yarnrc create mode 100644 scripts/check-no-prepare.js create mode 100644 scripts/check-working-tree.js delete mode 100755 scripts/release create mode 100644 scripts/release.js diff --git a/.npmignore b/.npmignore index c6429e75c..1d3e4b403 100644 --- a/.npmignore +++ b/.npmignore @@ -9,10 +9,12 @@ CONTRIBUTING.md appveyor.yml babel.config.js dist/test +dist/typings header.png npm-debug.log* npm-shrinkwrap.json package-lock.json +scripts src test_apps tsconfig.json diff --git a/.npmrc b/.npmrc index ad50df662..e031d3432 100644 --- a/.npmrc +++ b/.npmrc @@ -1,3 +1,4 @@ engine-strict = true package-lock = false save-exact = true +scripts-prepend-node-path = true diff --git a/.travis.yml b/.travis.yml index 63f1fc3ad..bd496c30a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ os: - linux - osx node_js: - - "8.11.3" - "8" - "10" before_install: @@ -11,10 +10,13 @@ before_install: - export PATH="$HOME/.yarn/bin:$HOME/.config/yarn/global/node_modules/.bin:$PATH" cache: yarn: true +env: + - EMBARK_NO_PREPARE=true install: - yarn install - cd embark-ui && yarn install && cd .. - - git status && test -z "$(git status --porcelain)" + - npm run check-working-tree script: - npm run build:node - npm test + - npm run check-working-tree diff --git a/.yarnrc b/.yarnrc new file mode 100644 index 000000000..277017f65 --- /dev/null +++ b/.yarnrc @@ -0,0 +1 @@ +--install.check-files true diff --git a/appveyor.yml b/appveyor.yml index d6ed05391..9157e49a3 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,8 +1,8 @@ environment: matrix: - - nodejs_version: "8.11.3" - nodejs_version: "8" - nodejs_version: "10" + EMBARK_NO_PREPARE: true cache: - "%LOCALAPPDATA%\\Yarn" install: @@ -11,11 +11,13 @@ install: - cmd /c start /wait msiexec.exe /i yarn-1.12.3.msi /quiet /qn /norestart - rm yarn-1.12.3.msi - node --version + - npm --version - yarn --version - yarn install - cd embark-ui && yarn install && cd .. - - git status && node -e "process.exit(require('child_process').execSync('git status --porcelain').toString().trim()===''?0:1)" + - npm run check-working-tree test_script: - npm run build:node - npm test + - npm run check-working-tree build: off diff --git a/embark-ui/.npmrc b/embark-ui/.npmrc index ad50df662..e031d3432 100644 --- a/embark-ui/.npmrc +++ b/embark-ui/.npmrc @@ -1,3 +1,4 @@ engine-strict = true package-lock = false save-exact = true +scripts-prepend-node-path = true diff --git a/embark-ui/.yarnrc b/embark-ui/.yarnrc new file mode 100644 index 000000000..277017f65 --- /dev/null +++ b/embark-ui/.yarnrc @@ -0,0 +1 @@ +--install.check-files true diff --git a/embark-ui/package.json b/embark-ui/package.json index 78bd054ae..03b174331 100644 --- a/embark-ui/package.json +++ b/embark-ui/package.json @@ -72,6 +72,7 @@ "redux": "4.0.1", "redux-saga": "0.16.2", "resolve": "1.8.1", + "rimraf": "2.6.2", "sass-loader": "7.1.0", "simple-line-icons": "2.4.1", "style-loader": "0.23.0", @@ -85,6 +86,7 @@ }, "scripts": { "build": "node scripts/build.js", + "clean": "rimraf build", "lint": "eslint src/", "start": "node scripts/start.js", "test": "node scripts/test.js" diff --git a/embark-ui/yarn.lock b/embark-ui/yarn.lock index e2c981032..1c779f019 100644 --- a/embark-ui/yarn.lock +++ b/embark-ui/yarn.lock @@ -9364,7 +9364,7 @@ rgba-regex@^1.0.0: resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM= -rimraf@2, rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@~2.6.2: +rimraf@2, rimraf@2.6.2, rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@~2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" integrity sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w== diff --git a/package.json b/package.json index e398e7464..96bf8f4be 100644 --- a/package.json +++ b/package.json @@ -32,28 +32,33 @@ "build": "npm-run-all build:*", "build:node": "npm run babel:node", "build:ui": "cd embark-ui && npm run build", - "clean": "rimraf dist embark-*.tgz package embark-ui/build", + "check-no-prepare": "node scripts/check-no-prepare.js", + "check-working-tree": "node scripts/check-working-tree.js", + "clean": "npm-run-all clean:*", + "clean:core": "rimraf dist embark-*.tgz package", + "clean:ui": "cd embark-ui && npm run clean", "eslint": "eslint", - "install:core": "yarn install", + "install:core": "cross-env EMBARK_NO_PREPARE=t yarn install", "install:ui": "cd embark-ui && yarn install", "install_all": "npm-run-all install:*", "lint": "npm-run-all lint:*", "lint:js": "npm-run-all lint:js:*", - "lint:js:core": "eslint babel.config.js bin/embark src/bin/ src/lib/", + "lint:js:core": "eslint babel.config.js bin/embark scripts/ src/bin/ src/lib/", "lint:js:ui": "cd embark-ui && npm run lint", "lint:ts": "tslint -c tslint.json 'src/**/*.ts'", - "prepublishOnly": "npm-run-all clean build test", - "test": "npm-run-all lint test:*", - "test:core": "mocha dist/test/ --exit --no-timeouts --require source-map-support/register", - "test:test_app": "cross-env DAPP=\"test_app\" npm run test_dapp", - "test:contracts_app": "cross-env DAPP=\"contracts_app\" npm run test_dapp", - "test_dapp": "cross-env-shell \"cd test_apps/$DAPP && npm install && npm test\"", - "release": "./scripts/release", + "prepare": "npm run --silent check-no-prepare && npm-run-all install_all clean build || exit 0", + "prepublishOnly": "npm-run-all test check-working-tree", + "release": "node scripts/release.js", "start": "run-p start:*", "start:embark": "run-p start:embark:*", "start:embark:babel": "npm run babel:watch", "start:embark:type-check": "npm run type-check:watch", "start:ui": "cd embark-ui && npm run start", + "test": "npm-run-all lint test:*", + "test:core": "mocha dist/test/ --exit --no-timeouts --require source-map-support/register", + "test:test_app": "cross-env DAPP=test_app npm run test_dapp", + "test:contracts_app": "cross-env DAPP=contracts_app npm run test_dapp", + "test_dapp": "cross-env-shell \"cd test_apps/$DAPP && npm install && npm test\"", "tsc": "tsc", "tslint": "tslint", "type-check": "tsc", diff --git a/scripts/check-no-prepare.js b/scripts/check-no-prepare.js new file mode 100644 index 000000000..3774f9f7b --- /dev/null +++ b/scripts/check-no-prepare.js @@ -0,0 +1,3 @@ +/* global process */ + +process.exit(process.env.EMBARK_NO_PREPARE ? 1 : 0); diff --git a/scripts/check-working-tree.js b/scripts/check-working-tree.js new file mode 100644 index 000000000..a23fe4453 --- /dev/null +++ b/scripts/check-working-tree.js @@ -0,0 +1,12 @@ +/* global process require */ + +const {execSync} = require('child_process'); + +try { + execSync('git status', {stdio: 'inherit'}); + process.exit( + execSync('git status --porcelain').toString().trim() === '' ? 0 : 1 + ); +} catch (e) { + process.exit(1); +} diff --git a/scripts/release b/scripts/release deleted file mode 100755 index b46109c4d..000000000 --- a/scripts/release +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env node - -const execSync = require('child_process').execSync; -const minimist = require('minimist'); -const standardVersion = require('standard-version'); -const chalk = require('chalk'); -const args = minimist(process.argv.slice(2)); - -const DEFAULT_UPSTREAM_REPO_ORIGIN = 'origin'; -const DEFAULT_UPSTREAM_REPO_BRANCH = 'master'; -const origin = args['repo-origin'] || DEFAULT_UPSTREAM_REPO_ORIGIN; -const branch = args['repo-branch'] || DEFAULT_UPSTREAM_REPO_BRANCH; -const distTag = args['npm-dist-tag']; - -console.log(chalk.blue('ℹ'), `Fetching from origin '${origin}' to read upstream version...`); -try { - console.log(execSync(`git fetch ${origin}`).toString()); -} catch (e) { - console.log(chalk.red('✘'), 'Couldn\'t fetch latest commits. Please check the error above.'); - return; -} - -const localRef = execSync(`git rev-parse ${branch}`).toString(); -const originRef = execSync(`git rev-parse ${origin}/${branch}`).toString(); - -if (localRef !== originRef) { - console.log(chalk.red('✘'), `Local branch '${branch}' is not up to date with '${origin}/${branch}'. Please update your local branch first.`); - return; -} - -console.log(chalk.green('✔'), 'Release branch is up to date with remote branch.'); - -console.log(chalk.blue('ℹ'), 'Reinstalling node_modules just to make sure that everything is up to date...'); - -try { - console.log(execSync('npm run install_all').toString()); - console.log(chalk.green('✔'), 'Depencencies installed.'); -} catch (e) { - console.log(chalk.red('✘'), 'A problem occured. Please check the error above'); - return; -} - -standardVersion({ - dryRun: args['dry-run'], - prerelease: args.prerelease, - sign: args.sign, - releaseAs: args['release-as'] -}).then(() => { - if (!args['dry-run']) { - console.log(chalk.blue('ℹ'), `Pushing release commit to origin '${origin}' on branch '${branch}'...`); - try { - const output = execSync(`git push ${origin} ${branch} --follow-tags`).toString(); - console.log(chalk.green(output)); - console.log(chalk.green('✔'), 'Successfully pushed release commit'); - } catch (e) { - console.log(chalk.red('✘'), 'Couldn\'t push release commit. Please check the error above.'); - return { error: true }; - } - } else { - console.log(chalk.blue('ℹ'), 'This is a dry run. Nothing\'s being pushed.'); - } -}).then(error => { - if (error) { - return error; - } - console.log(chalk.blue('ℹ'), 'Publishing new Embark version on npm...'); - - let npmPublishCommand = distTag ? `npm publish --tag ${distTag}` : 'npm publish'; - - npmPublishCommand += args['dry-run'] ? ' --dry-run' : ''; - - try { - const output = execSync(npmPublishCommand).toString(); - console.log(chalk.green(output)); - if (args['dry-run']) { - console.log(chalk.blue('ℹ'), 'This was a dry run: Successfuly published latest version.'); - } else { - console.log(chalk.green('✔'), 'Successfully published latest version.'); - } - } catch (e) { - console.log(chalk.red('✘'), 'Couldn\'t publish version on npm. Please check the error above.'); - return { error: true }; - } -}).then(error => { - if (error) { - console.log(chalk.red('✘'), 'Stopping right here. Make sure to clean up commits and tags if needed.'); - } else { - console.log(chalk.green('✔'), 'Woohoo! Done.'); - } -}); diff --git a/scripts/release.js b/scripts/release.js new file mode 100644 index 000000000..b6d32cd63 --- /dev/null +++ b/scripts/release.js @@ -0,0 +1,189 @@ +/* global process require */ + +const chalk = require('chalk'); +const {execSync} = require('child_process'); +const minimist = require('minimist'); +const {prompt} = require('promptly'); +const standardVersion = require('standard-version'); + +const args = minimist(process.argv.slice(2)); + +const DEFAULT_UPSTREAM_REPO_BRANCH = 'master'; +const DEFAULT_UPSTREAM_REPO_ORIGIN = 'origin'; +const branch = args['repo-branch'] || DEFAULT_UPSTREAM_REPO_BRANCH; +const origin = args['repo-origin'] || DEFAULT_UPSTREAM_REPO_ORIGIN; + +const distTag = args['npm-dist-tag']; +const dryRun = args['dry-run']; +const prerelease = args.prerelease; +const releaseAs = args['release-as']; +const sign = args.sign; + +// eslint-disable-next-line no-confusing-arrow +const dryRunMaybe = () => dryRun ? leftPad1(chalk.yellow('(DRY RUN)')) : ''; +const execSyncInherit = (cmd) => execSync(cmd, {stdio: 'inherit'}); +// eslint-disable-next-line no-confusing-arrow +const leftPad1 = (str) => ' ' + str; +// eslint-disable-next-line no-confusing-arrow +const leftPad1Maybe = (str) => str ? leftPad1(str) : str; +const log = (mark, str) => console.log(mark, str.filter(s => !!s).join(' ')); +const logError = (...str) => log(chalk.red('✘'), str); +const logInfo = (...str) => log(chalk.blue('ℹ'), str); +const logSuccess = (...str) => log(chalk.green('✔'), str); +const logWarning = (...str) => log(chalk.yellow('‼︎'), str); + +const yesNoValidator = (input) => { + const _input = input && input[0].toLowerCase(); + if (!['y', 'n'].includes(_input)) { + throw new Error(chalk.red('✘') + leftPad1(`Please answer [y]es or [n]o.`)); + } + return _input; +}; + +const promptOpts = {default: 'blank', validator: yesNoValidator}; + +const proceedAnywayPrompt = async () => { + let answer = await prompt( + `${chalk.yellow('⁉︎')} Proceed anyway? [y/n]`, + promptOpts + ); + if (answer === 'n') { + logWarning(`Stopping right here.`); + process.exit(0); + } +}; + +const dryRunPrompt = async () => { + let answer = await prompt( + `${chalk.blue('⁇')} This is ${chalk.yellow('NOT')} a --dry-run.` + + leftPad1(`Did you complete a successful --dry-run first? [y/n]`), + promptOpts + ); + if (answer === 'n') await proceedAnywayPrompt(); +}; + +(async () => { + try { + if (!dryRun) await dryRunPrompt(); + + logInfo(`Determining the current branch...`); + + let currentBranch; + try { + currentBranch = execSync(`git rev-parse --abbrev-ref HEAD`) + .toString() + .trim(); + } catch (e) { + logError(`Couldn't determine the branch. Please check the error above.`); + throw new Error(); + } + + if (currentBranch !== branch) { + logError( + `Current branch '${currentBranch}' is not the same as release branch`, + `'${branch}'. Please checkout the release branch before rerunning this`, + `script or rerun with '--repo-branch ${currentBranch}'.` + ); + throw new Error(); + } + + logSuccess(`Current branch and release branch are the same.`); + logInfo(`Checking the working tree...`); + + try { + execSyncInherit(`npm run --silent check-working-tree`); + logSuccess(`Working tree is clean.`); + } catch (e) { + logError( + `Working tree is dirty or has untracked files. Please make necessary`, + `changes or commits before rerunning this script.` + ); + throw new Error(); + } + + logInfo( + `Fetching from origin '${origin}' to compare local and remote branches...` + ); + + try { + execSyncInherit(`git fetch ${origin}`); + } catch (e) { + logError(`Couldn't fetch latest commits. Please check the error above.`); + throw new Error(); + } + + let localRef, originRef; + try { + localRef = execSync(`git rev-parse ${branch}`).toString(); + originRef = execSync(`git rev-parse ${origin}/${branch}`).toString(); + } catch (e) { + logError(`A problem occured. Please check the error above.`); + throw new Error(); + } + + if (localRef !== originRef) { + logError( + `Local branch '${branch}' is not in sync with '${origin}/${branch}'.`, + `Please sync branches before rerunning this script.` + ); + throw new Error(); + } + + logSuccess(`Local branch is in sync with remote branch.`); + logInfo(`Running Standard Version${dryRunMaybe()}...`); + + await standardVersion({ + dryRun, + prerelease, + releaseAs, + sign + }); + + logInfo(`Publishing new Embark version on npm${dryRunMaybe()}...`); + + const npmPublishCommand = [ + `npm publish`, + leftPad1Maybe(`${distTag ? `--tag ${distTag}` : ''}`), + leftPad1Maybe(`${dryRun ? '--dry-run' : ''}`) + ].join(''); + + try { + execSyncInherit(npmPublishCommand); + logSuccess(`Successfully published latest version${dryRunMaybe()}.`); + } catch (e) { + logError( + `Couldn't publish version on npm${dryRunMaybe()}.`, + `Please check the error above.` + ); + throw new Error(); + } + + logInfo( + `Pushing release commit and tag to origin '${origin}' on branch`, + `'${branch}'${dryRunMaybe()}...` + ); + + const gitPushCommand = [ + `git push --follow-tags ${origin} ${branch}`, + leftPad1Maybe(`${dryRun ? '--dry-run' : ''}`) + ].join(''); + + try { + execSyncInherit(gitPushCommand); + logSuccess(`Successfully pushed${dryRunMaybe()}.`); + } catch (e) { + logError( + `Couldn't push${dryRunMaybe()}. Please check the error above.` + ); + throw new Error(); + } + + logSuccess(`Woohoo! Done${dryRunMaybe()}.`); + } catch (e) { + logError( + `Stopping right here${dryRunMaybe()}.`, + dryRun ? '' : `Make sure to clean up commits and tags as necessary.` + ); + process.exit(1); + } +})();