tagy
Version:
Create a new git tag by following the 'Semantic Versioning' and push it on remote.
366 lines (292 loc) • 13.7 kB
JavaScript
const fs = require('fs-extra')
const path = require('path')
const shell = require("shelljs")
const semver = require('semver')
const args = require('yargs').argv
const prompts = require('prompts')
const chalk = require("chalk");
const replace = require("replace-in-file");
const packageVersionBump = async (vv) => {
const pkgPath = path.join(process.cwd(), 'package.json')
if (fs.existsSync(pkgPath)) {
let pkgContent;
try {
pkgContent = await fs.readJSON(pkgPath);
} catch (err) {
throw new Error(`Couldn't parse "package.json"`)
}
pkgContent.version = vv;
try {
await fs.writeJSON(pkgPath, pkgContent, {
spaces: 2
});
} catch (err) {
throw new Error(`Couldn't write to "package.json"`)
}
return true;
}
return true;
}
module.exports = function () {
(async () => {
const totalArguments = Object.values(args).length;
if (totalArguments > 5) {
await console.log(chalk.red.bold('Too many arguments!'))
return;
}
const haveOption = (
args.p ||
args.m ||
args.patch ||
args.minor ||
args.major ||
args.reverse ||
args.custom ||
args.info
)
if (!haveOption || args.h) {
await console.log(chalk.red.bold(`'Please specify the increment type') [-p, -m, --minor, --patch, --major, --reverse, --custom, --info, --soft]'
Options:
-p, --patch # Will increase the version from 1.0.0 to 1.0.1
-m, --minor # Will increase the version from 1.0.0 to 1.1.0
--major # Will increase the version from 1.0.0 to 2.0.0
--reverse # Will remove the last tag and revert to previously created one.
--info # Get some info about current project.
--custom # Define the new Semantic version manually.
--soft # Create a soft tag. This will not commit the changes to git or create a new git tag.
-h, --help # Show this message.
`));
await console.log(chalk.blue('Example: ') + chalk.yellow('tagy --patch'))
return;
}
if (args.m && args.a) {
await console.log(chalk.red.bold('Did you mean `--major`? Try again!'))
return;
}
if (args.r && args.e) {
await console.log(chalk.red.bold('Did you mean `--reverse`? Try again!'))
return;
}
// abort if multiple args are passed from this list [p, m, major, minor, patch]
// if in array then abort
const multipleArgs = ['p', 'm', 'major', 'minor', 'patch', 'reverse', 'custom', 'info'];
const multipleArgsPassed = multipleArgs.filter(arg => args[arg]);
if (multipleArgsPassed.length > 1) {
await console.log(chalk.red.bold('Too many arguments!'))
return;
}
// read package.json
const pkgPath = path.join(process.cwd(), 'package.json')
if (!fs.existsSync(pkgPath)) {
await console.log(chalk.red.bold('package.json not found. Make sure that you are in the right directory!'))
return;
}
// Get package.json configuration
let pkgContent;
try {
pkgContent = await fs.readJSON(pkgPath);
} catch (err) {
throw new Error(`Couldn't parse "package.json"`)
}
const tagPrefix = (pkgContent && pkgContent.tagy && pkgContent.tagy.tagPrefix) || '';
let vv;
// This will soft create a tag
// ----------------------------------------------------------------------------
const isSoft = args.soft || (pkgContent && pkgContent.tagy && (pkgContent.tagy.soft || pkgContent.tagy.method === 'soft'));
if (pkgContent && pkgContent.tagy && pkgContent.tagy.method === 'soft'){
console.log(chalk.red(`{"method": "soft"} is deprecated, please use {"soft": true} instead.`))
}
if (isSoft) {
console.log(chalk.green('➡️ Soft Processing!'));
}
let branchName;
// This will create the tag in git
// ----------------------------------------------------------------------------
try {
if (!isSoft) {
let currentBranchName = shell.exec("git branch | grep \\* | cut -d ' ' -f2", {silent: true}).stdout;
if (!currentBranchName) {
return console.log(chalk.red.bold('Can\'t determine the branch name!'))
}
branchName = currentBranchName.trim();
if (!(branchName === 'master' || branchName === 'main')) {
// await console.log(chalk.red.bold('You can create tags only from "master" branch.'))
// return;
const confirmBranch = await prompts({
type: 'confirm',
name: 'value',
message: `Current branch name is '${branchName}'. Do you want to tag this branch?`,
initial: false
});
if (!confirmBranch.value) {
return console.log(chalk.red.bold('Aborted! Please switch the branch.'))
}
}
shell.exec('git fetch --tags', {silent: true});
// vv = shell.exec('git tag --sort=v:refname | grep -E \'^[0-9]\' | tail -1', {silent: true}).stdout;
vv = shell.exec(`git tag --sort=v:refname | grep -E '^${tagPrefix}[0-9]' | tail -1`, {silent: true}).stdout;
if (args.info) {
if (!vv) {
await console.log(chalk.blue(`Looks like no tags were created by this moment.`))
} else {
await console.log(chalk.blue(`Last created tag is: ${vv}`))
}
return;
}
} else {
vv = pkgContent.version;
}
if (args.reverse) {
if (isSoft) {
return console.log(chalk.red(`Can't perform a reverse when the method is soft. Please reverse it manually.`))
}
if (!vv) {
await console.log(chalk.blue(`Looks like no tags were created by this moment. Nothing to delete.`))
} else {
const confirmReverse = await prompts({
type: 'confirm',
name: 'value',
message: `Are you sure that you want to remove this tag?(${vv.trim()})`,
initial: false
})
if (!confirmReverse.value) {
return console.log(chalk.red.bold('Aborted!'))
}
shell.exec(`git tag -d ${vv}`);
shell.exec(`git push origin :refs/tags/${vv}`);
await console.log(chalk.blue(`Tag ${vv} --> deleted!.`))
}
return;
}
let currentTag;
if (args.custom) {
const customVer = await prompts({
type: 'text',
name: 'value',
message: 'Please enter a custom version. Make sure to be valid according to semver.org standard.',
initial: false,
// validate: val => {
// if (!/\d+\.\d+\.\d+/g.test(val)) {
// return 'Invalid version!';
// }
//
// return true;
// }
validate: val => {
if (!new RegExp(`^\\d+\\.\\d+\\.\\d+$`).test(val)) {
return 'Invalid version!';
}
return true;
}
})
if (!customVer.value) {
return console.log(chalk.red.bold('Aborted!'))
}
vv = customVer.value;
currentTag = vv;
} else {
if (!vv) {
vv = '0.0.0';
}
vv = vv.trim();
if (semver.ltr(vv, '0.0.0')) {
vv = '0.0.0'
}
currentTag = vv;
if (args.p || args.patch) {
vv = semver.inc(vv, 'patch')
} else if (args.m || args.minor) {
vv = semver.inc(vv, 'minor')
} else if (args.major) {
vv = semver.inc(vv, 'major')
} else {
console.log(chalk.red(`Something went wrong!.`))
return;
}
if (args.major) {
const confirmMajorRelease = await prompts({
type: 'confirm',
name: 'value',
message: `Are you sure that you want to create a major release? Current tag is "${currentTag}" and the next will be "${vv}"`,
initial: false
})
if (!confirmMajorRelease.value) {
return console.log(chalk.red.bold('Aborted!'))
}
}
}
let canCreate
// Bump the version in package.json
try {
canCreate = await packageVersionBump(vv);
} catch (err) {
return console.log(err.message);
}
// Check for custom config inside of current directory.
const tagyExtraFile = path.resolve(`${process.cwd()}/tagy.js`);
try {
if (fs.existsSync(tagyExtraFile)) {
console.log('"tagy.js" file is found!');
const tagyExtra = require(tagyExtraFile)
await tagyExtra(vv, currentTag, args)
}
} catch (err) {
console.error(err)
}
// In some configurations, the replacements can be defined in the package.json.
const replacementMethods = pkgContent && pkgContent.tagy && pkgContent.tagy.replace && Array.isArray(pkgContent.tagy.replace) ? pkgContent.tagy.replace : [];
if (replacementMethods.length > 0) {
console.log(chalk.green('♾️ Replacement methods are found!'));
for (let i = 0; i < replacementMethods.length; i++) {
const {files, from, to, flags} = replacementMethods[i];
if (files && from && to) {
const _files = Array.isArray(files) ? files : [files];
const replaceConf = {
files: _files.map(file => path.resolve(`${process.cwd()}/${file}`)),
from: new RegExp(from.replaceAll('__CURRENT_TAG__', currentTag).replaceAll('__VERSION__', vv), flags !== false ? (flags || 'g') : undefined),
to: to.replaceAll('__CURRENT_TAG__', currentTag).replaceAll('__VERSION__', vv),
};
replace.sync(replaceConf);
}
}
}
if (canCreate) {
if (!isSoft) {
await shell.exec(`git config --global core.autocrlf true`);// Replace CRLF with LF on Windows OS.
await shell.exec(`git config --global core.safecrlf false`);// Disable CRLF warnings.
// await shell.exec(`git commit -a -m "Release ${vv}"`);
// await shell.exec(`git push origin ${branchName}`);
// await shell.exec(`git tag ${vv}`);
// await shell.exec(`git push origin ${vv}`);
await shell.exec(`git commit -a -m "Release ${tagPrefix}${vv}"`);
await shell.exec(`git push origin ${branchName}`);
await shell.exec(`git tag ${tagPrefix}${vv}`);
await shell.exec(`git push origin ${tagPrefix}${vv}`);
// Check if github CLI is installed and create a release
const ghInstalled = shell.exec(`gh --version`, {silent: true}).stdout;
if (ghInstalled) {
const autoRelease = args['auto-release'] || (pkgContent && pkgContent.tagy && pkgContent.tagy['auto-release']);
let releaseIt = autoRelease;
if (!autoRelease) {
const ghRelease = await prompts({
type: 'confirm',
name: 'value',
message: `Do you want to create a release on GitHub?`,
initial: false
});
releaseIt = ghRelease.value;
}
if (releaseIt) {
await shell.exec(`gh release create ${tagPrefix}${vv} --title "${tagPrefix}${vv}" --notes "Release ${tagPrefix}${vv}"`)
}
}
}
await console.log(chalk.blue(`💥 Tag ${vv} --> created!.`))
}
} catch (e) {
console.log(e)
}
})()
}