diff --git a/scripts/release.mjs b/scripts/release.mjs index 47ac81423..87880686a 100644 --- a/scripts/release.mjs +++ b/scripts/release.mjs @@ -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); }