Skip to content

Instantly share code, notes, and snippets.

@husa
Last active Jun 24, 2022
Embed
What would you like to do?
Increase version number based on tags

Version Bump Script

Script finds latest ancestor tag in git tree and outputs to stdout next increased tag.

Defaults

Prop Default Possible Values Description Example
part "major" "major" | "minor" Which part of version to increase --major, --minor
prefix "v" String prefix of the tag(before version numbers) --prefix=v, --prefix=pod5-v

Examples

Using defaults

# latest tag is v1.5
node bumpVersion.js
v2.0

Increase major/minor

# latest tag is v1.5
node bumpVersion.js --major
v2.0

node bumpVersion.js --minor
v1.6

Custom prefixes

# latest tag is version1.5
node bumpVersion.js --major --prefix=version
v2.0
# latest tag is pod1-v1.5
node bumpVersion.js --prefix=pod1-v
pod1-v2.0
const child_process = require("child_process");
const exec = (command) => {
return new Promise((resolve, reject) => {
child_process.exec(command, (error, stdout, stderr) => {
if (error) return reject(error);
resolve({ stdout, stderr });
});
});
};
const DEFAULT_TAG_PREFIX = "v";
const getDefaultTag = (prefix) => `${prefix}0.0`;
// const tagRegex = new RegExp(`^${TAG_PREFIX}\\d+\.\\d+$`);
const getLatestAncestorTag = async (tagPrefix) => {
const { stdout } = await exec(
`git describe --tags --abbrev=0 --match "${tagPrefix}[0-9]*.[0-9]*" --first-parent`
);
return stdout.trim();
};
const getHeadCommitSha = async () => {
const { stdout } = await exec("git rev-parse HEAD");
return stdout;
};
const getTagCommitSha = async (tag) => {
const { stdout } = await exec(`git rev-list -1 ${tag}`);
return stdout;
};
const increaseVersion = ([major, minor], part) => {
const newMajor = part === "major" ? parseInt(major, 10) + 1 : major;
const newMinor = part === "minor" ? parseInt(minor, 10) + 1 : minor;
return [newMajor, newMinor];
};
const getNextTag = (latestTag, part, prefix) => {
if (!latestTag) latestTag = getDefaultTag(prefix);
const [major, minor] = latestTag.replace(prefix, "").split(".");
const [newMajor, newMinor] = increaseVersion([major, minor], part);
return `${prefix}${newMajor}.${newMinor}`;
};
const bumpVersion = async (part, prefix) => {
let latestTag;
const defaultTag = getDefaultTag(prefix);
try {
latestTag = await getLatestAncestorTag(prefix);
} catch (err) {
latestTag = defaultTag;
}
const headCommitSha = await getHeadCommitSha();
// if current commit(HEAD) already has a version tag, skip
if (latestTag !== defaultTag) {
const latestTagCommitSha = await getTagCommitSha(latestTag);
if (latestTagCommitSha === headCommitSha) {
const error = new Error("ExistingTagPointsToHEAD");
error.tag = latestTag;
throw error;
}
}
const nextTag = getNextTag(latestTag, part, prefix);
return nextTag;
};
module.exports = {
bumpVersion,
};
(async () => {
if (require.main === module) {
let versionPart;
if (process.argv.includes("--minor")) {
versionPart = "minor";
} else {
versionPart = "major";
}
let prefix = DEFAULT_TAG_PREFIX;
const prefixArg = process.argv.find((arg) => {
return !!arg.match(/^--prefix=.*$/);
});
if (prefixArg) {
prefix = prefixArg.replace("--prefix=", "");
}
try {
const nextVersion = await bumpVersion(versionPart, prefix);
console.log(nextVersion);
process.exit(0);
} catch (err) {
if (err.message === "ExistingTagPointsToHEAD") {
console.error(
`Tag "${err.tag}" already points to HEAD. No need to create new one.`
);
process.exit(1);
}
console.error(err);
process.exit(1);
}
}
})();
const child_process = require("child_process");
const { bumpVersion } = require("./bumpVersion");
jest.mock("child_process", () => {
let execMockResults = {};
return {
exec: (arg, cb) => {
for (let [prefix, result] of Object.entries(execMockResults)) {
if (arg.startsWith(prefix)) {
if (result instanceof Error) return cb(result, null, null);
return cb(null, result, null);
}
}
return "mock";
},
__setExecResults(map) {
execMockResults = map;
},
};
});
describe("bumpVersion", () => {
beforeEach(() => {
child_process.__setExecResults({
"git describe --tags --abbrev=0": "v2.0",
"git rev-parse HEAD": "sha1",
"git rev-list -1": "sha2",
});
});
it("should increase major version", async () => {
const nextVersion = await bumpVersion("major", "v");
expect(nextVersion).toBe("v3.0");
});
it("should increase minor version", async () => {
const nextVersion = await bumpVersion("minor", "v");
expect(nextVersion).toBe("v2.1");
});
it("should accept custom prefix", async () => {
child_process.__setExecResults({
"git describe --tags --abbrev=0": "test2.0",
"git rev-parse HEAD": "sha1",
"git rev-list -1": "sha2",
});
let nextVersion = await bumpVersion("major", "test");
expect(nextVersion).toBe("test3.0");
nextVersion = await bumpVersion("minor", "test");
expect(nextVersion).toBe("test2.1");
});
it("should default to v1.0 if no tag present", async () => {
child_process.__setExecResults({
"git describe --tags --abbrev=0": new Error("no tag"),
"git rev-parse HEAD": "sha1",
"git rev-list -1": "sha2",
});
expect(await bumpVersion("major", "v")).toBe("v1.0");
});
it("should default to $prefix1.0 if no tag present and prefix passed", async () => {
child_process.__setExecResults({
"git describe --tags --abbrev=0": new Error("no tag"),
"git rev-parse HEAD": "sha1",
"git rev-list -1": "sha2",
});
expect(await bumpVersion("major", "test")).toBe("test1.0");
expect(await bumpVersion("minor", "test")).toBe("test0.1");
});
it("should throw if HEAD already has a version tag", () => {
child_process.__setExecResults({
"git describe --tags --abbrev=0": "v2.0",
"git rev-parse HEAD": "sha1",
"git rev-list -1": "sha1",
});
expect(bumpVersion("major", "v")).rejects.toThrow(
"ExistingTagPointsToHEAD"
);
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment