-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cli): add support for minor releases (#3686)
- Loading branch information
Showing
2 changed files
with
218 additions
and
79 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,10 +8,10 @@ | |
'use strict'; | ||
|
||
const parse = require('@commitlint/parse'); | ||
const chalk = require('chalk'); | ||
const execa = require('execa'); | ||
const { prompt } = require('inquirer'); | ||
const semver = require('semver'); | ||
const { createLogger, displayBanner } = require('../logger'); | ||
|
||
// All supported commit types from our conventional-changelog preset | ||
const types = [ | ||
|
@@ -30,9 +30,12 @@ const types = [ | |
|
||
// Filter supported commit types per release bump | ||
const typesByReleaseBump = { | ||
minor: types, | ||
patch: types.filter(type => type !== 'feat'), | ||
}; | ||
|
||
const logger = createLogger('release'); | ||
|
||
/** | ||
* Create a branch with the commits necessary to generate a release for the | ||
* given release bump. This command will execute the commands a developer may | ||
|
@@ -47,7 +50,6 @@ const typesByReleaseBump = { | |
async function release({ bump }) { | ||
displayBanner(); | ||
|
||
const logger = createLogger(); | ||
logger.start('Getting latest tag'); | ||
|
||
// Make sure we've fetched the latest tags from upstream | ||
|
@@ -73,15 +75,98 @@ async function release({ bump }) { | |
logger.stop(); | ||
|
||
const branchName = `chore/release-${nextTag}`; | ||
logger.start(`Creating branch: ${branchName} from tag ${latestTag}`); | ||
|
||
await execa('git', ['checkout', latestTag]); | ||
logger.start(`Creating branch: ${branchName}`); | ||
|
||
if (bump === 'patch') { | ||
logger.info(`Using tag ${latestTag} as base`); | ||
// If we're bumping for a patch release, we'll need to base our release off | ||
// of the previous known tag | ||
await execa('git', ['checkout', latestTag]); | ||
} else { | ||
logger.info(`Using master branch as base`); | ||
// If we're publishing other releases, we'll need to base our release off of | ||
// the latest stable `master` branch | ||
await fetchLatestFromUpstream(); | ||
} | ||
await execa('git', ['checkout', '-b', branchName]); | ||
|
||
logger.stop(); | ||
|
||
const commitRange = `${latestTag}...master`; | ||
if (bump === 'patch') { | ||
const commitRange = `${latestTag}...master`; | ||
await cherryPickCommitsFrom(commitRange, bump); | ||
} | ||
|
||
// After making sure our base branch is up-to-date, let's go ahead and reset | ||
// our project and rebuild everything from a known state. This is helpful for | ||
// getting rid of any local inconsistencies. Ultimately this process | ||
// replicates what we do in our Continuous Integration checks. | ||
await resetProjectState(); | ||
|
||
// Just in case there are any freshly generated files after running the steps | ||
// above, we'll check to see if the local branch is dirty before proceeding | ||
await checkIfBranchIsDirty(); | ||
|
||
// Call out to lerna to handle versioning changed packages | ||
await execa( | ||
'yarn', | ||
['lerna', 'version', bump, '--no-push', '--no-git-tag-version', '--exact'], | ||
{ | ||
stdio: 'inherit', | ||
} | ||
); | ||
|
||
logger.start('Creating final commit'); | ||
logger.info( | ||
'The next step will be to manually create a Pull Request for this branch' | ||
); | ||
|
||
const versionCommitMessage = 'chore(release): update package versions'; | ||
await execa('git', ['add', '-A']); | ||
await execa('git', ['commit', '-m', versionCommitMessage]); | ||
|
||
logger.stop(); | ||
} | ||
|
||
/** | ||
* For certain release types, we want to be certain that our base branch is | ||
* up-to-date with the upstream remote. This helper will first check that the | ||
* upstream remote exists, and create it if it does not, and then will pull the | ||
* latest changes into the local project. | ||
* | ||
* @returns {void} | ||
*/ | ||
async function fetchLatestFromUpstream() { | ||
try { | ||
// This command will fail is no upstream is present, with `catch` we can | ||
// create the appropriate remote before running the next commands | ||
await execa('git', ['remote', 'get-url', 'upstream']); | ||
} catch { | ||
await execa('git', [ | ||
'remote', | ||
'add', | ||
'upstream', | ||
'[email protected]:carbon-design-system/carbon.git', | ||
]); | ||
} | ||
await execa('git', ['checkout', 'master']); | ||
await execa('git', ['pull', 'upstream', 'master']); | ||
} | ||
|
||
/** | ||
* When working with patch releases, we'll want to cherry pick commits that are | ||
* found in the commit range between two tags. This helper also considers the | ||
* version bump and the types of commits found. Depending on the bump certain | ||
* commit types will be included. If an appropriate commit type is not found, | ||
* we'll prompt the user for whether or not to include it. If a merge conflict | ||
* occurs, we'll prompt the user to address it before proceeding. | ||
* | ||
* @param {string} commitRange - the two tags we'll want to grab commits from. | ||
* The format should follow `tagA...tagB`, where tagA is older than tagB. | ||
* @param {string} bump - the version bump | ||
* @returns {void} | ||
*/ | ||
async function cherryPickCommitsFrom(commitRange, bump) { | ||
logger.start(`Getting commits to cherry-pick from ${commitRange}`); | ||
|
||
const { stdout: commitInfo } = await execa('git', [ | ||
|
@@ -167,94 +252,75 @@ async function release({ bump }) { | |
} | ||
|
||
logger.stop(); | ||
} | ||
|
||
await execa( | ||
'yarn', | ||
['lerna', 'version', bump, '--no-push', '--no-git-tag-version', '--exact'], | ||
{ | ||
stdio: 'inherit', | ||
} | ||
); | ||
/** | ||
* When working with multiple local environments, it's helpful to reset the | ||
* project to a known state. This helper will try and clean everything up so | ||
* that the environment is clean and good-to-go moving forward. Most of the | ||
* steps in this method ultimately reflect what we do in Continous Integration | ||
* environments, with the addition of a `clean` command to remove generated | ||
* artifacts locally. | ||
* | ||
* @returns {void} | ||
*/ | ||
async function resetProjectState() { | ||
logger.start('Resetting the project to a known state'); | ||
|
||
logger.start('Creating final commit'); | ||
logger.info( | ||
'The next step will be to manually create a Pull Request for this branch' | ||
); | ||
logger.info('Cleaning any local artifacts or node_modules'); | ||
// Make sure that our tooling is defined before running clean | ||
await execa('yarn', ['install', '--offline']); | ||
await execa('yarn', ['clean']); | ||
|
||
const versionCommitMessage = 'chore(release): update package versions'; | ||
await execa('git', ['add', '-A']); | ||
await execa('git', ['commit', '-m', versionCommitMessage]); | ||
logger.info('Installing known dependencies from offline mirror'); | ||
await execa('yarn', ['install', '--offline']); | ||
|
||
logger.info('Building packages from source'); | ||
await execa('yarn', ['build']); | ||
|
||
logger.stop(); | ||
} | ||
|
||
/** | ||
* When working with generated files, sometimes we'll want to check if the | ||
* working branch is dirty and if the caller wants to commit these files as part | ||
* of the release process. | ||
* | ||
* @returns {void} | ||
*/ | ||
async function checkIfBranchIsDirty() { | ||
const { stdout } = await execa('git', ['status', '--porcelain']); | ||
if (stdout !== '') { | ||
const { confirmed } = await prompt([ | ||
{ | ||
type: 'confirm', | ||
name: 'confirmed', | ||
message: | ||
'The git status of the project is currently not clean. Would ' + | ||
'you like to commit these changes to the project?', | ||
}, | ||
]); | ||
|
||
if (confirmed) { | ||
await execa('git', ['add', '-A']); | ||
await execa('git', [ | ||
'commit', | ||
'-m', | ||
'chore(project): sync generated files [skip ci]', | ||
]); | ||
} | ||
} | ||
} | ||
|
||
module.exports = { | ||
command: 'release [bump]', | ||
desc: 'run the release step for the given version bump', | ||
builder(yargs) { | ||
yargs.positional('bump', { | ||
describe: 'choose a release version to bump', | ||
choices: ['patch'], | ||
choices: ['minor', 'patch'], | ||
default: 'patch', | ||
}); | ||
}, | ||
handler: release, | ||
}; | ||
|
||
/** | ||
* Create a logger to be used in a handler. This is typically just for | ||
* formatting the output, adding a prefix, and connecting the output with | ||
* box-drawing ASCII characters. | ||
* @returns {object} | ||
*/ | ||
function createLogger() { | ||
let start; | ||
|
||
/** | ||
* Display the given message with a box character. This also includes | ||
* formatting for the logger prefix and box character itself. | ||
* @param {string} boxCharacter | ||
* @param {string?} message | ||
* @returns {void} | ||
*/ | ||
function log(boxCharacter, message = '') { | ||
console.log(chalk`{yellow release ▐} {gray ${boxCharacter}} ${message}`); | ||
} | ||
|
||
return { | ||
info(message) { | ||
log('┣', chalk.gray(message)); | ||
}, | ||
start(message) { | ||
start = Date.now(); | ||
log('┏', message); | ||
}, | ||
stop(message) { | ||
const duration = ((Date.now() - start) / 1000).toFixed(2); | ||
if (message) { | ||
log('┗', message); | ||
} else { | ||
log('┗', chalk`{gray Done in {italic ${duration}s}}`); | ||
} | ||
}, | ||
newline() { | ||
log('┃'); | ||
}, | ||
}; | ||
} | ||
|
||
/** | ||
* Display the banner in the console, typically at the beginning of a handler | ||
* @returns {void} | ||
*/ | ||
function displayBanner() { | ||
console.log(` | ||
_ | ||
| | | ||
___ __ _ _ __| |__ ___ _ __ | ||
/ __/ _\` | '__| '_ \\ / _ \\| '_ \\ | ||
| (_| (_| | | | |_) | (_) | | | | | ||
\\___\\__,_|_| |_.__/ \\___/|_| |_| | ||
`); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
/** | ||
* Copyright IBM Corp. 2019, 2019 | ||
* | ||
* This source code is licensed under the Apache-2.0 license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
*/ | ||
|
||
'use strict'; | ||
|
||
const chalk = require('chalk'); | ||
|
||
/** | ||
* Create a logger to be used in a handler. This is typically just for | ||
* formatting the output, adding a prefix, and connecting the output with | ||
* box-drawing ASCII characters. | ||
* @returns {object} | ||
*/ | ||
function createLogger(command) { | ||
let start; | ||
|
||
/** | ||
* Display the given message with a box character. This also includes | ||
* formatting for the logger prefix and box character itself. | ||
* @param {string} boxCharacter | ||
* @param {string?} message | ||
* @returns {void} | ||
*/ | ||
function log(boxCharacter, message = '') { | ||
console.log(chalk`{yellow ${command} ▐} {gray ${boxCharacter}} ${message}`); | ||
} | ||
|
||
return { | ||
info(message) { | ||
log('┣', chalk.gray(message)); | ||
}, | ||
start(message) { | ||
start = Date.now(); | ||
log('┏', message); | ||
}, | ||
stop(message) { | ||
const duration = ((Date.now() - start) / 1000).toFixed(2); | ||
if (message) { | ||
log('┗', message); | ||
} else { | ||
log('┗', chalk`{gray Done in {italic ${duration}s}}`); | ||
} | ||
}, | ||
newline() { | ||
log('┃'); | ||
}, | ||
}; | ||
} | ||
|
||
/** | ||
* Display the banner in the console, typically at the beginning of a handler | ||
* @returns {void} | ||
*/ | ||
function displayBanner() { | ||
console.log(` | ||
_ | ||
| | | ||
___ __ _ _ __| |__ ___ _ __ | ||
/ __/ _\` | '__| '_ \\ / _ \\| '_ \\ | ||
| (_| (_| | | | |_) | (_) | | | | | ||
\\___\\__,_|_| |_.__/ \\___/|_| |_| | ||
`); | ||
} | ||
|
||
module.exports = { | ||
createLogger, | ||
displayBanner, | ||
}; |