Created
January 28, 2021 09:38
-
-
Save claudioc/77eb0c06c4c415bb1273d5499aaeccb1 to your computer and use it in GitHub Desktop.
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
#!/usr/bin/env node | |
// This is a script "recipe" for releasing | |
// It has been inspired by yeoman generator-release | |
// The reason for this to exist is that I didn't want to include an external dependency (yeoman) for a 'task', | |
// when all the tasks of the project are managed by gulp | |
// | |
// What this recipe does: | |
// - Runs sanity check to see if we can release | |
// - are we in the `releaseEvents` branch? | |
// - is the working directory git-clean? | |
// - are we behind some commits, after fetching? | |
// - Calculates the next version, either from the command line (using the --magnolia:version or --magnolia:incr switches) | |
// or automatically getting the next `patch` number | |
// - Verifies that tne new version makes sense (semver.valid()) | |
// - Finds the range of commits for the this release (using two methods) and parses each of them | |
// to understand if they belong to a PR or not | |
// - Shows a preview of the changes (list of PR and commits) and asks for confirmation | |
// - Creates or updates `release-notes.md` with the list of changes | |
// - Updates package.json with the new version | |
// - git add and commits the changes (release-notes.md and packages.json) | |
// - git push to origin | |
// - git tag (annotated) with the version and pushes | |
// | |
// The difference with generator-release is that this recipe doesn't need to interact | |
// with the github API, which means that it's easier to use but also that (for the moment) | |
// we are not automatically creating a release (but Github will have it anyway) and the | |
// release-notes are less descriptive | |
const semver = require('semver'); | |
const minimist = require('minimist'); | |
const inq = require('inquirer'); | |
const dateFormat = require('dateformat'); | |
const exec = require('child_process').exec; | |
const _ = require('lodash'); | |
const fs = require('fs'); | |
const path = require('path'); | |
let tagName; | |
let remoteRepo = ''; | |
let newVersion; | |
let currentVersion; | |
const sprintf = require('util').format; | |
const pulls = []; | |
const commits = []; | |
const packageJson = require('../package.json'); | |
testGitBranch() | |
.then(testGitWorkingDirectory) | |
.then(gitFetch) | |
.then(testGitCommits) | |
.then(increaseVersionNumber) | |
.then(gatherRepoInfo) | |
.then(gatherCommits) | |
.then(previewChanges) | |
.then(updateReleaseNotes) | |
.then(commitRelease) | |
.then(tagRelease) | |
// .then(pushDockerImage) | |
.catch(e => { | |
console.log(e); | |
}); | |
function gitExec(command) { | |
return new Promise((resolve, reject) => { | |
exec(`git ${command}`, (err, data) => { | |
if (err) { | |
reject(`Error running git ${command}`); | |
return; | |
} | |
resolve(data); | |
}); | |
}); | |
} | |
function testGitBranch() { | |
return gitExec('rev-parse --abbrev-ref HEAD').then(branch => { | |
if (branch.trim() !== 'release' && branch.trim() !== 'release-script') { | |
console.warn('You have to run `release` from the `release` branch.'); | |
process.exit(); | |
} | |
}); | |
} | |
function testGitWorkingDirectory() { | |
return gitExec('diff-index --name-only HEAD --').then(data => { | |
if (data.trim() !== '') { | |
// throw 'Git working directory not clean.'; | |
} | |
}); | |
} | |
function gitFetch() { | |
return gitExec('fetch'); | |
} | |
function testGitCommits() { | |
return gitExec('branch -v --no-color | grep -e "^\\*"', data => { | |
if (/\[behind (.*)\]/.test(data)) { | |
throw `Your repo is behind by ${RegExp.$1} commits.`; | |
} | |
}); | |
} | |
function increaseVersionNumber() { | |
const options = minimist(process.argv.slice(2)); | |
// Get the current version | |
let incr; | |
currentVersion = packageJson.version; | |
// Calculate the new version | |
// Command line param can specify: | |
// --version <new version> | |
// --incr <major, minor, patch> | |
// no params: increases patch version | |
if (options['version']) { | |
newVersion = options['version']; | |
if (!semver.valid(newVersion)) { | |
throw 'Requested version is not syntactically correct. It should be in the form <major>.<minor>.<patch>.'; | |
} | |
} | |
if (options['incr']) { | |
incr = options['incr']; | |
if (incr === true || ['major', 'minor', 'patch'].indexOf(incr) === -1) { | |
incr = 'patch'; | |
} | |
newVersion = semver.inc(currentVersion, incr); | |
} | |
if (!newVersion) { | |
newVersion = semver.inc(currentVersion, 'patch'); | |
} | |
if (semver.lt(newVersion, currentVersion)) { | |
throw 'The new version must be greater than the old one.'; | |
} | |
tagName = `v${newVersion}`; | |
} | |
function gatherRepoInfo() { | |
return gitExec('remote show origin').then(data => { | |
/Fetch URL: .*:(.*)\.git/.test(data); | |
remoteRepo = RegExp.$1; | |
}); | |
} | |
function gatherCommits() { | |
return gitExec('log --no-color --oneline HEAD...origin/release').then(data => { | |
_.compact( | |
_.map(data.trim().split('\n'), line => { | |
const info = /(\S+) (.*)/.exec(line); | |
let url; | |
let pr; | |
if (!info) { | |
return; | |
} | |
if (/^Merge branch/.test(info[2])) { | |
return; | |
} | |
if (/Merge pull request #(\d+) from/.test(info[2])) { | |
pr = RegExp.$1; | |
url = sprintf('https://github.com/%s/pull/%s', remoteRepo, pr); | |
} | |
return { | |
sha: info[1], | |
title: info[2], | |
pr, | |
url | |
}; | |
}) | |
).forEach(commit => { | |
(commit.pr ? pulls : commits).push(commit); | |
}); | |
}); | |
} | |
function previewChanges() { | |
const executor = (resolve, reject) => { | |
console.log('\nReleasing %s', newVersion); | |
if (pulls.length > 0) { | |
console.log('\nList of included Pull Requests:'); | |
console.log('———————————————————————————————'); | |
pulls.forEach(pull => { | |
console.log('✓ %s [ %s ]', pull.title, pull.url); | |
}); | |
} | |
console.log('\nList of included Commits:'); | |
console.log('—————————————————————————'); | |
commits.forEach(commit => { | |
console.log('✓ %s', commit.title); | |
}); | |
console.log(''); | |
// Ask the user if it's OK to continue | |
inq | |
.prompt([ | |
{ | |
type: 'confirm', | |
name: 'confirm', | |
message: `About to release new version ${newVersion} upgrading from ${currentVersion}. Is it OK?`, | |
default: false | |
} | |
]) | |
.then(response => { | |
if (!response.confirm) { | |
console.info('Aborted per user request. Bye.'); | |
process.exit(); | |
} | |
resolve(); | |
}); | |
}; | |
return new Promise(executor); | |
} | |
function updateReleaseNotes() { | |
let notes = fs.readFileSync('release-notes.md').toString(); | |
const notesContent = []; | |
notesContent.push( | |
sprintf('## Release %s — %s\n', newVersion, dateFormat(new Date(), 'mmmm dS, yyyy')) | |
); | |
notesContent.push('#### Pull Requests'); | |
pulls.forEach(pull => { | |
notesContent.push(sprintf('- [%s](%s) – %s', pull.pr, pull.url, pull.title)); | |
}); | |
notesContent.push('\n#### Commits'); | |
commits.forEach(commit => { | |
notesContent.push(sprintf('- %s', commit.title)); | |
}); | |
notesContent.push( | |
sprintf( | |
'\n[Compare to previous release](https://github.com/%s/compare/v%s...%s)\n', | |
remoteRepo, | |
currentVersion, | |
tagName | |
) | |
); | |
notes = notes.replace(/# Latest updates/, '# Latest updates\n\n' + notesContent.join('\n')); | |
fs.writeFileSync('release-notes.md', notes); | |
packageJson.version = newVersion; | |
fs.writeFileSync( | |
path.resolve(process.cwd(), './package.json'), | |
JSON.stringify(packageJson, undefined, 2) + '\n' | |
); | |
return gitExec('add release-notes.md package.json'); | |
} | |
function commitRelease() { | |
return gitExec(`commit -m "Release ${newVersion}"`).then( | |
gitExec.bind(null, 'push origin release') | |
); | |
} | |
function pushDockerImage() { | |
return new Promise((resolve, reject) => { | |
exec('./docker.sh release', (err, data) => { | |
if (err) { | |
console.log(err); | |
reject(`Error running ./docker.sh release`); | |
return; | |
} | |
resolve(data); | |
}); | |
}); | |
} | |
function tagRelease() { | |
return gitExec(`tag -a ${tagName} -m "Release ${newVersion}"`).then( | |
gitExec.bind(null, `push origin ${tagName}`) | |
); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment