chore(release): add colored output to release script

This commit is contained in:
t8y2 2026-07-07 02:32:18 +08:00
parent d8b4ec92cf
commit 56fbbbfc8e
1 changed files with 61 additions and 44 deletions

View File

@ -42,6 +42,23 @@ let yes = false;
let skipFetch = false;
let force = false;
// NO_COLOR (https://no-color.org) wins, then FORCE_COLOR, then autodetect TTY.
const USE_COLOR = !process.env.NO_COLOR && (process.stdout.isTTY || (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0"));
function ansi(code, text) {
return USE_COLOR ? `\x1b[${code}m${text}\x1b[0m` : text;
}
const bold = (s) => ansi(1, s);
const dim = (s) => ansi(2, s);
const cyan = (s) => ansi(36, s);
const green = (s) => ansi(32, s);
const yellow = (s) => ansi(33, s);
const red = (s) => ansi(31, s);
const magenta = (s) => ansi(35, s);
// `Label: value` with a dimmed label and a colored value.
function kv(label, value, color = cyan) {
return `${dim(label)}: ${color(value)}`;
}
for (const arg of args) {
switch (arg) {
case "--":
@ -109,29 +126,29 @@ async function releasePackages(bump) {
const releaseTag = `${PACKAGE_TAG_PREFIX}${releaseVersion}`;
const workflowArgs = ["workflow", "run", PACKAGES_WORKFLOW, "--repo", REPO, "-f", `version=${releaseVersion}`];
console.log(`Release target: Node packages / MCP`);
console.log(`Current package version: ${latestVersion ?? "none"}`);
console.log(kv("Release target", "Node packages / MCP", bold));
console.log(kv("Current package version", latestVersion ?? "none", green));
printReleaseStatus(status);
if (!status.needed && !force) {
console.log("No Node package release needed; publish-relevant package files have not changed.");
console.log("Use --force to trigger the workflow anyway.");
console.log(yellow("No Node package release needed; publish-relevant package files have not changed."));
console.log(dim("Use --force to trigger the workflow anyway."));
return;
}
console.log(`New package version: ${releaseVersion}`);
console.log(`Release tag: ${releaseTag}`);
console.log(`Workflow: Node Packages Release (${PACKAGES_WORKFLOW})`);
console.log(`Command: gh ${workflowArgs.join(" ")}`);
console.log(kv("New package version", releaseVersion, green));
console.log(kv("Release tag", releaseTag, yellow));
console.log(kv("Workflow", `Node Packages Release (${PACKAGES_WORKFLOW})`, magenta));
console.log(kv("Command", `gh ${workflowArgs.join(" ")}`, dim));
if (dryRun) {
console.log("Dry run only; workflow was not triggered.");
console.log(dim("Dry run only; workflow was not triggered."));
return;
}
ensureGhReady(PACKAGES_WORKFLOW);
await confirmOrExit(`Confirm triggering Node Packages Release for ${releaseVersion}? [y/N] `);
await confirmOrExit(`Confirm triggering Node Packages Release for ${bold(releaseVersion)}? [y/N] `);
run("gh", workflowArgs, { stdio: "inherit" });
console.log(`Triggered Node Packages Release for ${releaseVersion}.`);
console.log(green(`Triggered Node Packages Release for ${releaseVersion}.`));
}
async function releaseAgents(bump) {
@ -143,28 +160,28 @@ async function releaseAgents(bump) {
fail(`Tag ${releaseTag} already exists.`);
}
console.log(`Release target: Agents`);
console.log(`Current agent tag: ${latest.tag}${latest.source ? ` (${latest.source})` : ""}`);
console.log(kv("Release target", "Agents", bold));
console.log(kv("Current agent tag", `${latest.tag}${latest.source ? ` (${latest.source})` : ""}`, yellow));
printReleaseStatus(status);
if (!status.needed && !force) {
console.log("No agents release needed; publish-relevant agent runtime files have not changed.");
console.log("Use --force to create the tag anyway.");
console.log(yellow("No agents release needed; publish-relevant agent runtime files have not changed."));
console.log(dim("Use --force to create the tag anyway."));
return;
}
console.log(`New agent tag: ${releaseTag}`);
console.log(`Workflow: Agents Release (.github/workflows/agents-release.yml)`);
console.log(`Commands: git tag ${releaseTag} && git push origin ${releaseTag}`);
console.log(kv("New agent tag", releaseTag, yellow));
console.log(kv("Workflow", "Agents Release (.github/workflows/agents-release.yml)", magenta));
console.log(kv("Commands", `git tag ${releaseTag} && git push origin ${releaseTag}`, dim));
if (dryRun) {
console.log("Dry run only; tag was not created or pushed.");
console.log(dim("Dry run only; tag was not created or pushed."));
return;
}
await confirmOrExit(`Confirm creating and pushing tag ${releaseTag}? [y/N] `);
await confirmOrExit(`Confirm creating and pushing tag ${bold(releaseTag)}? [y/N] `);
run("git", ["tag", releaseTag], { stdio: "inherit" });
run("git", ["push", "origin", releaseTag], { stdio: "inherit" });
console.log(`Pushed ${releaseTag}; Agents Release will run from the tag push.`);
console.log(green(`Pushed ${releaseTag}; Agents Release will run from the tag push.`));
}
async function publishApp(tagInput) {
@ -172,33 +189,33 @@ async function publishApp(tagInput) {
const releaseTag = tagInput ? resolveAppTag(tagInput) : latest.tag;
const workflowArgs = ["workflow", "run", APP_PUBLISH_WORKFLOW, "--repo", REPO, "-f", `tag=${releaseTag}`];
console.log(`Release target: App distribution`);
console.log(`Latest app tag: ${latest.tag}`);
console.log(`Publish tag: ${releaseTag}`);
console.log(`Workflow: Publish Packages (${APP_PUBLISH_WORKFLOW})`);
console.log(`Command: gh ${workflowArgs.join(" ")}`);
console.log(kv("Release target", "App distribution", bold));
console.log(kv("Latest app tag", latest.tag, yellow));
console.log(kv("Publish tag", releaseTag, yellow));
console.log(kv("Workflow", `Publish Packages (${APP_PUBLISH_WORKFLOW})`, magenta));
console.log(kv("Command", `gh ${workflowArgs.join(" ")}`, dim));
if (dryRun) {
console.log("Dry run only; workflow was not triggered.");
console.log(dim("Dry run only; workflow was not triggered."));
return;
}
ensureGhReady(APP_PUBLISH_WORKFLOW);
run("gh", ["release", "view", releaseTag, "--repo", REPO], { stdio: "inherit" });
await confirmOrExit(`Confirm publishing app distribution for ${releaseTag}? [y/N] `);
await confirmOrExit(`Confirm publishing app distribution for ${bold(releaseTag)}? [y/N] `);
run("gh", workflowArgs, { stdio: "inherit" });
console.log(`Triggered Publish Packages for ${releaseTag}.`);
console.log(green(`Triggered Publish Packages for ${releaseTag}.`));
}
async function promptTarget() {
const packageStatus = getPackageReleaseStatus();
const agentStatus = getAgentReleaseStatus(getLatestAgentTag());
const answer = await ask(`Select release target:
1. Node packages / MCP (${formatStatusSummary(packageStatus)})
2. Agents (${formatStatusSummary(agentStatus)})
3. App distribution
Choice [1]: `);
const answer = await ask(`${bold("Select release target:")}
${cyan("1")}. Node packages / MCP (${formatStatusSummary(packageStatus)})
${cyan("2")}. Agents (${formatStatusSummary(agentStatus)})
${cyan("3")}. App distribution
${dim("Choice")} [1]: `);
const normalized = answer.trim().toLowerCase();
if (!normalized || normalized === "1" || normalized === "packages" || normalized === "mcp") return "packages";
@ -215,7 +232,7 @@ async function confirmOrExit(message) {
const answer = await ask(message);
if (!["y", "yes"].includes(answer.trim().toLowerCase())) {
console.log("Cancelled.");
console.log(dim("Cancelled."));
process.exit(0);
}
}
@ -337,18 +354,18 @@ function getChangedFilesSince(ref, paths) {
}
function printReleaseStatus(status) {
console.log(`Release needed: ${status.needed ? "yes" : "no"}`);
console.log(`Compared against: ${status.baseline}`);
console.log(kv("Release needed", status.needed ? "yes" : "no", status.needed ? yellow : green));
console.log(kv("Compared against", status.baseline, dim));
if (status.reason) {
console.log(`Reason: ${status.reason}`);
console.log(kv("Reason", status.reason, dim));
}
if (status.changedFiles.length > 0) {
console.log(`Changed publish-relevant files:`);
console.log(dim("Changed publish-relevant files:"));
for (const file of status.changedFiles.slice(0, 20)) {
console.log(` - ${file}`);
console.log(` ${dim("-")} ${file}`);
}
if (status.changedFiles.length > 20) {
console.log(` ... and ${status.changedFiles.length - 20} more`);
console.log(dim(` ... and ${status.changedFiles.length - 20} more`));
}
}
}
@ -356,9 +373,9 @@ function printReleaseStatus(status) {
function formatStatusSummary(status) {
if (status.needed) {
const detail = status.changedFiles.length > 0 ? `${status.changedFiles.length} changed file${status.changedFiles.length === 1 ? "" : "s"}` : "release baseline missing";
return `needs release, ${detail}`;
return `${yellow("needs release")}, ${detail}`;
}
return `no release needed since ${status.baseline}`;
return `${green("no release needed")} since ${status.baseline}`;
}
function getLatestSemverTag(prefix) {
@ -503,7 +520,7 @@ function escapeRegExp(value) {
}
function fail(message) {
console.error(message);
console.error(red(message));
process.exit(1);
}