yankee
Version:
Easy release management with YAML changelogs
180 lines (152 loc) • 5.13 kB
JavaScript
; // eslint-disable-line strict
// http://stackoverflow.com/q/33063206
const fs = require('fs');
const yaml = require('js-yaml');
const tinyError = require('tiny-error');
const isObject = require('isobject');
const omit = require('object.omit');
const includes = require('array-includes');
const dateFormat = require('date-format');
const spawnSync = require('child_process').spawnSync;
// (String) => Error
const prettyError = (message) => (
tinyError(`Oops! Things went wrong. ${message}`)
);
// (
// previousVersion: String,
// bump: 'initial' | 'breaking' | 'feature' | 'bugfix'
// ) => String
const nextVersion = (previousVersion, bump) => {
if (bump === 'initial') {
return '1.0.0';
}
const versionNumbers = previousVersion.split('.').map(Number);
return (
(bump === 'breaking' &&
`${versionNumbers[0] + 1}.0.0`
) ||
(bump === 'feature' &&
`${versionNumbers[0]}.${versionNumbers[1] + 1}.0`
) ||
`${versionNumbers[0]}.${versionNumbers[1]}.${versionNumbers[2] + 1}`
);
};
/* (see git.io/rtype)
({
path = process.cwd(): String,
// Path to your project directory
date = new Date(): Date,
// Date of the release (will appear in the changelog)
stream = process.stdout: WritableStream,
// Stream to write messages to
npm = false: Boolean,
// If `true`, we’ll update the `version` in the `package.json`,
// `package-lock.json` and `npm-shrinkwrap.json` (whichever are present)
commit = false: Boolean,
// If `true`, we’ll commit the results with git
tag = false: Boolean,
// If `true`, we’ll tag the results with git. Implies `commit`
}) => {
bump: 'breaking' | 'feature' | 'bugfix' | 'initial',
previousVersion: String,
newVersion: String,
}
*/
module.exports = (paramsArg) => {
const params = paramsArg || {};
const path = params.path || process.cwd();
const date = params.date || new Date();
const npm = params.npm || false;
const commit = params.commit || params.tag || false;
const tag = params.tag || false;
const changelogPath = `${path}/Changelog.yaml`;
const changelog = yaml.safeLoad(fs.readFileSync(changelogPath, 'utf8'));
if (!isObject(changelog)) {
throw prettyError('Make sure `Changelog.yaml` is a YAML object.');
}
if (!changelog.hasOwnProperty('master')) {
throw prettyError(
'Make sure you have a top-level `master:` property in your ' +
'`Changelog.yaml`.'
);
}
const changelogWithoutMaster = omit(changelog, 'master');
const masterData = changelog.master;
const masterDataKeys = Object.keys(masterData);
const previousVersion = Object.keys(changelogWithoutMaster)[0];
const bump = (
(!previousVersion && 'initial') ||
(includes(masterDataKeys, 'breaking changes') && 'breaking') ||
(includes(masterDataKeys, 'new features') && 'feature') ||
'bugfix'
);
const newVersion = nextVersion(previousVersion, bump);
const newVersionData = Object.assign({
date: dateFormat('yyyy-MM-dd', date),
}, masterData);
const newChangelog = Object.assign({
[newVersion]: newVersionData,
}, changelogWithoutMaster);
const newChangelogString = yaml.safeDump(newChangelog)
// Reformat dates
.replace(/(^\s*date:\s*)'(.*)'$/mg, '$1$2')
.replace(/(^\s*date:\s*)(\d{4}-\d{2}-\d{2})[tT][\d:\.+zZ]+$/mg, '$1$2')
// Add more air
.replace(/(.)\n([^\s])/g, '$1\n\n$2');
fs.writeFileSync(changelogPath, newChangelogString);
const tryUpdatingFile = (filename) => {
let fileContents;
try {
fileContents = fs.readFileSync(`${path}/${filename}`, 'utf8');
} catch (error) {
if (error.code === 'ENOENT') return null;
/* istanbul ignore next */
throw error;
}
let data;
try {
data = JSON.parse(fileContents);
} catch (error) {
if (error instanceof SyntaxError) throw prettyError(
`Make sure \`${filename}\` is valid JSON.`
);
}
if (typeof data !== 'object' || data === null) throw prettyError(
`Make sure \`${filename}\` is a JSON object.`
);
data.version = newVersion;
fs.writeFileSync(
`${path}/${filename}`,
`${JSON.stringify(data, null, ' ')}\n`
);
return filename;
};
const jsonFilesToUpdate = (npm
? ['package.json', 'npm-shrinkwrap.json', 'package-lock.json']
: []
);
const updatedFiles = jsonFilesToUpdate
.map(tryUpdatingFile)
.filter(filename => filename !== null);
if (commit) {
const args = [
'commit', `--message=${newVersion}`, 'Changelog.yaml',
].concat(
updatedFiles
);
process.stdout.write(`\n❭ git ${args.join(' ')}\n`);
spawnSync('git', args, { cwd: path, stdio: 'inherit' });
}
if (tag) {
const args = [
'tag', '--annotate', `--message=${newVersion}`, `v${newVersion}`,
];
process.stdout.write(`\n❭ git ${args.join(' ')}\n`);
spawnSync('git', args, { cwd: path, stdio: 'inherit' });
}
return {
bump,
previousVersion,
newVersion,
};
};