Release script (#11223)

* First chunk of new release script

* Re-ordered build steps to combine error codes and releases

* Reorganized build files; added stub publish script

* First pass at publis script. Also collect and print dry-run commits/publish commands.

* Deleted old react-release-manager scripts

* Cleaned up release package.json

* Basic README instructions

* Removed unnecessary 'async' keyword from a method

* Wordsmithing

* Tweaked README

* Renamed build -> build-commands and publish -> publish-commands to avoid conflict with .gitignore

* Bump pre-release package versions differently

* Prettier

* Improved CircleCI API token setup instructions message

* Lint fix

* Typofix
This commit is contained in:
Brian Vaughn 2017-10-16 15:01:14 -07:00 committed by GitHub
parent b5a2a1349d
commit c371c152ab
43 changed files with 1543 additions and 1687 deletions

View File

@ -1,7 +0,0 @@
'use strict';
module.exports = {
rules: {
'no-shadow': 0,
},
};

View File

@ -1,3 +0,0 @@
.config.json
node_modules
data

View File

@ -1,480 +0,0 @@
# react-release-manager
This is a tool that is being used to manage React releases.
## Prerequisites
You should have an existing clone of the React repo. We will call this a **“working copy”**. Ideally this is where you are most comfortable working on React.
Your working copy of React **should be up to date**. Check out the `master` branch in it and run `git pull` just to be sure.
## Cloning the Release Manager
**If this is your first time using the Release Manager**, you need to set it up.
Skip this section if youve done this before.
The Release Manager is also located inside the React repository so you need to **clone it to a separate folder**. Call it something other than `react` so that you dont confuse it with the working copy.
Check it out, install the dependencies, and run the CLI:
```
cd ~/projects # or wherever
git clone https://github.com/facebook/react.git react-release-manager
cd react-release-manager/scripts/release-manager
yarn
./cli.js
```
You will see a command-line interface that lets you enter commands.
It will need to learn a few things to work on your machine.
Type `init` and press Enter. It will ask you a few prompts:
1. `GitHub token? (needs "repo" privs)`
Follow [these instructions](https://help.github.com/articles/creating-an-access-token-for-command-line-use/) to generate a GitHub token. Make sure to put a checkmark for `repo` privileges. Dont share it with anyone!
2. `Location of local React checkout?`
Enter the local path to your React working copy. For example, it is `~/projects/react` on my machine.
Now you should be all set for releasing React on this machine!
## Before You Do Anything Else
You should have two separate React checkouts by now:
* **The Release Manager copy.** The previous section described how to set it up. You will only use this checkout for *running* the Release Manager. Run `git checkout master` and `git pull` to ensure it is up-to-date.
* **Your working copy of React.** The Release Manager will operate on it, and you will fix any merge conflicts inside of it. This should be the folder path you specified when you ran `init` in the previous section. Run `git checkout master` and `git pull` to ensure it is up-to-date.
Both clones clean and up-to-date?
If you arent already running it, run the Release Manager CLI:
```
cd react-release-manager/scripts/release-manager
./cli.js
```
Keep your working copy and the running Release Manager in separate terminal tabs.
## Updating the Documentation
When we merge a pull request to the documentation and it is relevant to the current version, we tag it with a `Documentation: needs merge to stable` label. The Release Manager can cherry-pick those commits so that they appear on the website.
The documentation is built from the current stable branch. For example, for React 15.x the branch is called `15-stable`. Switch your working copy to it:
```
cd react
git checkout 15-stable
git pull
```
Then, in the Release Manager, run the command:
```
docs-prs
```
The Release Manager should find the PRs that havent been merged yet. Reply with `y` to get them merged and then with `y` to push your changes.
**Tip:** If you see an error like `The previous cherry-pick is now empty, possibly due to conflict resolution` it might mean that theres a stray PR with a label that has already been merged to the stable branch. In this case you need to remove the label manually and retry the command.
## Cutting a Release
### Verifying Permissions
In the Release Manager, verify you have npm publish permissions:
```
npm-check-access
```
You will need to get all permissions before you can proceed.
### Cherry Picking PRs
If the permissions are cool, run:
```
start-release
```
**Tip:** if you get an error saying `'upstream' does not appear to be a git repository`, run `git remote add upstream https://github.com/facebook/react.git` in your working copy of React and try again.
If everything went well, you should see a green `OK!` in the output.
Create a new milestone in the [GitHub web interface](https://github.com/facebook/react/milestones) for the new release. Name it exactly after the version you intend to cut (e.g. `15.4.1`). Then run:
```
stable-prs
```
First, choose the current major “stable” milestone (such as `15-next`). Note that the Release Manager only sees merged PRs that have this milestone.
**Tip:** our 15.x branch has diverged significantly so we are using `15-hipri` for things we really need to get out, and `15-lopri` for everything else. This is a temporary situation that should get better after Fiber is out.
Next, choose the milestone you just created. This one should be specific and correspond to the version you intend to publish (such as `15.4.1`). The Release Manager will re-tag all PRs matching the previous “stable” milestone with this specific milestone after cherry-picking them.
Finally, pick all appropriate labels with a spacebar. For example, a patch release usually contains `exempt` and `patch` PRs, and a minor release contains `minor` PRs in addition to them.
Now the Release Manager will find all relevant PRs and attempt to cherry-pick them. Before agreeing to this, copy the list of PRs it prints out so that you can look them up later when you write the changelog.
It is likely that some PRs wont get merged cleanly. Youll need to manually resolve the conflicts in the working copy. If the resolutions are not obvious it might be a sign the branches diverged too much which might be bad. (Talk to the team.)
### Verifying the Changes
Your working copy should now be in a clean state on a development branch. For example, during 15.x the development branch is `15-dev`.
Verify it by running:
```
git status
>On branch 15-dev
>Your branch is ahead of 'origin/15-dev' by 10 commits.
> (use "git push" to publish your local commits)
>nothing to commit, working directory clean
```
Next, run `npm test`.
If there are any issues you might have introduced mistakes resolving conflicts.
You can fix them in a separate commit.
**Tip:** tests might also be failing if dependency versions are incorrect. You might want to run `yarn` first since sometimes `package.json` on master is different from the stable branches.
### Update the Error Codes
**This step is only necessary for a stable release.**
If youre just cutting an alpha, you should skip it.
Run this so that `scripts/error-codes/codes.json` is up to date:
```
npm run build -- --extract-errors
```
Check `git diff`. Do changes, if any, look sensible?
If there are any changes, commit them:
```
git commit -am 'Update error codes'
```
You will see the commit hash. Copy it in your editor. You will need it later to cherry-pick the error codes update to master.
If there were no changes, its also fine.
### Push and Choose the Branch
If you followed the guide correctly (and ran `start-release` in the beginning), you should be on a “stable development” branch such as `15-dev`. Now is a good time to push the development branch:
```
git push
```
Then comes the important part.
**If you plan to cut a stable release, switch the branch to the stable branch now.**
For example, if you plan to cut `15.4.1` (rather than a `15.4.1-0` alpha release), run:
```
git checkout 15-stable
git merge --no-ff 15-dev
```
This will merge the commits you cherry-picked into the stable branch.
However, if you plan to cut an alpha or a beta, you should stay on the “stable development” branch.
### Update the Lockfile
Run this so that the build is reproducible:
```
rm yarn.lock
rm -rf node_modules
yarn cache clean
yarn
```
Check `git diff`. Do changes look sensible?
Commit your changes:
```
git commit -am 'Update Yarn lockfile'
```
If youre feeling extra careful, you can run `npm test` again.
### Write the Changelog
**This step is only necessary for a stable release.**
If youre just cutting an alpha, you should skip it.
Open `CHANGELOG.md` in the working copy and add release notes in the same format as earlier. Its usually a good idea to summarize changes in a friendly way instead of using PR titles. While you may skip non-essential changes, its still good to give credit to contributors, so maybe group them together. You can verify that you havent messed up the markup by previewing them in an online Markdown editor.
Commit your changes, for example:
```
git commit -am 'Add <put the version here> changelog'
```
You will see the commit hash. Copy it in your editor. You will need it later to cherry-pick the changelog update to master.
### Bump the Version
In the Release Manager, run:
```
version
```
It will ask you about the version you want to ship and it will commit the result with a tag.
Were not pushing anything yet, it will just create a local commit.
### Ensure You Have the Bower Repo
**This step is only necessary for a stable release.**
If youre just cutting an alpha, you should skip it.
Theres another repository you need to clone!
This time, it should be a sibling of your React working copy.
In the working copy directory, you can run:
```
git clone https://github.com/reactjs/react-bower.git ../react-bower
```
### Build It!
Run in the working copy:
```
npm run build
```
This will create the build products in the working copy. You wont see changes in git because the `build` folder is ignored.
### Verify the Build Works
At the very least, open `fixtures/packaging/babel-standalone/dev.html` in the browser. You should see a “Hello, World!” there, and the console should have no errors.
If you changed anything related to how packages are created, I recommend following the instructions in `fixtures/packaging/README.md` to verify all fixtures still work. You can skip the “build React” step in it but still need to build the fixtures. In short, run `node build-all.js` in `fixtures` folder and follow the instructions it prints.
They are manual tests, so the CI wouldnt have caught errors in them.
### Update Bower and Docs
**This step is only necessary for a stable release.**
If youre just cutting an alpha, you should skip it.
**TODO: We used to have a `grunt release` command that does a few extra things but it was deleted when we moved build process to Rollup. We need to decide if we care about Bower or not, and if we do, either add a similar script back, or write up commands to do it by hand.**
[Here's what the script used to do:](https://github.com/facebook/react/blob/50b3cab3ec7565085da21106791dbaa6fe22d862/grunt/tasks/release.js)
* Copy UMD bundles to `../react-bower`.
* Commit and tag them in `react-bower` (but not push yet).
* Copy them to `docs/js/` (note: we changed their filenames so might need to change docs too).
In addition to those changes, bump the version inside `docs/_config.yml`:
```diff
- react_version: 15.4.0
+ react_version: <put the new version here>
```
Now commit the changes:
```
git commit -am 'Update React version in docs'
```
### Push the Working Copy
Now we are ready to push the branch in the working copy:
```
git push
git push --tags
```
### Release on Bower
**This step is only necessary for a stable release.**
If youre just cutting an alpha, you should skip it.
Go to the Bower folder from your working copy and push the new commit and tag:
```
cd ../react-bower
git push
git push --tags
cd ../react
```
### Release on npm
In the Release Manager, run:
```
npm-publish
```
### Cherry-Pick the Changelog
**This step is only necessary for a stable release.**
If youre just cutting an alpha, you should skip it.
Remember how you saved the hash of the commit changelog a few hours before?
Now its time to switch our working copy to `master` and cherry-pick it:
```
git checkout master
git pull
git cherry-pick <hash of the changelog commit>
```
Verify you picked the right commit:
```
git diff HEAD~
```
Looks good? Push it.
```
git push
```
### Cherry-Pick the Error Codes
**This step is only necessary for a stable release.**
If youre just cutting an alpha, you should skip it.
If error codes were updated, you were supposed to commit that earlier and record the commit hash.
Did this happen?
If so, cherry-pick it to `master` as well:
```
git cherry-pick <hash of the error codes update commit>
```
Verify you picked the right commit:
```
git diff HEAD~
```
Looks good? Push it.
```
git push
```
### Creating a GitHub Release
**This step is only necessary for a stable release.**
If youre just cutting an alpha, you should skip it.
Copy your new release notes from `CHANGELOG.md` and [create a new Release](https://github.com/facebook/react/releases/new) on GitHub. Choose the tag version you just pushed in the dropdown so that it says “Existing tag”. Paste the release notes.
Finally, attach these files to the release:
* `build/dist/react.development.js`
* `build/dist/react.production.min.js`
* `build/dist/react-dom.development.js`
* `build/dist/react-dom.production.min.js`
### Force-Updating the Website
**This step is only necessary for a stable release.**
If youre just cutting an alpha, you should skip it.
Normally the docs should update themselves after CI runs.
However sometimes our CI might be slow or something might break.
You can rebuild the docs manually if you want to.
Make sure you have a React copy in a sibling folder called `react-gh-pages`:
```
git clone https://github.com/facebook/react.git ../react-gh-pages
```
Then make sure its on `gh-pages` branch and that its up-to-date:
```
cd ../react-gh-pages
git checkout gh-pages
git pull
```
Switch back to the working copy and go to the `docs` folder:
```
cd ../react/docs
```
Switch to the stable branch (the one you just spent a lot of time with).
For example:
```
git checkout 15-stable
```
Build the docs now:
```
bundle install # Might need sudo.
bundle exec rake release
```
If this fails, maybe youre missing some Ruby dependencies:
```
gem install bundler
```
Install them and try again.
This should not produce any changes in the working copy, but `react-gh-pages` should get some file changes:
```
cd ../../react-gh-pages
git diff
```
If they look alright, commit and push them:
```
git commit -am 'Rebuild the website'
git push
```
Now open https://reactjs.org/, give it a few minutes, refresh, and behold.
Dont forget to switch to `master` for the future development.
```
git checkout master
```
### Bonus: Trying It Out
Run:
```
npm i -g create-react-app
create-react-app ../my-new-app
cd ../my-new-app
npm start
```
This should use the latest version of React.

View File

@ -1,138 +0,0 @@
#!/usr/bin/env node
'use strict';
const chalk = require('chalk');
const Vorpal = require('vorpal');
const GitHubAPI = require('github-api');
const untildify = require('untildify');
const fs = require('fs');
const path = require('path');
const os = require('os');
const child_process = require('child_process');
const execSync = child_process.execSync;
const vorpal = new Vorpal();
// Expects to be in a checkout of react that is a sibling of the react checkout you want to operate on
// eg ~/code/react@release-manager/scripts/release-manager & ~/code/react
// TODO: Make this an argument to the script
let PATH_TO_REPO = null;
const PATH_TO_CONFIG = path.resolve(
os.homedir(),
'.react-release-manager.json'
);
const DEFAULT_CONFIG = {
githubToken: null,
reactPath: path.resolve('../../../react'),
};
// Quick dry run opt-in. This allows quick debugging of execInRepo without
// actually running the command, ensuring no accidental publishing.
const DRY_RUN = false;
// Enabled commands
const COMMANDS = [
'init',
'docs-prs',
'q',
'stable-prs',
'version',
'npm-publish',
'npm-check-access',
'npm-grant-access',
'start-release',
];
// HELPERS
// Simple helper to write out some JSON for debugging
function writeTo(file, data) {
var folder = path.join(__dirname, 'data');
if (!fs.existsSync(folder)) {
fs.mkdirSync(folder);
}
fs.writeFile(path.join(folder, file), JSON.stringify(data, null, 2));
}
// Wrapper around exec so we don't have to worry about paths
function execInRepo(command) {
vorpal.log(chalk.gray(`Executing ${chalk.underline(command)}`));
if (DRY_RUN) {
return '';
}
return execSync(command, {
cwd: PATH_TO_REPO,
encoding: 'utf8',
}).trim();
}
function getReactVersion() {
return JSON.parse(
fs.readFileSync(path.join(PATH_TO_REPO, 'package.json'), 'utf8')
).version;
}
const app = {
vorpal,
updateConfig() {
// TODO: write this. This should make it possible to start without a config
// and go through the init process to create one and then re-init the github
// setup.
this.config = this.loadConfig();
},
loadConfig() {
try {
// TODO: validate config
let config = JSON.parse(fs.readFileSync(PATH_TO_CONFIG, 'utf8'));
config.reactPath = path.normalize(untildify(config.reactPath));
PATH_TO_REPO = config.reactPath;
return config;
} catch (e) {
console.error(
'Attempt to load config file failed. Please run `init` command for initial setup or make sure ' +
'~/.react-release-manager.json is valid JSON. Using a default config which may not work ' +
'properly.'
);
return DEFAULT_CONFIG;
}
},
init() {
this.config = this.loadConfig();
this.PATH_TO_CONFIG = PATH_TO_CONFIG;
// GITHUB
this.github = new GitHubAPI({
token: this.config.githubToken,
});
this.ghrepo = this.github.getRepo('facebook', 'react');
this.ghissues = this.github.getIssues('facebook', 'react');
// HELPERS
this.writeTo = writeTo;
this.execInRepo = execInRepo;
this.getReactVersion = getReactVersion;
// Register commands
COMMANDS.forEach(command => {
vorpal.use(require(`./commands/${command}`)(vorpal, app));
});
var v = vorpal.history('react-release-manager').delimiter('rrm \u2234');
v.exec('help');
v.show();
},
};
app.init();

View File

@ -1,150 +0,0 @@
'use strict';
const chalk = require('chalk');
const git = require('./utils/git');
const DOCS_LABEL = 'Documentation: needs merge to stable';
// FOR DOCS
// get all issues with label
// ensure all have pull_request
// FAIL: log issues that aren't prs
// fetch each pr, (issues.pull_request.url)
// sort each by merged_at
// git cherry-pick -x sha || git cherry-pick -x -m1 sha
// (or use API to look up number of parents, 2 = use -m1)
// track progress. on fail, pause and force user to handle manually, continue? prompt
// git push
// update labels on each PR
// ALT: dump link to
// https://github.com/facebook/react/issues?q=label%3A%22Documentation%3A+needs+merge+to+stable%22+is%3Aclosed
// and say manual step to remove label
module.exports = function(vorpal, app) {
vorpal
.command('docs-prs')
.description(
'Get list of documentation pull requests that need to be merged to the stable branch'
)
.action(function(args, actionCB) {
const branch = git.getBranch(app);
if (!branch.match(/-stable$/)) {
this.log(chalk.red('Aborting...'));
this.log(
`You need to be on the latest stable branch in the React repo ` +
`to execute this command.\nYou are currently in ${branch}.`
);
actionCB();
return;
}
const query = {
labels: [DOCS_LABEL].join(), // github-api doesn't join automatically
state: 'closed',
};
app.ghissues.listIssues(query, (err, body) => {
app.writeTo('issues.json', body);
// console.log(body);
// fs.writeFileSync('body.json', JSON.stringify(body, null, 2));
// fs.writeFileSync('headers.json', JSON.stringify(headers, null, 2));
// const prs = require('./body');
// This API *could* return issues that aren't pull requests, so filter out
// issues that don't have pull_request set.
const pulls = body.filter(issue => issue.pull_request);
// We don't enough data about the pull request (merge sha or merge time) so we
// need to fetch more. We'll use promises so we don't have to count completions.
const pullPromises = pulls.map(pr => {
return new Promise((resolve, reject) => {
app.ghrepo.getPullRequest(pr.number, (err, body) => {
if (err) {
reject(err);
}
app.writeTo(`pr-${pr.number}.json`, body);
// We want to track the original issue as well since it has the
// label information.
const richPull = body;
richPull.__originalIssue = pr;
resolve(richPull);
});
});
});
Promise.all(pullPromises).then(richPulls => {
richPulls.forEach(pr => {
// Convert merged_at to real Date for sorting
pr.merged_at_date = new Date(pr.merged_at);
});
richPulls = richPulls.sort(
(a, b) => a.merged_at_date - b.merged_at_date
);
this.log(`Found ${chalk.bold(richPulls.length)} pull requests:`);
richPulls.forEach(pr => {
this.log(`${pr.html_url}: ${chalk.bold(pr.title)}`);
});
this.prompt(
{
name: 'merge',
type: 'confirm',
message: `Merge these ${richPulls.length} pull requests?`,
},
res => {
if (res.merge) {
richPulls.forEach(pr => {
git.cherryPickMerge(app, pr.merge_commit_sha);
});
this.prompt(
{
name: 'push',
type: 'confirm',
message: 'Push these commits upstream?',
},
res => {
if (res.push) {
git.push(app);
this.log(
`Pushed upstream! Removing "${DOCS_LABEL}" label from pull requests.`
);
}
// TODO: actually test this
var removeLabelsPromises = richPulls.map(pr => {
return new Promise((resolve, reject) => {
const updatedLabels = pr.__originalIssue.labels
.filter(label => label.name !== DOCS_LABEL)
.map(label => label.name);
app.ghissues.editIssue(
pr.number,
{labels: updatedLabels},
(err, body) => {
if (err) {
reject(err);
} else {
resolve(pr);
}
}
);
});
});
Promise.all(removeLabelsPromises).then(() => {
this.log('Done!');
actionCB();
});
}
);
} else {
actionCB();
}
}
);
});
});
});
};

View File

@ -1,74 +0,0 @@
/**
* Command to init a project. This will create the .config.json file if it
* doesn't already exist.
*/
'use strict';
const chalk = require('chalk');
const fs = require('fs');
const path = require('path');
const untildify = require('untildify');
module.exports = function(vorpal, app) {
vorpal
.command('init')
.description('Initializes a .config.json file for use')
.action(function(args) {
return new Promise((resolve, reject) => {
fs.stat(app.PATH_TO_CONFIG, (err, stats) => {
if (stats) {
this.log('Config file exists, nothing to do.');
reject();
return;
}
this.prompt([
{
name: 'githubToken',
type: 'input',
message: `${chalk.bold('GitHub token?')} ${chalk.grey('(needs "repo" privs)')} `,
},
{
name: 'reactPath',
type: 'input',
message: `${chalk.bold('Location of local React checkout?')} `,
validate: input => {
let npath = path.normalize(untildify(input));
if (npath === '.') {
return 'Cannot be `.`';
}
let stats;
try {
stats = fs.statSync(npath);
} catch (e) {
return `Error: ${e}`;
}
if (!stats.isDirectory()) {
return `${npath} is not a directory.`;
}
// TODO: Look for markers indicating this is a React checkout.
return true;
},
},
]).then(answers => {
fs.writeFile(
app.PATH_TO_CONFIG,
JSON.stringify(answers, null, 2),
err => {
if (err) {
this.log('Error writing config file.', err);
reject();
}
resolve();
}
);
});
});
});
});
};

View File

@ -1,43 +0,0 @@
'use strict';
const npmUtils = require('./utils/npm');
const chalk = require('chalk');
const opn = require('opn');
module.exports = function(vorpal, app) {
vorpal
.command('npm-check-access')
.description('Check to ensure you have correct access to npm packages')
.action(function(args) {
return new Promise((resolve, reject) => {
const username = npmUtils.whoami(app);
if (!username) {
return reject(
`${chalk.red('FAILED')} You aren't logged in to npm. Please run ` +
`${chalk.underline(`npm adduser`)} and try again.`
);
}
this.log(`${chalk.green('OK')} Logged in as ${chalk.bold(username)}`);
const packagesNeedingAccess = npmUtils.packagesNeedingAccess(
app,
username
);
if (packagesNeedingAccess.length) {
this.log(
`${chalk.red('FAILED')} You don't have access to all of the packages ` +
`you need. We just opened a URL to file a new issue requesting access.`
);
opn(
npmUtils.generateAccessNeededIssue(username, packagesNeedingAccess),
{wait: false}
).then(resolve);
} else {
this.log(`${chalk.green('OK')} You can publish all React packages`);
resolve();
}
});
});
};

View File

@ -1,46 +0,0 @@
'use strict';
const npmUtils = require('./utils/npm');
const chalk = require('chalk');
module.exports = function(vorpal, app) {
vorpal
.command('npm-grant-access')
.description(
'Grant access to somebody to publish React. Assumes you ran "npm-check-access" first.'
)
.action(function(args) {
return new Promise((resolve, reject) => {
this.prompt({
type: 'input',
message: 'Who would you like to grant access to? ',
name: 'username',
}).then(answers => {
if (!answers.username) {
return reject('ABORTING');
}
const packagesNeedingAccess = npmUtils.packagesNeedingAccess(
app,
answers.username
);
if (packagesNeedingAccess.length) {
this.log(
`${chalk.yellow('PENDING')} Granting access to ${packagesNeedingAccess}`
);
npmUtils.grantAccess(app, answers.username, packagesNeedingAccess);
this.log(
`${chalk.green('OK')} Access has been granted to ${answers.username}.`
);
resolve();
} else {
this.log(
`${chalk.green('OK')} ${answers.username} already has access.`
);
resolve();
}
});
});
});
};

View File

@ -1,74 +0,0 @@
// Publishes the built npm packages from build/packages
// 1. Show checklist (automate later)
// 2. Prompt to ensure build is complete
// 3. Prompt for dist-tag?
'use strict';
const path = require('path');
const semver = require('semver');
const glob = require('glob');
module.exports = function(vorpal, app) {
vorpal
.command('npm-publish')
.description("After you've run grunt release, publishes the npm packages")
.action(function(args) {
return new Promise((resolve, reject) => {
const currentVersion = app.getReactVersion();
const isStable = semver.prerelease(currentVersion) === null;
this.log(`Preparing to publish v${currentVersion}`);
if (isStable) {
this.log(`"latest" dist-tag will be added to this version`);
}
// TODO: show checklist
this.prompt([
{
type: 'confirm',
message: 'Did you run `grunt build` or `grunt release` and bump the version number?',
default: false,
name: 'checklist',
},
]).then(answers => {
if (!answers.checklist) {
return reject('Complete the build process first');
}
// We'll grab all the tarballs and publish those directly. This
// is how we've historically done it, though in the past it was
// just npm publish pkg1.tgz && npm publish pkg2.tgz. This
// avoided the need to cd and publish.
const tgz = glob.sync('build/packages/*.tgz', {
cwd: app.config.reactPath,
});
// Just in case they didn't actually prep this.
// TODO: verify packages?
if (tgz.length === 0) {
reject('No built packages found');
}
// TODO: track success
tgz.forEach(file => {
this.log(app.execInRepo(`npm publish ${file} --tag=next`));
});
if (isStable) {
tgz.forEach(file => {
const pkg = path.parse(file).name;
this.log(
app.execInRepo(
`npm dist-tag add ${pkg}@${currentVersion} latest`
)
);
});
}
resolve();
});
});
});
};

View File

@ -1,11 +0,0 @@
/**
* Stupid command to run exit. 'q' is way shorter, like less.
*/
'use strict';
module.exports = function(vorpal, config) {
vorpal.command('q').hidden().action((args, cb) => {
vorpal.exec('exit').then(cb);
});
};

View File

@ -1,286 +0,0 @@
'use strict';
const chalk = require('chalk');
const pify = require('pify');
const git = require('./utils/git');
const SEMVER_LABELS = [
'semver-major',
'semver-minor',
'semver-patch',
'semver-exempt',
];
module.exports = function(vorpal, app) {
vorpal
.command('stable-prs')
.description(
'Get list of stable pull requests that need to be merged to the stable branch'
)
.action(function(args) {
// This makes the chaining easier but obfuscates the actual API, which is
// unfortunate. The standalone API will return the right data but
// promisified will get the response object and then we need to pull data
// off of that.
let listMilestones = pify(app.ghissues.listMilestones.bind(app.ghissues));
let listIssues = pify(app.ghissues.listIssues.bind(app.ghissues));
let editIssue = pify(app.ghissues.editIssue.bind(app.ghissues));
let getPullRequest = pify(app.ghrepo.getPullRequest.bind(app.ghrepo));
let targetMilestone = null;
return new Promise((resolveAction, rejectAction) => {
listMilestones(null)
.then(milestones => {
app.writeTo('milestones.json', milestones);
// Turn the milestones into choices for Inquirer
let milestoneChoices = milestones.map(milestone => {
return {
value: milestone.number,
name: milestone.title,
};
});
// We need label choices too
let labelChoices = SEMVER_LABELS.map(label => {
return {
value: label,
name: label.split('-')[1], // "major" instead of "semver-major"
};
});
// Ask about source milestone
// Ask about dest milestone
// TODO: allow creation of milestone here.
// Ask about which labels to pull from
return this.prompt([
{
name: 'srcMilestone',
type: 'list',
message: 'Which milestone should we pull PRs from?',
choices: milestoneChoices,
},
{
name: 'destMilestone',
type: 'list',
message: 'Which milestone should we assign PRs to upon completion?',
choices: milestoneChoices,
},
{
name: 'labels',
type: 'checkbox',
message: 'Which PRs should we select (use spacebar to check all that apply)',
choices: labelChoices,
},
]).then(answers => {
// this.log(JSON.stringify(answers, null, 2));
targetMilestone = answers.destMilestone;
let labels = {};
answers.labels.forEach(label => {
labels[label] = true;
});
return {
labels: labels,
query: {
milestone: answers.srcMilestone,
per_page: 100,
state: 'closed',
},
};
});
})
// Request issues, filter to applicable PRs
.then(({labels, query}) => {
return (
listIssues(query)
.then(issues => {
app.writeTo('stable-issues.json', issues);
// This API *could* return issues that aren't pull requests, so filter out
// issues that don't have pull_request set. Also filter out issues that
// aren't the right level of semver (eg if running a patch release)
let filteringLabels = Object.keys(labels).length > 0;
const pulls = issues.filter(issue => {
if (!issue.pull_request) {
return false;
}
if (!filteringLabels) {
return true;
}
return issue.labels.some(label => labels[label.name]);
});
app.writeTo('stable-prs.json', pulls);
return pulls;
})
// We need to convert the issues to PRs. We don't actually have enough
// info for the pull request data, so we need to get more. Then we'll
// do some filtering and sorting to make sure we apply merged PRs in
// the order they were originally committed to avoid conflicts as much
// as possible.
.then(pulls => {
return Promise.all(
pulls.map(pr => {
return getPullRequest(pr.number).then(richPR => {
app.writeTo(`pr-${pr.number}.json`, richPR);
richPR.__originalIssue = pr;
return richPR;
});
})
).then(richPRs => {
return richPRs
.filter(pr => {
if (!pr.merged_at) {
this.log(
`${chalk.yellow.bold('WARNING')} ${pr.html_url} was not merged,` +
` should have the milestone unset.`
);
return false;
}
return true;
})
.map(pr => {
pr.merged_at_date = new Date(pr.merged_at);
return pr;
})
.sort((a, b) => a.merged_at_date - b.merged_at_date);
});
})
);
})
// Quick prompt to double check that we should proceed.
.then(pulls => {
this.log(`Found ${chalk.bold(pulls.length)} pull requests:`);
pulls.forEach(pr => {
this.log(`${pr.html_url}: ${chalk.bold(pr.title)}`);
});
return this.prompt({
name: 'merge',
type: 'confirm',
message: `Merge these ${pulls.length} pull requests?`,
}).then(answers => {
return answers.merge ? pulls : rejectAction('cancelled');
});
})
// Ok, now we finally have rich pull request data. We can start cherry picking…
.then(pulls => {
// We're going to do some error handling here so we don't get into a
// terrible state.
this.log(`Found ${chalk.bold(pulls.length)} pull requests:`);
return new Promise((resolve, reject) => {
cherryPickPRs
.call(this, app, pulls)
.then(results => {
resolve(results);
})
.catch(err => {
this.log(
`${chalk.red.bold('ERROR')} Something went wrong and your repo is` +
` probably in a bad state. Sorry.`
);
resolve({
successful: [],
skipped: [],
didAbort: true,
});
});
});
})
// Update the milestone on successful PRs
// TODO: maybe handle didAbort and git reset --hard to a rev we read when we start the process?
.then(({successful, aborted, didAbort}) => {
if (didAbort) {
return undefined;
}
return Promise.all(
successful.map(pr => {
return editIssue(pr.number, {milestone: targetMilestone});
})
);
})
// yay, we're done
.then(() => {
resolveAction();
})
.catch(err => {
this.log('ERROR', err);
rejectAction();
});
});
});
};
function cherryPickPRs(app, prs) {
let successful = [];
let skipped = [];
return new Promise((resolve, reject) => {
// Build array of thenables
let promises = prs.map(pr => {
return () =>
new Promise((res, rej) => {
this.log(
chalk.yellow(`Cherry-picking #${pr.number} (${pr.title})...`)
);
let failed = false;
try {
git.cherryPickMerge(app, pr.merge_commit_sha);
} catch (e) {
failed = true;
}
if (!failed) {
this.log(chalk.green`Success`);
successful.push(pr);
return res();
}
return this.prompt({
name: 'handle',
type: 'list',
message: `${chalk.red`Failed!`} ${chalk.yellow('This must be resolved manually!')}`,
choices: [
{value: 'ok', name: 'Continue, mark successful'},
{value: 'skip', name: 'Continue, mark skipped'},
{
value: 'abort',
name: 'Abort process. Will require manual resetting of git state.',
},
],
}).then(answers => {
switch (answers.handle) {
case 'ok':
successful.push(pr);
break;
case 'skip':
skipped.push(pr);
break;
case 'abort':
return rej(pr.number);
}
res(pr.number);
});
});
});
// Since promises run on creation and we don't actually want that, we create
// an array of functions that return promises. We'll chain them here, not
// actually creating the next promise until we're ready.
var p = promises[0]();
for (let i = 1; i < promises.length; i++) {
p = p.then(() => promises[i]());
}
p
.then(() => {
resolve({successful, skipped, didAbort: false});
})
.catch(e => {
resolve({successful, skipped, didAbort: true});
});
});
}

View File

@ -1,46 +0,0 @@
// fetch upstream
// checkout 15-dev, update
// merge upstream/15-stable in
// done
'use strict';
const chalk = require('chalk');
var git = require('./utils/git');
module.exports = function(vorpal, app) {
vorpal
.command('start-release')
.description('Start the process for shipping the next release')
.action(function(args) {
return new Promise((resolve, reject) => {
// TODO: ensure that repo has upstream remote, correct branches setup.
if (!git.isClean(app)) {
this.log('ERROR: repo not in clean state');
return reject();
}
// Fetch upstream - this ensures upstream/15-stable is updated and we
// won't rely on the local branch.
git.fetch(app, 'upstream');
// Checkout 15-dev
git.checkout(app, '15-dev');
// Update to ensure latest commits are in. Will hit network again but
// shouldn't need to get anything.
git.pull(app);
// Merge 15-stable in
git.merge(app, 'upstream/15-stable', false);
this.log(chalk.green.bold(`OK!`));
this.log(
`You can now start cherry-picking commits to this branch using the "stable-prs" command.`
);
resolve();
});
});
};

View File

@ -1,87 +0,0 @@
'use strict';
function isClean(app) {
return getStatus(app) === '';
}
function getStatus(app) {
return app.execInRepo(`git status --untracked-files=no --porcelain`);
}
function getBranch(app) {
return app.execInRepo(`git symbolic-ref HEAD`);
}
function fetch(app, remote) {
return app.execInRepo(`git fetch ${remote}`);
}
function checkout(app, ref) {
return app.execInRepo(`git checkout ${ref}`);
}
function pull(app, ref) {
ref = ref || '';
return app.execInRepo(`git pull ${ref}`);
}
function merge(app, ref, ff, msg) {
let opts = [ff ? '--ff-only' : '--no-ff'];
if (!msg) {
opts.push('--no-edit');
} else {
opts.push(`-m '${msg}''`);
}
return app.execInRepo(`git merge ${opts.join(' ')} ${ref}`);
}
function tag(app, tag, ref) {
ref = ref || '';
return app.execInRepo(`git tag ${tag} ${ref}`);
}
function commit(app, msg, all) {
return app.execInRepo(`git commit -m '${msg}' ${all ? '-a' : ''}`);
}
function push(app, remote, refspec, tags) {
let opts = [remote, refspec, tags ? '--tags' : ''];
return app.execInRepo(`git push ${opts.join(' ')}`);
}
/**
* Cherry picks a single sha to the given branch. Very crude, but establishes
* some API. We don't know if the sha is a merge or a squashed commit so just
* try both.
*
* Assume we're already on the right branch.
*/
function cherryPickMerge(app, ref) {
// console.log(`cherry picking ${sha}`)
// git cherry-pick -x sha || git cherry-pick -x -m1 sha
try {
app.execInRepo(`git cherry-pick -x ${ref}`);
} catch (e) {
// Assume for now this just means it was actually a merge.
// TODO: gracefully handle other cases, like possibility the commit was
// already cherry-picked and should be skipped.
app.execInRepo(`git cherry-pick -x -m1 ${ref}`);
}
}
module.exports = {
getBranch,
getStatus,
isClean,
commit,
checkout,
fetch,
pull,
push,
merge,
tag,
cherryPickMerge,
};

View File

@ -1,42 +0,0 @@
'use strict';
const querystring = require('querystring');
const PACKAGES = [
'react-dom',
'react-native-renderer',
'react-test-renderer',
'react',
];
function whoami(app) {
return app.execInRepo('npm whoami');
}
function packagesNeedingAccess(app, username) {
let packages = JSON.parse(
app.execInRepo(`npm access ls-packages ${username}`)
);
return PACKAGES.filter(pkg => packages[pkg] !== 'read-write');
}
function generateAccessNeededIssue(username, packages) {
let data = {
title: `npm access request: ${username}`,
body: `In order to publish React to npm I need access to the following repositories:
${packages.map(pkg => `- [${pkg}](https://npm.im/${pkg})`).join('\n')}`,
};
return `https://github.com/facebook/react/issues/new?${querystring.stringify(data)}`;
}
function grantAccess(app, username, packages) {
packages.forEach(pkg => {
app.execInRepo(`npm owner add ${username} ${pkg}`);
});
}
module.exports.PACKAGES = PACKAGES;
module.exports.whoami = whoami;
module.exports.packagesNeedingAccess = packagesNeedingAccess;
module.exports.generateAccessNeededIssue = generateAccessNeededIssue;
module.exports.grantAccess = grantAccess;

View File

@ -1,176 +0,0 @@
'use strict';
const fs = require('fs');
const path = require('path');
const semver = require('semver');
const chalk = require('chalk');
const git = require('./utils/git');
// Overview
// 1. Display current version
// 2. Prompt for new version
// 3. Update appropriate files
// - package.json (version)
// - npm-shrinkwrap.json (version)
// - packages/react/package.json (version)
// - packages/react-addons/package.json (version, peerDependencies.react)
// - packages/react-dom/package.json (version, peerDependencies.react)
// - packages/react-native-renderer/package.json (version, peerDependencies.react)
// - packages/react-test-renderer/package.json (version, peerDependencies.react)
// - src/ReactVersion.js (module.exports)
// 4. Commit?
function updateJSON(path, fields, value) {
let data;
try {
data = JSON.parse(fs.readFileSync(path, 'utf8'));
} catch (e) {
this.log(chalk.red('ERROR') + ` ${path} doesn't exist… skipping.`);
return;
}
fields.forEach(field => {
let fieldPath = field.split('.');
if (fieldPath.length === 1) {
data[field] = value;
} else {
// assume length of 2 is some dep.react and we can just use ^ because we
// know it's true. do something more versatile later
data[fieldPath[0]][fieldPath[1]] = '^' + value;
}
});
fs.writeFileSync(path, JSON.stringify(data, null, 2) + '\n');
}
module.exports = function(vorpal, app) {
vorpal
.command('version')
.description('Update the version of React, useful while publishing')
.action(function(args, actionCB) {
let currentVersion = app.getReactVersion();
// TODO: See if we can do a better job for handling pre* bumps. The ones
// semver adds are of the form -0, but we've used -alpha.0 or -rc.0.
// 'prerelease' will increment those properly (but otherwise has the same problem).
// Live with it for now since it won't be super common. Write docs.
let choices = ['prerelease', 'patch', 'minor', 'major'].map(release => {
let version = semver.inc(currentVersion, release);
return {
value: version,
name: `${chalk.bold(version)} (${release})`,
};
});
choices.push('Other');
this.prompt([
{
type: 'list',
name: 'version',
choices: choices,
message: `New version (currently ${chalk.bold(currentVersion)}):`,
},
{
type: 'input',
name: 'version',
message: `New version (currently ${chalk.bold(currentVersion)}): `,
when: res => res.version === 'Other',
},
]).then(res => {
let newVersion = semver.valid(res.version);
if (!newVersion) {
return actionCB(
`${chalk.red('ERROR')} ${res.version} is not a semver-valid version`
);
}
this.log(`Updating to ${newVersion}`);
// The JSON files. They're all updated the same way so batch.
[
{
file: 'package.json',
fields: ['version'],
},
{
file: 'npm-shrinkwrap.json',
fields: ['version'],
},
{
file: 'packages/react/package.json',
fields: ['version'],
},
{
file: 'packages/react-addons/package.json',
fields: ['version', 'peerDependencies.react'],
},
{
file: 'packages/react-dom/package.json',
fields: ['version', 'peerDependencies.react'],
},
{
file: 'packages/react-native-renderer/package.json',
fields: ['version', 'peerDependencies.react'],
},
{
file: 'packages/react-noop-renderer/package.json',
fields: ['version'],
},
{
file: 'packages/react-test-renderer/package.json',
fields: ['version', 'peerDependencies.react'],
},
].forEach(opts => {
updateJSON.apply(this, [
path.join(app.config.reactPath, opts.file),
opts.fields,
newVersion,
]);
});
// We also need to update src/ReactVersion.js which has the version in
// string form in JS code. We'll just do a string replace.
const PATH_TO_REACTVERSION = path.join(
app.config.reactPath,
'src/ReactVersion.js'
);
let reactVersionContents = fs.readFileSync(
PATH_TO_REACTVERSION,
'utf8'
);
reactVersionContents = reactVersionContents.replace(
currentVersion,
newVersion
);
fs.writeFileSync(PATH_TO_REACTVERSION, reactVersionContents);
this.prompt([
{
name: 'commit',
type: 'confirm',
message: 'Commit these changes (`git commit -a`)?',
default: true,
},
{
name: 'tag',
type: 'confirm',
message: 'Tag the version commit (not necessary for non-stable releases)?',
default: true,
when: res => res.commit,
},
]).then(res => {
if (res.commit) {
git.commit(app, newVersion, true);
}
if (res.tag) {
git.tag(app, `v${newVersion}`);
}
actionCB();
});
});
});
};

View File

@ -1,24 +0,0 @@
{
"private": true,
"name": "react-release-manager",
"version": "1.0.0",
"description": "Tool to manage React releases",
"main": "cli.js",
"dependencies": {
"chalk": "^1.1.3",
"colors": "^1.1.2",
"github-api": "^2.2.0",
"glob": "^7.0.5",
"opn": "^4.0.2",
"pify": "^2.3.0",
"semver": "^5.3.0",
"untildify": "^3.0.2",
"vorpal": "^1.10.10"
},
"devDependencies": {},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Paul O'Shannessy <paul@oshannessy.com>",
"license": "MIT"
}

13
scripts/release/README.md Normal file
View File

@ -0,0 +1,13 @@
# React Release Script
At a high-level, the new release script runs in 2 passes: **build** and **publish**.
1. The **build** script does the heavy lifting (eg checking CI, running automated tests, building Rollup bundles) and then prints instructions for manual verification.
1. The **release** script then publishes the built artifacts to NPM and pushes to GitHub.
Run a script without any parameters to see its usage, eg:
```
./scripts/release/build.js
./scripts/release/publish.js
```
Each script will guide the release engineer through any necessary steps (including environment setup and manual testing steps).

View File

@ -0,0 +1,32 @@
#!/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');
};

View File

@ -0,0 +1,46 @@
#!/usr/bin/env node
'use strict';
const chalk = require('chalk');
const http = require('request-promise-json');
const {execRead, logPromise} = require('../utils');
// https://circleci.com/docs/api/v1-reference/#projects
const CIRCLE_CI_BASE_URL =
'https://circleci.com/api/v1.1/project/github/facebook/react/tree/master';
const check = async ({cwd}) => {
const token = process.env.CIRCLE_CI_API_TOKEN;
const uri = `${CIRCLE_CI_BASE_URL}?circle-token=${token}&limit=1`;
const response = await http.get(uri, true);
const {outcome, status, vcs_revision: ciRevision} = response[0];
const gitRevision = await execRead('git rev-parse HEAD', {cwd});
if (gitRevision !== ciRevision) {
throw Error(
chalk`
CircleCI is stale
{white The latest Git revision is {yellow.bold ${gitRevision}}}
{white The most recent CircleCI revision is {yellow.bold ${ciRevision}}}
{white Please wait for CircleCI to catch up.}
`
);
} else if (outcome !== 'success') {
throw Error(
chalk`
CircleCI failed
{white The most recent CircleCI build has a status of {red.bold ${outcome || status}}}
{white Please retry this build in CircleCI if you believe this is an error.}
`
);
}
};
module.exports = async params => {
return logPromise(check(params), 'Checking CircleCI status');
};

View File

@ -0,0 +1,27 @@
#!/usr/bin/env node
'use strict';
const chalk = require('chalk');
module.exports = () => {
if (!process.env.CIRCLE_CI_API_TOKEN) {
throw Error(
chalk`
{red Missing CircleCI API token}
{white The CircleCI API is used to check the status of the latest commit.}
{white This API requires a token which must be exposed via a {yellow.bold CIRCLE_CI_API_TOKEN} environment var.}
{white In order to run this script you will need to create your own API token.}
{white Instructions can be found at:}
{blue.bold https://circleci.com/docs/api/v1-reference/#getting-started}
{white To make this token available to the release script, add it to your {yellow.bold .bash_profile} like so:}
{gray # React release script}
{white export CIRCLE_CI_API_TOKEN=<your-token-here>}
`
);
}
};

View File

@ -0,0 +1,41 @@
#!/usr/bin/env node
'use strict';
const chalk = require('chalk');
const {execRead, logPromise} = require('../utils');
const {projects} = require('../config');
module.exports = async () => {
const currentUser = await execRead('npm whoami');
const failedProjects = [];
const checkProject = async project => {
const owners = (await execRead(`npm owner ls ${project}`))
.split('\n')
.filter(owner => owner)
.map(owner => owner.split(' ')[0]);
if (!owners.includes(currentUser)) {
failedProjects.push(project);
}
};
await logPromise(
Promise.all(projects.map(checkProject)),
`Checking ${chalk.yellow.bold(currentUser)}'s NPM permissions`
);
if (failedProjects.length) {
throw Error(
chalk`
Insufficient NPM permissions
{white NPM user {yellow.bold ${currentUser}} is not an owner for:}
{red ${failedProjects.join(', ')}}
{white Please contact a React team member to be added to the above project(s).}
`
);
}
};

View File

@ -0,0 +1,57 @@
#!/usr/bin/env node
'use strict';
const chalk = require('chalk');
const {readJson} = require('fs-extra');
const {join} = require('path');
const {dependencies, projects} = require('../config');
const {logPromise} = require('../utils');
const check = async ({cwd}) => {
const rootPackage = await readJson(join(cwd, 'package.json'));
const projectPackages = [];
for (let i = 0; i < projects.length; i++) {
const project = projects[i];
projectPackages.push(
await readJson(join(cwd, join('packages', project), 'package.json'))
);
}
const invalidDependencies = [];
const checkModule = module => {
const rootVersion = rootPackage.devDependencies[module];
projectPackages.forEach(projectPackage => {
const projectVersion = projectPackage.dependencies[module];
if (rootVersion !== projectVersion && projectVersion !== undefined) {
invalidDependencies.push(
`${module} is ${chalk.red.bold(rootVersion)} in root but ` +
`${chalk.red.bold(projectVersion)} in ${projectPackage.name}`
);
}
});
};
await Promise.all(dependencies.map(checkModule));
if (invalidDependencies.length) {
throw Error(
chalk`
Dependency mismatch
{white The following dependencies do not match between the root package and NPM dependencies:}
${invalidDependencies
.map(dependency => chalk.white(dependency))
.join('\n')}
`
);
}
};
module.exports = async ({cwd}) => {
return logPromise(check({cwd}), 'Checking runtime dependencies');
};

View File

@ -0,0 +1,20 @@
#!/usr/bin/env node
'use strict';
const chalk = require('chalk');
const {execRead} = require('../utils');
module.exports = async ({cwd}) => {
const status = await execRead('git diff HEAD', {cwd});
if (status) {
throw Error(
chalk`
Uncommitted local changes
{white Please revert or commit all local changes before making a release.}
`
);
}
};

View File

@ -0,0 +1,12 @@
#!/usr/bin/env node
'use strict';
const {exec} = require('child-process-promise');
const {logPromise} = require('../utils');
const install = async ({cwd}) => await exec('yarn', {cwd});
module.exports = async ({cwd}) => {
return logPromise(install({cwd}), 'Installing NPM dependencies');
};

View File

@ -0,0 +1,55 @@
#!/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} = require('../config');
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: 'Automated pre-release build script.',
},
{
header: 'Options',
optionList: paramDefinitions,
},
{
header: 'Examples',
content: [
{
desc: '1. A concise example.',
example: '$ ./build.js [bold]{-v} [underline]{16.0.0}',
},
{
desc: '2. Dry run build a release candidate (no git commits).',
example: '$ ./build.js [bold]{--dry} [bold]{-v} [underline]{16.0.0-rc.0}',
},
{
desc: '3. Release from another checkout.',
example: '$ ./build.js [bold]{--version}=[underline]{16.0.0} [bold]{--path}=/path/to/react/repo',
},
],
},
]);
console.log(usage);
process.exit(1);
}
return {
...params,
cwd: params.path, // For script convenience
};
};

