@cinnabar-forge/meta
Version:
Version updater
732 lines (713 loc) • 23.2 kB
JavaScript
// src/changelog.ts
import fs from "node:fs";
import path from "node:path";
import { promptText } from "clivo";
// src/git.ts
import { execSync } from "node:child_process";
function getGitLog(tagOrCommit) {
try {
const log = execSync(
`git log ${tagOrCommit ? `${tagOrCommit}..HEAD ` : ""}--pretty=format:'%H%n%B' --no-merges`
).toString();
return log.split("\n\n").map((line) => {
const [hash, ...message] = line.split("\n");
const messages = [];
for (const msg of message) {
if (msg.length > 0) {
messages.push(msg);
}
}
return { hash, message: messages.join("\n") };
});
} catch (error) {
console.error("Error fetching git log:", error);
return [];
}
}
function checkVersionExistsInGitTags(version) {
try {
const tags = execSync("git tag").toString();
const tagList = tags.split("\n");
return tagList.includes(`v${version}`);
} catch (error) {
console.error("Error checking version in git tags:", error);
return false;
}
}
function getMostRecentGitTag() {
try {
return execSync("git tag --sort=-creatordate | head -n 1").toString().trim();
} catch (error) {
console.error("Error fetching the most recent git tag:", error);
return "";
}
}
function commitChanges(version, push) {
try {
console.log("Adding changes to git...");
execSync("git add -A");
console.log(
`Committing changes with message: "release version ${version}"`
);
execSync(`git commit -m "release version ${version}"`);
console.log(`Creating tag: "v${version}"`);
execSync(`git tag "v${version}"`);
if (push) {
console.log("Pushing changes to origin...");
execSync("git push origin");
execSync("git push origin --tags");
}
console.log("Git operations completed successfully.");
return true;
} catch (error) {
console.error(`Error committing changes to git repository: ${error}`);
throw new Error(`Error committing changes to git repository: ${error}`);
}
}
function checkGithubRepo(text) {
return text != null ? text.includes("/") && text.split("/").length === 2 && text.split("/")[0].length > 0 && text.split("/")[1].length > 0 : false;
}
function getTheLastCommitHash() {
try {
return execSync("git rev-parse HEAD").toString().trim();
} catch (error) {
console.error("Error fetching the last commit hash:", error);
return false;
}
}
function resetToCommit(commit) {
try {
execSync(`git reset --hard ${commit}`);
return true;
} catch (error) {
console.error("Error resetting to commit:", error);
return false;
}
}
// src/changelog.ts
var COMMENT_LINE = "[comment]: # (Insert new version after this line)";
function getReleaseUrl(gitRepo, newVersion) {
if (gitRepo == null) {
return null;
}
return gitRepo.type === "github" ? `https://github.com/${gitRepo.value}/releases/tag/v${newVersion}` : gitRepo.type === "gitea" ? `${gitRepo.value}/releases/tag/v${newVersion}` : null;
}
function getCommitUrl(gitRepo, commitHash) {
if (gitRepo == null) {
return null;
}
return gitRepo.type === "github" ? `https://github.com/${gitRepo.value}/commit/${commitHash}` : gitRepo.type === "gitea" ? `${gitRepo.value}/commit/${commitHash}` : null;
}
function getCompareUrl(gitRepo) {
if (gitRepo == null) {
return null;
}
return gitRepo.type === "github" ? `https://github.com/${gitRepo.value}/compare/HEAD...HEAD` : gitRepo.type === "gitea" ? `${gitRepo.value}/compare/HEAD...HEAD` : null;
}
function prepareCommitVersionChangelog(gitRepo, oldVersion, newVersion, disableLinks, versionComment) {
const lastTag = getMostRecentGitTag();
const gitLogs = checkVersionExistsInGitTags(
oldVersion
) ? getGitLog(`v${oldVersion}`) : lastTag != null ? getGitLog(lastTag) : getGitLog();
const changesMap = /* @__PURE__ */ new Map();
for (const log of gitLogs) {
for (const message of log.message.split("\n")) {
if (!changesMap.has(message)) {
changesMap.set(message, []);
}
changesMap.get(message)?.push(log.hash.slice(0, 7));
}
}
const sortedChanges = Array.from(changesMap).sort();
let fullListMarkdown = "";
for (const [message, hashes] of sortedChanges) {
fullListMarkdown += disableLinks ? `- ${message} (${hashes.join(", ")})
` : `- ${message} ([${hashes.join("], [")}])
`;
}
const releaseDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
const releaseUrl = getReleaseUrl(gitRepo, newVersion);
const releaseLink = releaseUrl != null ? `[${newVersion}](${releaseUrl})` : newVersion;
const releaseDateHeader = `## ${releaseLink} \u2014 ${releaseDate}
`;
let newVersionMarkdown = `${versionComment && versionComment.length > 0 ? versionComment : ""}${fullListMarkdown && fullListMarkdown.length > 0 ? `${versionComment && versionComment.length > 0 ? "Full list:\n\n" : ""}${fullListMarkdown}` : ""}
`;
if (!disableLinks) {
for (const log of gitLogs) {
const commitHash = log.hash.slice(0, 7);
const commitUrl = getCommitUrl(gitRepo, commitHash);
if (commitUrl != null) {
const commitLink = `[${commitHash}]: ${commitUrl}`;
newVersionMarkdown += `${commitLink}
`;
}
}
}
return { version: newVersionMarkdown, header: releaseDateHeader };
}
function preparePullRequestChangelog(gitRepo, newVersion) {
const releaseDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
const releaseUrl = getReleaseUrl(gitRepo, newVersion);
const releaseLink = releaseUrl != null ? `[${newVersion}](${releaseUrl})` : newVersion;
const header = `## ${releaseLink} \u2014 ${releaseDate}
`;
return { header };
}
async function updateChangelog(isInteractive, oldVersion, newVersion, gitRepo, disableChangelogCheck, disableLinks, versionComment) {
const changelogPath = path.join(process.cwd(), "CHANGELOG.md");
const changelogExists = fs.existsSync(changelogPath);
let changelogContent = "";
if (!changelogExists) {
changelogContent = `# Changelog
This changelog is updated by [Cinnabar Meta](https://github.com/cinnabar-forge/node-meta).
## [Unreleased]
Visit the link above to see all unreleased changes.
${COMMENT_LINE}
[unreleased]: ${getCompareUrl(gitRepo)}
`;
fs.writeFileSync(changelogPath, changelogContent);
} else {
changelogContent = fs.readFileSync(changelogPath, "utf8");
}
const newVersionComment = versionComment != null ? versionComment : isInteractive ? await promptText("Enter version summary") : null;
const pullRequestsPath = path.join(
process.cwd(),
".cinnabar-meta-pull-requests.md"
);
const pullRequestsExists = fs.existsSync(pullRequestsPath);
const pullRequests = fs.readFileSync(pullRequestsPath, "utf8");
let versionResult;
if (!disableChangelogCheck && pullRequestsExists && pullRequests?.length > 0) {
const markdowns = preparePullRequestChangelog(gitRepo, newVersion);
changelogContent = changelogContent.replace(
COMMENT_LINE,
`${COMMENT_LINE}
${markdowns.header}${pullRequests}`
);
versionResult = pullRequests;
fs.rmSync(pullRequestsPath);
} else {
const markdowns = prepareCommitVersionChangelog(
gitRepo,
oldVersion,
newVersion,
disableLinks,
newVersionComment
);
changelogContent = changelogContent.replace(
COMMENT_LINE,
`${COMMENT_LINE}
${markdowns.header}${markdowns.version}`
);
versionResult = markdowns.version;
}
const unreleasedLinkRegex = /compare\/(.+?)\.\.\.HEAD/;
const unreleasedLinkMatch = changelogContent.match(unreleasedLinkRegex);
if (unreleasedLinkMatch) {
changelogContent = changelogContent.replace(
unreleasedLinkRegex,
`compare/v${newVersion}...HEAD`
);
}
fs.writeFileSync(changelogPath, changelogContent);
return versionResult;
}
function writeChangelog(version, changelog) {
fs.mkdirSync("tmp", { recursive: true });
fs.writeFileSync(`tmp/CHANGELOG-${version}.md`, changelog);
fs.writeFileSync("tmp/CHANGELOG-latest.md", changelog);
fs.writeFileSync("tmp/version", version);
}
// src/cinnabar.ts
var CINNABAR_PROJECT_VERSION = "0.4.3";
// src/cli.ts
import { parseCli, promptOptions, promptText as promptText2 } from "clivo";
// src/version.ts
function parseVersion(version) {
const [versionAndPrereleasePart] = version.split("+");
const [versionPart, prereleasePart] = versionAndPrereleasePart.split("-");
const [major, minor, patch] = versionPart.split(".");
const [prerelease, prereleaseNumber] = prereleasePart ? prereleasePart.split(".") : [];
return {
major: Number.parseInt(major) ?? 0,
minor: Number.parseInt(minor) ?? 0,
patch: Number.parseInt(patch) ?? 0,
prerelease: prerelease || void 0,
prereleaseNumber: prerelease && prereleaseNumber != null ? Number.parseInt(prereleaseNumber) : void 0
};
}
function checkVersion(version) {
if (checkVersionExistsInGitTags(version)) {
throw new Error(`Version ${version} already exists`);
}
}
function updateVersion(parsedVersion, updateType, prerelease) {
let { major, minor, patch } = parsedVersion;
if (updateType === "major") {
major++;
minor = 0;
patch = 0;
} else if (updateType === "minor") {
minor++;
patch = 0;
} else if (updateType === "patch") {
patch++;
}
return `${major}.${minor}.${patch}${prerelease ? `-${prerelease}` : ""}`;
}
function updatePrerelease(parsedVersion, newPrerelease) {
const { major, minor, patch } = parsedVersion;
let { prerelease, prereleaseNumber } = parsedVersion;
if (newPrerelease === "yes") {
prerelease = void 0;
prereleaseNumber = void 0;
} else if (prerelease != null) {
if (newPrerelease !== prerelease) {
prerelease = newPrerelease;
prereleaseNumber = void 0;
} else if (prereleaseNumber == null) {
prereleaseNumber = 1;
} else {
prereleaseNumber++;
}
}
return `${major}.${minor}.${patch}${prerelease ? `-${prerelease}` : ""}${prereleaseNumber ? `.${prereleaseNumber}` : ""}`;
}
function markBuild(parsedVersion) {
const { major, minor, patch, prerelease, prereleaseNumber } = parsedVersion;
const datetime = new Date(Date.now()).toISOString().replaceAll("-", "").replaceAll(":", "").replace("T", ".").replace("Z", "").split(".");
const build = `${datetime[0]}_${datetime[1]}`;
return `${major}.${minor}.${patch}${prerelease ? `-${prerelease}` : ""}${prereleaseNumber ? `.${prereleaseNumber}` : ""}+next.${build}`;
}
// src/cli.ts
function setupCli() {
return parseCli({
args: process.argv,
options: [
{ letter: "p", name: "pwd" },
{ letter: "u", name: "update" },
{ letter: "r", name: "prerelease" },
{ letter: "b", name: "build" },
{ letter: "i", name: "interactive" },
{ letter: "f", name: "file" },
{ name: "push" }
]
});
}
async function askUpdateType(parsedVersion, oldVersion) {
const answers = parsedVersion.prerelease != null ? [
{
label: `Update (${updatePrerelease(parsedVersion, parsedVersion.prerelease)})`,
name: "prerelease-update"
},
{
label: `Change tag (${updatePrerelease(parsedVersion, "newtag")})`,
name: "prerelease-change"
},
{
label: `Release (${updatePrerelease(parsedVersion, "yes")})`,
name: "prerelease-release"
},
{ label: "Mark build info", name: "build" }
] : [
{
label: `Major (${updateVersion(parsedVersion, "major")})`,
name: "major"
},
{
label: `Minor (${updateVersion(parsedVersion, "minor")})`,
name: "minor"
},
{
label: `Patch (${updateVersion(parsedVersion, "patch")})`,
name: "patch"
},
{
label: `Major prerelease (${updateVersion(parsedVersion, "major", "tag")})`,
name: "major-prerelease"
},
{
label: `Minor prerelease (${updateVersion(parsedVersion, "minor", "tag")})`,
name: "minor-prerelease"
},
{
label: `Patch prerelease (${updateVersion(parsedVersion, "patch", "tag")})`,
name: "patch-prerelease"
},
{ label: "Mark build info", name: "build" }
];
const answer = await promptOptions(
parsedVersion.prerelease != null ? `What to do with prerelease version ${oldVersion}?` : `Update version ${oldVersion} to...`,
answers
);
return answer.name;
}
async function askGitProvider() {
return (await promptOptions("What is your git provider?", [
{ label: "GitHub", name: "github" },
{ label: "Gitea", name: "gitea" }
])).name;
}
async function askGithubRepo() {
const text = await promptText2("Enter GitHub repository (format: user/repo)");
if (!checkGithubRepo(text)) {
console.log("Invalid GitHub repository format");
return await askGithubRepo();
}
return text;
}
async function askGiteaRepo() {
const text = await promptText2(
"Enter GitHub repository (format: protocol://gitea.example.com/user/repo)"
);
return text;
}
async function askPrereleaseTag() {
return await promptText2("Enter new prerelease tag");
}
async function askCommitType() {
return (await promptOptions("What to do next?", [
{ label: "Nothing", name: "nothing" },
{ label: "Commit and tag", name: "commit" },
{ label: "Commit and tag and push", name: "commit-push" }
])).name;
}
async function askYesOrNo(text) {
return await promptText2(`${text} (yes/no)`);
}
// src/files.ts
import fs2 from "node:fs";
import path2 from "node:path";
import { listenClivoEvent } from "clivo";
var ANCA_JSON_PATH = "anca.json";
var CINNABAR_JSON_PATH = "cinnabar.json";
var UPDATE_CINNABARMETA_PATH = "update.cinnabarmeta";
function getUpdateTypeFromFile() {
const updateContent = fs2.readFileSync(UPDATE_CINNABARMETA_PATH, "utf8");
const lines = updateContent.split("\n");
const update = lines[0];
const description = lines.slice(1).join("\n").trim();
fs2.rmSync(UPDATE_CINNABARMETA_PATH);
return {
update,
description
};
}
function getMetaDataFromFiles() {
if (fs2.existsSync(ANCA_JSON_PATH)) {
const ancaContent = fs2.readFileSync(ANCA_JSON_PATH, "utf8");
try {
const ancaJson = JSON.parse(ancaContent);
if (ancaJson.cinnabarMeta) {
return ancaJson.cinnabarMeta;
}
} catch (error) {
console.error("Error parsing anca.json:", error);
}
}
if (fs2.existsSync(CINNABAR_JSON_PATH)) {
const cinnabarContent = fs2.readFileSync(CINNABAR_JSON_PATH, "utf8");
try {
return JSON.parse(cinnabarContent);
} catch (error) {
console.error("Error parsing cinnabar.json:", error);
}
}
}
async function updateMetaDataFiles(oldVersion, newVersion, isBuild, files, repo) {
const updateMeta = async (data) => {
data.dataVersion = 0;
data.version = {
latest: isBuild ? oldVersion : newVersion,
latestNext: isBuild ? newVersion : void 0,
timestamp: Math.floor(Date.now() / 1e3)
};
if (data.files == null) {
data.files = [];
}
if (repo != null) {
data.repo = repo;
}
};
let success = false;
if (fs2.existsSync(ANCA_JSON_PATH)) {
const ancaContent = fs2.readFileSync(ANCA_JSON_PATH, "utf8");
try {
const ancaJson = JSON.parse(ancaContent);
if (ancaJson.cinnabarMeta == null) {
ancaJson.cinnabarMeta = {};
}
await updateMeta(ancaJson.cinnabarMeta);
fs2.writeFileSync(
ANCA_JSON_PATH,
`${JSON.stringify(ancaJson, null, 2)}
`
);
success = true;
} catch (error) {
console.error("Error parsing anca.json:", error);
}
} else if (fs2.existsSync(CINNABAR_JSON_PATH)) {
const cinnabarContent = fs2.readFileSync(CINNABAR_JSON_PATH, "utf8");
try {
const cinnabarJson = JSON.parse(cinnabarContent);
await updateMeta(cinnabarJson);
fs2.writeFileSync(
CINNABAR_JSON_PATH,
`${JSON.stringify(cinnabarJson, null, 2)}
`
);
success = true;
} catch (error) {
console.error("Error parsing cinnabar.json:", error);
}
}
for (const file of files) {
if (isBuild && !file.updateBuild) {
continue;
}
if (file.type === "nodejs-package-json") {
let packageJson = {};
if (fs2.existsSync(file.path)) {
packageJson = JSON.parse(fs2.readFileSync(file.path, "utf8"));
}
packageJson.version = newVersion;
fs2.writeFileSync(file.path, `${JSON.stringify(packageJson, null, 2)}
`);
} else if (file.type === "nodejs-package-lock-json") {
if (fs2.existsSync(file.path)) {
const packageLockJson = JSON.parse(fs2.readFileSync(file.path, "utf8"));
packageLockJson.version = newVersion;
if (packageLockJson.packages?.[""]) {
packageLockJson.packages[""].version = newVersion;
}
fs2.writeFileSync(
file.path,
`${JSON.stringify(packageLockJson, null, 2)}
`
);
}
} else if (file.type === "javascript" || file.type === "typescript") {
const content = `// This file was generated by Cinnabar Meta. Do not edit.
export const CINNABAR_PROJECT_TIMESTAMP = ${Math.floor(Date.now() / 1e3)};
export const CINNABAR_PROJECT_VERSION = "${newVersion}";
`;
fs2.writeFileSync(path2.resolve(file.path), content);
}
}
return success;
}
function detectVersionsFromFiles() {
const versions = [];
const packageJsonPath = "package.json";
if (fs2.existsSync(packageJsonPath)) {
const packageJsonContent = fs2.readFileSync(packageJsonPath, "utf8");
try {
const packageJson = JSON.parse(packageJsonContent);
if (packageJson.version) {
versions.push(packageJson.version);
}
} catch (error) {
console.error("Error parsing package.json:", error);
}
}
return versions;
}
function lockCinnabar() {
const lockFilePath = "cinnabar.lock";
if (fs2.existsSync(lockFilePath)) {
return fs2.readFileSync(lockFilePath, "utf8");
}
fs2.writeFileSync(lockFilePath, getTheLastCommitHash() || "");
return true;
}
function unlockCinnabar() {
const lockFilePath = "cinnabar.lock";
if (fs2.existsSync(lockFilePath)) {
fs2.unlinkSync(lockFilePath);
}
}
function lockCinnabarPid() {
const pidLockFilePath = "cinnabar.pid.lock";
if (fs2.existsSync(pidLockFilePath)) {
throw new Error("Cinnabar is already running");
}
fs2.writeFileSync(pidLockFilePath, process.pid.toString());
return true;
}
function unlockCinnabarPid() {
const pidLockFilePath = "cinnabar.pid.lock";
if (fs2.existsSync(pidLockFilePath)) {
fs2.unlinkSync(pidLockFilePath);
}
}
listenClivoEvent("clivoCancel", () => {
unlockCinnabar();
unlockCinnabarPid();
});
// src/index.ts
async function main() {
lockCinnabarPid();
const printIntro = () => {
const design1 = "=".repeat(4);
const text = `${design1} Cinnabar Meta v${CINNABAR_PROJECT_VERSION} ${design1}`;
const design2 = "=".repeat(text.length);
console.log(`
${design2}
${text}
${design2}
`);
};
printIntro();
const bye = () => {
unlockCinnabar();
console.log("\nBye!\n");
unlockCinnabarPid();
};
const success = lockCinnabar();
if (success !== true) {
console.log("[WARNING] Reverting to the last stable commit", success);
const askToRevert = await askYesOrNo(
"Do you want to revert to the last stable commit?"
);
if (askToRevert === "yes") {
resetToCommit(success);
} else {
bye();
return;
}
}
const options = setupCli();
const isInteractive = !!options.interactive?.[0];
let oldVersion;
const metaData = getMetaDataFromFiles();
if (metaData != null) {
oldVersion = metaData.version.latest;
} else {
const versions = detectVersionsFromFiles();
if (versions.length === 0) {
console.log("No versions found in files");
oldVersion = "0.0.0";
} else {
oldVersion = versions[0];
}
}
const parsedVersion = parseVersion(oldVersion);
let gitRepo = metaData?.repo?.type === "github" && checkGithubRepo(metaData?.repo?.value) ? metaData.repo : metaData?.repo?.type === "gitea" ? metaData?.repo : null;
if (gitRepo == null && metaData?.updateChangelog) {
if (isInteractive) {
const provider = await askGitProvider();
if (provider === "github") {
gitRepo = {
type: provider,
value: await askGithubRepo()
};
} else {
gitRepo = {
type: provider,
value: await askGiteaRepo()
};
}
}
}
let update;
let prerelease;
let build;
let newVersion;
const processUpdateType = async (updateType) => {
switch (updateType) {
case "prerelease-update":
prerelease = parsedVersion.prerelease;
break;
case "prerelease-change":
prerelease = await askPrereleaseTag();
break;
case "prerelease-release":
prerelease = "yes";
break;
case "build":
build = "yes";
break;
case "major":
case "minor":
case "patch":
update = updateType;
break;
case "major-prerelease":
case "minor-prerelease":
case "patch-prerelease":
update = updateType.split("-")[0];
prerelease = await askPrereleaseTag();
break;
}
};
let versionComment;
if (options.file) {
const result = getUpdateTypeFromFile();
versionComment = result.description;
processUpdateType(result.update);
} else if (options.interactive) {
const updateType = await askUpdateType(parsedVersion, oldVersion);
processUpdateType(updateType);
} else if (options.prerelease != null || options.update != null || options.build != null) {
if (options.update?.[0]) {
update = options.update[0];
}
if (options.prerelease?.[0]) {
prerelease = options.prerelease[0];
}
if (options.build?.[0]) {
build = options.build[0];
}
} else {
console.log(
"No update type specified. Pass --interactive option to choose one, or use specific update type with --update option."
);
bye();
return;
}
if (update != null) {
newVersion = updateVersion(parsedVersion, update, prerelease);
} else if (prerelease != null) {
newVersion = updatePrerelease(parsedVersion, prerelease);
} else if (build != null) {
newVersion = markBuild(parsedVersion);
} else {
console.log("No update.");
bye();
return;
}
checkVersion(newVersion);
console.log("New app version:", newVersion);
await updateMetaDataFiles(
oldVersion,
newVersion,
build != null,
metaData?.files || [],
gitRepo
);
if (metaData?.updateChangelog && build == null) {
const versionChangelog = await updateChangelog(
isInteractive,
oldVersion,
newVersion,
gitRepo,
!!metaData?.disableChangelogCheck,
!!metaData?.disableLinks,
versionComment
);
writeChangelog(newVersion, versionChangelog);
}
if (build == null) {
if (options.interactive) {
const commitType = await askCommitType();
if (commitType === "commit" || commitType === "commit-push") {
commitChanges(newVersion, commitType === "commit-push");
}
} else {
commitChanges(newVersion, options.push != null);
}
}
bye();
}
main();