UNPKG

@cinnabar-forge/meta

Version:
732 lines (713 loc) 23.2 kB
// 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();