View File

@ -0,0 +1,44 @@
#!/usr/bin/env node
'use strict';
const chalk = require('chalk');
const {getUnexecutedCommands} = require('../utils');
const CHANGELOG_PATH =
'https://github.com/facebook/react/edit/master/CHANGELOG.md';
module.exports = params => {
const command =
`./publish.js -v ${params.version}` +
(params.path ? ` -p ${params.path}` : '') +
(params.dry ? ' --dry' : '');
console.log(
chalk`
{green.bold Build successful!}
${getUnexecutedCommands()}
Next there are a couple of manual steps:
{bold.underline Step 1: Update the CHANGELOG}
Here are a few things to keep in mind:
The changes should be easy to understand. (Friendly one-liners are better than PR titles.)
Make sure all contributors are credited.
Verify that the markup is valid by previewing it in the editor: {blue.bold ${CHANGELOG_PATH}}
{bold.underline Step 2: Smoke test the packages}
1. Open {yellow.bold fixtures/packaging/babel-standalone/dev.html} in the browser.
2. It should say {italic "Hello world!"}
3. Next go to {yellow.bold fixtures/packaging} 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}
7. Verify every iframe shows {italic "Hello world!"}
After completing the above steps, resume the release process by running:
{yellow.bold ${command}}
`.replace(/\n +/g, '\n')
);
};

View File

@ -0,0 +1,33 @@
#!/usr/bin/env node
'use strict';
const chalk = require('chalk');
const {exec} = require('child-process-promise');
const {logPromise} = require('../utils');
const runYarnTask = async (cwd, task, errorMessage) => {
try {
await exec(`yarn ${task}`, {cwd});
} catch (error) {
throw Error(
chalk`
${errorMessage}
{white ${error.stdout}}
`
);
}
};
module.exports = async ({cwd}) => {
await logPromise(runYarnTask(cwd, 'lint', 'Lint failed'), 'Running ESLint');
await logPromise(
runYarnTask(cwd, 'flow', 'Flow failed'),
'Running Flow checks'
);
await logPromise(
runYarnTask(cwd, 'jest', 'Jest failed'),
'Running Jest tests'
);
};

View File

@ -0,0 +1,20 @@
#!/usr/bin/env node
'use strict';
const chalk = require('chalk');
const {exec} = require('child-process-promise');
const {logPromise} = require('../utils');
const update = async ({cwd}) => {
await exec('git fetch', {cwd});
await exec('git checkout master', {cwd});
await exec('git pull', {cwd});
};
module.exports = async ({cwd}) => {
return logPromise(
update({cwd}),
`Updating checkout ${chalk.yellow.bold(cwd)}`
);
};

View File

@ -0,0 +1,71 @@
#!/usr/bin/env node
'use strict';
const chalk = require('chalk');
const {exec} = require('child-process-promise');
const {readFileSync, writeFileSync} = require('fs');
const {readJson, writeJson} = require('fs-extra');
const {join} = require('path');
const semver = require('semver');
const {projects} = require('../config');
const {execUnlessDry, logPromise} = require('../utils');
const update = async ({cwd, dry, version}) => {
try {
// Update root package.json
const packagePath = join(cwd, 'package.json');
const rootPackage = await readJson(packagePath);
rootPackage.version = version;
await writeJson(packagePath, rootPackage, {spaces: 2});
// Update ReactVersion source file
const reactVersionPath = join(cwd, 'src/ReactVersion.js');
const reactVersion = readFileSync(reactVersionPath, 'utf8').replace(
/module\.exports = '[^']+';/,
`module.exports = '${version}';`
);
writeFileSync(reactVersionPath, reactVersion);
// Update renderer versions and peer dependencies
const updateProjectPackage = async project => {
const path = join(cwd, 'packages', project, 'package.json');
const json = await readJson(path);
// Unstable packages (eg version < 1.0) are treated differently.
// In order to simplify DX for the release engineer,
// These packages are auto-incremented by a minor version number.
if (semver.lt(json.version, '1.0.0')) {
json.version = `0.${semver.minor(json.version) + 1}.0`;
} else {
json.version = version;
}
if (project !== 'react') {
json.peerDependencies.react = `^${version}`;
}
await writeJson(path, json, {spaces: 2});
};
await Promise.all(projects.map(updateProjectPackage));
// Version sanity check
await exec('yarn version-check', {cwd});
await execUnlessDry(
`git commit -am "Updating package versions for release ${version}"`,
{cwd, dry}
);
} catch (error) {
throw Error(
chalk`
Failed while updating package versions
{white ${error.message}}
`
);
}
};
module.exports = async params => {
return logPromise(update(params), 'Updating package versions');
};

View File

@ -0,0 +1,38 @@
#!/usr/bin/env node
'use strict';
const chalk = require('chalk');
const {exec} = require('child-process-promise');
const {dependencies} = require('../config');
const {execRead, execUnlessDry, logPromise} = require('../utils');
const update = async ({cwd, dry, version}) => {
await exec(`yarn upgrade ${dependencies.join(' ')}`, {cwd});
const modifiedFiles = await execRead('git ls-files -m', {cwd});
// If yarn.lock has changed we should commit it.
// If anything else has changed, it's an error.
if (modifiedFiles) {
if (modifiedFiles !== 'yarn.lock') {
throw Error(
chalk`
Unexpected modifications
{white The following files have been modified unexpectedly:}
{gray ${modifiedFiles}}
`
);
}
await execUnlessDry(
`git commit -am "Updating yarn.lock file for ${version} release"`,
{cwd, dry}
);
}
};
module.exports = async params => {
return logPromise(update(params), 'Upgrading NPM dependencies');
};

View File

@ -0,0 +1,20 @@
'use strict';
const chalk = require('chalk');
const {readJson} = require('fs-extra');
const {join} = require('path');
const semver = require('semver');
module.exports = async ({cwd, version}) => {
if (!semver.valid(version)) {
throw Error('Invalid version specified');
}
const rootPackage = await readJson(join(cwd, 'package.json'));
if (!semver.gt(version, rootPackage.version)) {
throw Error(
chalk`Version {white ${rootPackage.version}} has already been published`
);
}
};

55
scripts/release/build.js Executable file
View File

@ -0,0 +1,55 @@
#!/usr/bin/env node
'use strict';
const chalk = require('chalk');
const logUpdate = require('log-update');
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 checkNpmPermissions = require('./build-commands/check-npm-permissions');
const checkPackageDependencies = require('./build-commands/check-package-dependencies');
const checkUncommittedChanges = require('./build-commands/check-uncommitted-changes');
const installYarnDependencies = require('./build-commands/install-yarn-dependencies');
const parseBuildParameters = require('./build-commands/parse-build-parameters');
const printPostBuildSummary = require('./build-commands/print-post-build-summary');
const runAutomatedTests = require('./build-commands/run-automated-tests');
const updateGit = require('./build-commands/update-git');
const updatePackageVersions = require('./build-commands/update-package-versions');
const updateYarnDependencies = require('./build-commands/update-yarn-dependencies');
const validateVersion = require('./build-commands/validate-version');
// Follows the steps outlined in github.com/facebook/react/issues/10620
const run = async () => {
try {
const params = parseBuildParameters();
await checkEnvironmentVariables(params);
await validateVersion(params);
await checkUncommittedChanges(params);
await checkNpmPermissions(params);
await updateGit(params);
await checkCircleCiStatus(params);
await installYarnDependencies(params);
await checkPackageDependencies(params);
await updateYarnDependencies(params);
await runAutomatedTests(params);
await updatePackageVersions(params);
await buildArtifacts(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);
}
};
run();

39
scripts/release/config.js Normal file
View File

@ -0,0 +1,39 @@
'use strict';
const dependencies = ['fbjs', 'object-assign', 'prop-types'];
const projects = [
'react',
'react-art',
'react-dom',
'react-reconciler',
'react-test-renderer',
];
const paramDefinitions = [
{
name: 'dry',
type: Boolean,
description: 'Build artifacts but do not commit or publish',
defaultValue: false,
},
{
name: 'path',
type: String,
alias: 'p',
description: 'Location of React repository to release; defaults to [bold]{cwd}',
defaultValue: '.',
},
{
name: 'version',
type: String,
alias: 'v',
description: 'Semantic version number',
},
];
module.exports = {
dependencies,
paramDefinitions,
projects,
};

View File

@ -0,0 +1,19 @@
{
"name": "react-release-script",
"version": "0.0.0",
"private": true,
"scripts": {},
"license": "MIT",
"dependencies": {
"chalk": "^2.1.0",
"child-process-promise": "^2.2.1",
"cli-spinners": "^1.1.0",
"command-line-args": "^4.0.7",
"command-line-usage": "^4.0.1",
"figlet": "^1.2.0",
"fs-extra": "^4.0.2",
"log-update": "^2.1.0",
"request-promise-json": "^1.0.4",
"semver": "^5.4.1"
}
}

View File

@ -0,0 +1,24 @@
#!/usr/bin/env node
'use strict';
const chalk = require('chalk');
const {existsSync} = require('fs');
const {readJson} = require('fs-extra');
const {join} = require('path');
module.exports = async ({cwd, version}) => {
const packagePath = join(cwd, 'build', 'packages', 'react', 'package.json');
if (!existsSync(packagePath)) {
throw Error('No build found');
}
const packageJson = await readJson(packagePath);
if (packageJson.version !== version) {
throw Error(
chalk`Expected version {bold.white ${version}} but found {bold.white ${packageJson.version}}`
);
}
};

View File

@ -0,0 +1,24 @@
#!/usr/bin/env node
'use strict';
const {exec} = require('child-process-promise');
const {execRead, logPromise} = require('../utils');
const update = async ({cwd, dry, version}) => {
const modifiedFiles = await execRead('git ls-files -m', {cwd});
if (!dry && modifiedFiles.includes('CHANGELOG.md')) {
await exec('git add CHANGELOG.md', {cwd});
await exec(
`git commit -am "Updating CHANGELOG.md for ${version} release"`,
{
cwd,
}
);
}
};
module.exports = async params => {
return logPromise(update(params), 'Committing CHANGELOG updates');
};

View File

@ -0,0 +1,55 @@
#!/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} = require('../config');
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: 'Automated release publishing script.',
},
{
header: 'Options',
optionList: paramDefinitions,
},
{
header: 'Examples',
content: [
{
desc: '1. A concise example.',
example: '$ ./publish.js [bold]{-v} [underline]{16.0.0}',
},
{
desc: '2. Dry run publish a release candidate.',
example: '$ ./publish.js [bold]{--dry} [bold]{-v} [underline]{16.0.0-rc.0}',
},
{
desc: '3. Release from another checkout.',
example: '$ ./publish.js [bold]{--version}=[underline]{16.0.0} [bold]{--path}=/path/to/react/repo',
},
],
},
]);
console.log(usage);
process.exit(1);
}
return {
...params,
cwd: params.path, // For script convenience
};
};

View File

@ -0,0 +1,41 @@
#!/usr/bin/env node
'use strict';
const chalk = require('chalk');
const {getUnexecutedCommands} = require('../utils');
module.exports = ({version}) => {
console.log(
chalk`
{green.bold Publish successful!}
${getUnexecutedCommands()}
Next there are a couple of manual steps:
{bold.underline Step 1: Create GitHub release}
1. Open new release page: {blue.bold https://github.com/facebook/react/releases/new}
2. Choose {bold ${version}} from the dropdown menu
3. Paste the new release notes from {yellow.bold CHANGELOG.md}
4. Attach all files in {yellow.bold build/dist/*.js} except {yellow.bold react-art.*} to the release.
5. Press {bold "Publish release"}!
{bold.underline Step 2: Update the version on reactjs.org}
1. Git clone (or update) {blue.bold https://github.com/reactjs/reactjs.org}
2. Open the {bold.yellow src/site-constants.js} file
3. Update the {bold version} value to {bold ${version}}
4. Open a Pull Request to {bold master}
{bold.underline Step 3: Test the new release}
1. Install CRA: {bold npm i -g create-react-app}
2. Create a test application: {bold create-react-app myapp}
3. Run it: {bold cd myapp && npm start}
{bold.underline Step 4: Notify the DOM team}
1. Notify DOM team members: {bold @nhunzaker @jquense @aweary}
`.replace(/\n +/g, '\n')
);
};

View File

@ -0,0 +1,52 @@
#!/usr/bin/env node
'use strict';
const chalk = require('chalk');
const {join} = require('path');
const semver = require('semver');
const {execRead, execUnlessDry, logPromise} = require('../utils');
const {projects} = require('../config');
const push = async ({cwd, dry, version}) => {
const errors = [];
const tag = semver.prerelease(version) ? 'next' : 'latest';
const publishProject = async project => {
try {
const path = join(cwd, 'build', 'packages', project);
await execUnlessDry(`npm publish --tag ${tag}`, {cwd: path, dry});
if (!dry) {
const status = JSON.parse(
await execRead(`npm info ${project} dist-tags --json`)
);
const remoteVersion = status[tag];
if (remoteVersion !== version) {
throw Error(
chalk`Publised version {yellow.bold ${version}} for ` +
`{bold ${project}} but NPM shows {yellow.bold ${remoteVersion}}`
);
}
}
} catch (error) {
errors.push(error.message);
}
};
await Promise.all(projects.map(publishProject));
if (errors.length > 0) {
throw Error(
chalk`
Failure publishing to NPM
{white ${errors.join('\n')}}`
);
}
};
module.exports = async params => {
return logPromise(push(params), 'Pushing to git remote');
};

View File

@ -0,0 +1,14 @@
#!/usr/bin/env node
'use strict';
const {execUnlessDry, logPromise} = require('../utils');
const push = async ({cwd, dry}) => {
await execUnlessDry('git push', {cwd, dry});
await execUnlessDry('git push --tags', {cwd, dry});
};
module.exports = async params => {
return logPromise(push(params), 'Pushing to git remote');
};

39
scripts/release/publish.js Executable file
View File

@ -0,0 +1,39 @@
#!/usr/bin/env node
'use strict';
const chalk = require('chalk');
const logUpdate = require('log-update');
const checkBuildStatus = require('./publish-commands/check-build-status');
const commitChangelog = require('./publish-commands/commit-changelog');
const parsePublishParams = require('./publish-commands/parse-publish-params');
const printPostPublishSummary = require('./publish-commands/print-post-publish-summary');
const pushGitRemote = require('./publish-commands/push-git-remote');
const publishToNpm = require('./publish-commands/publish-to-npm');
// Follows the steps outlined in github.com/facebook/react/issues/10620
const run = async () => {
const params = parsePublishParams();
try {
await checkBuildStatus(params);
await commitChangelog(params);
await pushGitRemote(params);
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);
}
};
run();

68
scripts/release/utils.js Normal file
View File

@ -0,0 +1,68 @@
'use strict';
const chalk = require('chalk');
const {dots} = require('cli-spinners');
const {exec} = require('child-process-promise');
const logUpdate = require('log-update');
const execRead = async (command, options) => {
const {stdout} = await exec(command, options);
return stdout.trim();
};
const unexecutedCommands = [];
const execUnlessDry = async (command, {cwd, dry}) => {
if (dry) {
unexecutedCommands.push(`${command} # {cwd: ${cwd}}`);
} else {
await exec(command, {cwd});
}
};
const getUnexecutedCommands = () => {
if (unexecutedCommands.length > 0) {
return chalk`
The following commands were not executed because of the {bold --dry} flag:
{gray ${unexecutedCommands.join('\n')}}
`;
} else {
return '';
}
};
const logPromise = async (promise, text, completedLabel = '') => {
const {frames, interval} = dots;
let index = 0;
const id = setInterval(() => {
index = ++index % frames.length;
logUpdate(
`${chalk.yellow(frames[index])} ${text} ${chalk.gray('- this may take a few seconds')}`
);
}, interval);
try {
const returnValue = await promise;
clearInterval(id);
logUpdate(`${chalk.green('✓')} ${text} ${chalk.gray(completedLabel)}`);
logUpdate.done();
return returnValue;
} catch (error) {
logUpdate.clear();
throw error;
}
};
module.exports = {
execRead,
execUnlessDry,
getUnexecutedCommands,
logPromise,
};

584
scripts/release/yarn.lock Normal file
View File

@ -0,0 +1,584 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
ajv@^5.1.0:
version "5.2.3"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.3.tgz#c06f598778c44c6b161abafe3466b81ad1814ed2"
dependencies:
co "^4.6.0"
fast-deep-equal "^1.0.0"
json-schema-traverse "^0.3.0"
json-stable-stringify "^1.0.1"
ansi-escape-sequences@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/ansi-escape-sequences/-/ansi-escape-sequences-4.0.0.tgz#e0ecb042958b71e42942d35c1fcf1d9b00a0f67e"
dependencies:
array-back "^2.0.0"
ansi-escapes@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-2.0.0.tgz#5bae52be424878dd9783e8910e3fc2922e83c81b"
ansi-regex@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
ansi-styles@^3.1.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88"
dependencies:
color-convert "^1.9.0"
array-back@^1.0.3, array-back@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/array-back/-/array-back-1.0.4.tgz#644ba7f095f7ffcf7c43b5f0dc39d3c1f03c063b"
dependencies:
typical "^2.6.0"
array-back@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/array-back/-/array-back-2.0.0.tgz#6877471d51ecc9c9bfa6136fb6c7d5fe69748022"
dependencies:
typical "^2.6.1"
asn1@~0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86"
assert-plus@1.0.0, assert-plus@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
aws-sign2@~0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
aws4@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
bcrypt-pbkdf@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d"
dependencies:
tweetnacl "^0.14.3"
boom@4.x.x:
version "4.3.1"
resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31"
dependencies:
hoek "4.x.x"
boom@5.x.x:
version "5.2.0"
resolved "https://registry.yarnpkg.com/boom/-/boom-5.2.0.tgz#5dd9da6ee3a5f302077436290cb717d3f4a54e02"
dependencies:
hoek "4.x.x"
caseless@~0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
chalk@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.1.0.tgz#ac5becf14fa21b99c6c92ca7a7d7cfd5b17e743e"
dependencies:
ansi-styles "^3.1.0"
escape-string-regexp "^1.0.5"
supports-color "^4.0.0"
child-process-promise@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/child-process-promise/-/child-process-promise-2.2.1.tgz#4730a11ef610fad450b8f223c79d31d7bdad8074"
dependencies:
cross-spawn "^4.0.2"
node-version "^1.0.0"
promise-polyfill "^6.0.1"
cli-cursor@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5"
dependencies:
restore-cursor "^2.0.0"
cli-spinners@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-1.1.0.tgz#f1847b168844d917a671eb9d147e3df497c90d06"
co@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
color-convert@^1.9.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a"
dependencies:
color-name "^1.1.1"
color-name@^1.1.1:
version "1.1.3"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
combined-stream@^1.0.5, combined-stream@~1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009"
dependencies:
delayed-stream "~1.0.0"
command-line-args@^4.0.7:
version "4.0.7"
resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-4.0.7.tgz#f8d1916ecb90e9e121eda6428e41300bfb64cc46"
dependencies:
array-back "^2.0.0"
find-replace "^1.0.3"
typical "^2.6.1"
command-line-usage@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-4.0.1.tgz#d89cf16c8ae71e8e8a6e6aabae1652af76ff644e"
dependencies:
ansi-escape-sequences "^4.0.0"
array-back "^2.0.0"
table-layout "^0.4.1"
typical "^2.6.1"
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"
cross-spawn@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-4.0.2.tgz#7b9247621c23adfdd3856004a823cbe397424d41"
dependencies:
lru-cache "^4.0.1"
which "^1.2.9"
cryptiles@3.x.x:
version "3.1.2"
resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe"
dependencies:
boom "5.x.x"
dashdash@^1.12.0:
version "1.14.1"
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
dependencies:
assert-plus "^1.0.0"
deep-extend@~0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.5.0.tgz#6ef4a09b05f98b0e358d6d93d4ca3caec6672803"
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
ecc-jsbn@~0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505"
dependencies:
jsbn "~0.1.0"
escape-string-regexp@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
extend@~3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
extsprintf@1.3.0, extsprintf@^1.2.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
fast-deep-equal@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff"
figlet@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/figlet/-/figlet-1.2.0.tgz#6c46537378fab649146b5a6143dda019b430b410"
find-replace@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-1.0.3.tgz#b88e7364d2d9c959559f388c66670d6130441fa0"
dependencies:
array-back "^1.0.4"
test-value "^2.1.0"
forever-agent@~0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
form-data@~2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf"
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.5"
mime-types "^2.1.12"
fs-extra@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.2.tgz#f91704c53d1b461f893452b0c307d9997647ab6b"
dependencies:
graceful-fs "^4.1.2"
jsonfile "^4.0.0"
universalify "^0.1.0"
getpass@^0.1.1:
version "0.1.7"
resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
dependencies:
assert-plus "^1.0.0"
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"
har-schema@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
har-validator@~5.0.3:
version "5.0.3"
resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd"
dependencies:
ajv "^5.1.0"
har-schema "^2.0.0"
has-flag@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51"
hawk@~6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/hawk/-/hawk-6.0.2.tgz#af4d914eb065f9b5ce4d9d11c1cb2126eecc3038"
dependencies:
boom "4.x.x"
cryptiles "3.x.x"
hoek "4.x.x"
sntp "2.x.x"
hoek@4.x.x:
version "4.2.0"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d"
http-signature@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
dependencies:
assert-plus "^1.0.0"
jsprim "^1.2.2"
sshpk "^1.7.0"
is-fullwidth-code-point@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
is-typedarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
isexe@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
isstream@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
jsbn@~0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
json-schema-traverse@^0.3.0:
version "0.3.1"
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340"
json-schema@0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
json-stable-stringify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af"
dependencies:
jsonify "~0.0.0"
json-stringify-safe@~5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
jsonfile@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
optionalDependencies:
graceful-fs "^4.1.6"
jsonify@~0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
jsprim@^1.2.2:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
dependencies:
assert-plus "1.0.0"
extsprintf "1.3.0"
json-schema "0.2.3"
verror "1.10.0"
lodash.padend@^4.6.1:
version "4.6.1"
resolved "https://registry.yarnpkg.com/lodash.padend/-/lodash.padend-4.6.1.tgz#53ccba047d06e158d311f45da625f4e49e6f166e"
log-update@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/log-update/-/log-update-2.1.0.tgz#ea37258b5354edb02e73b29190016c87d1c87141"
dependencies:
ansi-escapes "^2.0.0"
cli-cursor "^2.0.0"
wrap-ansi "^3.0.1"
lru-cache@^4.0.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55"
dependencies:
pseudomap "^1.0.2"
yallist "^2.1.2"
mime-db@~1.30.0:
version "1.30.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01"
mime-types@^2.1.12, mime-types@~2.1.17:
version "2.1.17"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a"
dependencies:
mime-db "~1.30.0"
mimic-fn@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18"
node-version@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/node-version/-/node-version-1.1.0.tgz#f437d7ba407e65e2c4eaef8887b1718ba523d4f0"
oauth-sign@~0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
onetime@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
dependencies:
mimic-fn "^1.0.0"
performance-now@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
promise-polyfill@^6.0.1:
version "6.0.2"
resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-6.0.2.tgz#d9c86d3dc4dc2df9016e88946defd69b49b41162"
pseudomap@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
punycode@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
q@^1.1.2:
version "1.5.0"
resolved "https://registry.yarnpkg.com/q/-/q-1.5.0.tgz#dd01bac9d06d30e6f219aecb8253ee9ebdc308f1"
qs@~6.5.1:
version "6.5.1"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
reduce-flatten@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-1.0.1.tgz#258c78efd153ddf93cb561237f61184f3696e327"
request-promise-json@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/request-promise-json/-/request-promise-json-1.0.4.tgz#71119e82a263c900a72be255c65e49a3d3ca5e6f"
dependencies:
q "^1.1.2"
request "^2.51.0"
request@^2.51.0:
version "2.83.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356"
dependencies:
aws-sign2 "~0.7.0"
aws4 "^1.6.0"
caseless "~0.12.0"
combined-stream "~1.0.5"
extend "~3.0.1"
forever-agent "~0.6.1"
form-data "~2.3.1"
har-validator "~5.0.3"
hawk "~6.0.2"
http-signature "~1.2.0"
is-typedarray "~1.0.0"
isstream "~0.1.2"
json-stringify-safe "~5.0.1"
mime-types "~2.1.17"
oauth-sign "~0.8.2"
performance-now "^2.1.0"
qs "~6.5.1"
safe-buffer "^5.1.1"
stringstream "~0.0.5"
tough-cookie "~2.3.3"
tunnel-agent "^0.6.0"
uuid "^3.1.0"
restore-cursor@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"
dependencies:
onetime "^2.0.0"
signal-exit "^3.0.2"
safe-buffer@^5.0.1, safe-buffer@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
semver@^5.4.1:
version "5.4.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e"
signal-exit@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
sntp@2.x.x:
version "2.0.2"
resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.0.2.tgz#5064110f0af85f7cfdb7d6b67a40028ce52b4b2b"
dependencies:
hoek "4.x.x"
sshpk@^1.7.0:
version "1.13.1"
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3"
dependencies:
asn1 "~0.2.3"
assert-plus "^1.0.0"
dashdash "^1.12.0"
getpass "^0.1.1"
optionalDependencies:
bcrypt-pbkdf "^1.0.0"
ecc-jsbn "~0.1.1"
jsbn "~0.1.0"
tweetnacl "~0.14.0"
string-width@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
dependencies:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^4.0.0"
stringstream@~0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
strip-ansi@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
dependencies:
ansi-regex "^3.0.0"
supports-color@^4.0.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e"
dependencies:
has-flag "^2.0.0"
table-layout@^0.4.1:
version "0.4.2"
resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-0.4.2.tgz#10e9043c142a1e2d155da7257e478f0ef4981786"
dependencies:
array-back "^2.0.0"
deep-extend "~0.5.0"
lodash.padend "^4.6.1"
typical "^2.6.1"
wordwrapjs "^3.0.0"
test-value@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/test-value/-/test-value-2.1.0.tgz#11da6ff670f3471a73b625ca4f3fdcf7bb748291"
dependencies:
array-back "^1.0.3"
typical "^2.6.0"
tough-cookie@~2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561"
dependencies:
punycode "^1.4.1"
tunnel-agent@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
dependencies:
safe-buffer "^5.0.1"
tweetnacl@^0.14.3, tweetnacl@~0.14.0:
version "0.14.5"
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
typical@^2.6.0, typical@^2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/typical/-/typical-2.6.1.tgz#5c080e5d661cbbe38259d2e70a3c7253e873881d"
universalify@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.1.tgz#fa71badd4437af4c148841e3b3b165f9e9e590b7"
uuid@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04"
verror@1.10.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
dependencies:
assert-plus "^1.0.0"
core-util-is "1.0.2"
extsprintf "^1.2.0"
which@^1.2.9:
version "1.3.0"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a"
dependencies:
isexe "^2.0.0"
wordwrapjs@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-3.0.0.tgz#c94c372894cadc6feb1a66bff64e1d9af92c5d1e"
dependencies:
reduce-flatten "^1.0.1"
typical "^2.6.1"
wrap-ansi@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-3.0.1.tgz#288a04d87eda5c286e060dfe8f135ce8d007f8ba"
dependencies:
string-width "^2.1.1"
strip-ansi "^4.0.0"
yallist@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"