#!/usr/bin/env node // Coordinates release-candidate validation runs and emits the publish command // only after required local, CI, npm, plugin, and E2E evidence is green. import { spawnSync } from "node:child_process"; import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; import { basename, join } from "node:path"; import { fileURLToPath } from "node:url"; import { isDeepStrictEqual } from "node:util"; import { stripLeadingPackageManagerSeparator } from "./lib/arg-utils.mjs"; import { readBoundedResponseText } from "./lib/bounded-response.mjs"; import { dedicatedSectionVersionForTag, extractChangelogReleaseSections, extractChangelogSection, formatShippedBaselineExclusions, parseShippedBaselineExclusions, releaseNotesSectionForTag, releaseNotesVersionForTag, renderGithubReleaseNotes, } from "./render-github-release-notes.mjs"; import { isShaPinnedReleaseValidationBranch, runStrictReleaseEvidenceValidation, validateFullReleaseValidationEvidence, } from "./validate-full-release-validation-evidence.mjs"; const DEFAULT_REPO = "openclaw/openclaw"; const DEFAULT_PROVIDER = "openai"; const DEFAULT_MODE = "both"; const DEFAULT_NPM_DIST_TAG = "beta"; const DEFAULT_PLUGIN_SCOPE = "all-publishable"; const DEFAULT_TELEGRAM_PROVIDER_MODE = "mock-openai"; const DEFAULT_GITHUB_API_TIMEOUT_MS = 30_000; const DEFAULT_GITHUB_API_RESPONSE_BODY_MAX_BYTES = 16 * 1024 * 1024; const COMMAND_CAPTURE_MAX_BUFFER_BYTES = 16 * 1024 * 1024; const TOOLING_ROOT = fileURLToPath(new URL("../", import.meta.url)); const TIDECLAW_ALPHA_WORKFLOW_REF_PATTERN = /^tideclaw\/alpha\/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$/u; const WINDOWS_NODE_TAG_PATTERN = /^v[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?$/u; const WINDOWS_NODE_REPO = "openclaw/openclaw-windows-node"; const WINDOWS_NODE_REQUIRED_ASSETS = [ "OpenClawCompanion-Setup-x64.exe", "OpenClawCompanion-Setup-arm64.exe", ]; const SHA256_DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/u; const RELEASE_CANDIDATE_STATE_VERSION = 1; const RELEASE_CANDIDATE_STATE_FILE = "release-candidate-state.json"; const RELEASE_CANDIDATE_STATE_KEYS = [ "repo", "tag", "targetSha", "toolingSha", "workflowRef", "provider", "mode", "releaseProfile", "npmDistTag", "pluginPublishScope", "plugins", "windowsNodeTag", "skipParallels", "skipTelegram", "telegramProviderMode", ]; function usage() { return `Usage: pnpm release:candidate -- --tag vYYYY.M.PATCH-beta.N [options] Dispatches or consumes release validation runs, validates the prepared npm tarball, builds plugin publish plans, writes a green evidence bundle, then prints the exact OpenClaw Release Publish command only after everything is green. Options: --tag Release tag to validate. --workflow-ref Trusted workflow ref. Default: main; matching Tideclaw branch required for alpha. --repo GitHub repo. Default: ${DEFAULT_REPO} --full-release-run Reuse successful Full Release Validation run. --npm-preflight-run Reuse successful OpenClaw NPM Release preflight run. --windows-node-tag Exact Windows Node release tag. Required for stable. --skip-dispatch Require both run ids; do not dispatch workflows. --skip-local-generated-check Do not run local generated release baseline checks before dispatch. --skip-parallels Do not run local Parallels fresh/update candidate smoke. --skip-telegram Do not run NPM Telegram E2E against the prepared tarball. --telegram-provider-mode mock-openai|live-frontier. Default: ${DEFAULT_TELEGRAM_PROVIDER_MODE} --provider Full validation provider. Default: ${DEFAULT_PROVIDER} --mode Full validation cross-OS mode. Default: ${DEFAULT_MODE} --release-profile Default: beta for prereleases; stable otherwise. --npm-dist-tag Default: ${DEFAULT_NPM_DIST_TAG} --plugin-publish-scope selected|all-publishable. Default: ${DEFAULT_PLUGIN_SCOPE} --plugins Required when plugin scope is selected. --output-dir Evidence output dir. Default: .artifacts/release-candidate/ `; } function requireValue(argv, index, flag) { const value = argv[index]; if (!value || value.startsWith("-")) { throw new Error(`${flag} requires a value`); } return value; } export function releaseBranchForTag(tag) { if (tag.includes("-alpha.")) { return ""; } const version = tag.replace(/^v/u, "").split("-", 1)[0]; return `release/${version}`; } /** * Parses release-candidate validation options and enforces publish-scope policy. */ export function parseArgs(argv) { const args = stripLeadingPackageManagerSeparator(argv); const options = { repo: DEFAULT_REPO, provider: DEFAULT_PROVIDER, mode: DEFAULT_MODE, releaseProfile: "", npmDistTag: DEFAULT_NPM_DIST_TAG, pluginPublishScope: DEFAULT_PLUGIN_SCOPE, plugins: "", skipDispatch: false, skipLocalGeneratedCheck: false, skipParallels: false, skipTelegram: false, telegramProviderMode: DEFAULT_TELEGRAM_PROVIDER_MODE, tag: "", workflowRef: "", fullReleaseRunId: "", npmPreflightRunId: "", windowsNodeTag: "", windowsNodeInstallerDigests: "", outputDir: "", }; const seen = new Set(); const setOnce = (flag, key, value) => { if (seen.has(flag)) { throw new Error(`${flag} was provided more than once`); } seen.add(flag); options[key] = value; }; parseArgv: for (let index = 0; index < args.length; index += 1) { const arg = args[index]; switch (arg) { case "--": break parseArgv; case "--tag": setOnce(arg, "tag", requireValue(args, ++index, arg)); break; case "--workflow-ref": setOnce(arg, "workflowRef", requireValue(args, ++index, arg)); break; case "--repo": setOnce(arg, "repo", requireValue(args, ++index, arg)); break; case "--full-release-run": setOnce(arg, "fullReleaseRunId", requireValue(args, ++index, arg)); break; case "--npm-preflight-run": setOnce(arg, "npmPreflightRunId", requireValue(args, ++index, arg)); break; case "--windows-node-tag": setOnce(arg, "windowsNodeTag", requireValue(args, ++index, arg)); break; case "--skip-dispatch": setOnce(arg, "skipDispatch", true); break; case "--skip-local-generated-check": setOnce(arg, "skipLocalGeneratedCheck", true); break; case "--skip-parallels": setOnce(arg, "skipParallels", true); break; case "--skip-telegram": setOnce(arg, "skipTelegram", true); break; case "--telegram-provider-mode": setOnce(arg, "telegramProviderMode", requireValue(args, ++index, arg)); break; case "--provider": setOnce(arg, "provider", requireValue(args, ++index, arg)); break; case "--mode": setOnce(arg, "mode", requireValue(args, ++index, arg)); break; case "--release-profile": setOnce(arg, "releaseProfile", requireValue(args, ++index, arg)); break; case "--npm-dist-tag": setOnce(arg, "npmDistTag", requireValue(args, ++index, arg)); break; case "--plugin-publish-scope": setOnce(arg, "pluginPublishScope", requireValue(args, ++index, arg)); break; case "--plugins": setOnce(arg, "plugins", requireValue(args, ++index, arg)); break; case "--output-dir": setOnce(arg, "outputDir", requireValue(args, ++index, arg)); break; case "-h": case "--help": process.stdout.write(usage()); process.exit(0); default: throw new Error(`unknown option: ${arg}`); } } if (!options.tag) { throw new Error("--tag is required"); } if (options.tag.includes("-alpha.")) { if (!TIDECLAW_ALPHA_WORKFLOW_REF_PATTERN.test(options.workflowRef)) { throw new Error( "--workflow-ref must be the matching tideclaw/alpha/YYYY-MM-DD-HHMMZ branch for alpha release candidates", ); } } else { options.workflowRef ||= "main"; } if (!options.tag.includes("-alpha.") && options.workflowRef !== "main") { throw new Error("--workflow-ref must be main for regular beta and stable release candidates"); } options.releaseProfile ||= options.tag.includes("-alpha.") || options.tag.includes("-beta.") ? "beta" : "stable"; if (!["beta", "stable", "full"].includes(options.releaseProfile)) { throw new Error("--release-profile must be beta, stable, or full"); } if (options.skipDispatch && (!options.fullReleaseRunId || !options.npmPreflightRunId)) { throw new Error("--skip-dispatch requires --full-release-run and --npm-preflight-run"); } if (options.pluginPublishScope === "selected" && !options.plugins.trim()) { throw new Error("--plugin-publish-scope selected requires --plugins"); } if (options.pluginPublishScope === "selected") { throw new Error( "--plugin-publish-scope selected is only for plugin-only repair publishes; release candidates publish OpenClaw with --plugin-publish-scope all-publishable", ); } if (options.pluginPublishScope === "all-publishable" && options.plugins.trim()) { throw new Error("--plugins is only valid with --plugin-publish-scope selected"); } if (options.windowsNodeTag && !WINDOWS_NODE_TAG_PATTERN.test(options.windowsNodeTag)) { throw new Error("--windows-node-tag must be an explicit version tag, not latest"); } if ( !options.tag.includes("-alpha.") && !options.tag.includes("-beta.") && !options.windowsNodeTag ) { throw new Error("stable release candidates require --windows-node-tag"); } if (!["mock-openai", "live-frontier"].includes(options.telegramProviderMode)) { throw new Error("--telegram-provider-mode must be mock-openai or live-frontier"); } return options; } export function run(command, args, options = {}) { const result = spawnSync(command, args, { cwd: options.cwd, encoding: "utf8", maxBuffer: COMMAND_CAPTURE_MAX_BUFFER_BYTES, stdio: options.capture ? ["ignore", "pipe", "pipe"] : "inherit", }); if (result.status !== 0) { throw new Error( `${command} ${args.join(" ")} failed with ${result.status ?? result.signal}\n${result.stderr ?? ""}`, ); } return result.stdout ?? ""; } function readJson(path, label) { try { return JSON.parse(readFileSync(path, "utf8")); } catch (error) { throw new Error( `${label} is invalid JSON: ${error instanceof Error ? error.message : String(error)}`, { cause: error }, ); } } export function buildReleaseCandidateState(options, { targetSha, toolingSha }) { return { version: RELEASE_CANDIDATE_STATE_VERSION, phase: "validated", repo: options.repo, tag: options.tag, targetSha, toolingSha, workflowRef: options.workflowRef, provider: options.provider, mode: options.mode, releaseProfile: options.releaseProfile, npmDistTag: options.npmDistTag, pluginPublishScope: options.pluginPublishScope, plugins: options.plugins, windowsNodeTag: options.windowsNodeTag, skipParallels: options.skipParallels, skipTelegram: options.skipTelegram, telegramProviderMode: options.telegramProviderMode, fullReleaseRunId: options.fullReleaseRunId, npmPreflightRunId: options.npmPreflightRunId, }; } export function reconcileReleaseCandidateState(saved, expected) { if (!saved) { return expected; } if ( typeof saved !== "object" || Array.isArray(saved) || saved.version !== RELEASE_CANDIDATE_STATE_VERSION ) { throw new Error("release candidate state has an unsupported schema"); } for (const key of RELEASE_CANDIDATE_STATE_KEYS) { if (!isDeepStrictEqual(saved[key], expected[key])) { throw new Error( `release candidate state mismatch for ${key}: saved=${JSON.stringify(saved[key])} current=${JSON.stringify(expected[key])}`, ); } } for (const key of ["fullReleaseRunId", "npmPreflightRunId"]) { if (saved[key] && expected[key] && saved[key] !== expected[key]) { throw new Error(`release candidate state mismatch for ${key}`); } } return { ...expected, phase: typeof saved.phase === "string" ? saved.phase : expected.phase, fullReleaseRunId: expected.fullReleaseRunId || saved.fullReleaseRunId || "", npmPreflightRunId: expected.npmPreflightRunId || saved.npmPreflightRunId || "", }; } function writeReleaseCandidateState(path, state) { mkdirSync(join(path, ".."), { recursive: true }); const temporaryPath = `${path}.tmp-${process.pid}`; writeFileSync(temporaryPath, `${JSON.stringify(state, null, 2)}\n`); renameSync(temporaryPath, path); } function updateReleaseCandidateState(path, state, phase, runIds = {}) { const next = { ...state, ...runIds, phase }; writeReleaseCandidateState(path, next); return next; } function githubApiTimeoutMs() { const raw = process.env.OPENCLAW_RELEASE_CANDIDATE_GITHUB_API_TIMEOUT_MS; if (!raw) { return DEFAULT_GITHUB_API_TIMEOUT_MS; } if (!/^[1-9]\d*$/u.test(raw)) { throw new Error("OPENCLAW_RELEASE_CANDIDATE_GITHUB_API_TIMEOUT_MS must be a positive integer"); } const value = Number(raw); if (!Number.isSafeInteger(value)) { throw new Error("OPENCLAW_RELEASE_CANDIDATE_GITHUB_API_TIMEOUT_MS must be a positive integer"); } return value; } function githubApiTimedOut(error) { return ( error instanceof DOMException && (error.name === "AbortError" || error.name === "TimeoutError") ); } /** * Calls the GitHub REST API with the gh-auth token and a bounded timeout. */ export async function githubApi(path, options = {}) { const token = options.token ?? run("gh", ["auth", "token"], { capture: true }).trim(); const timeoutMs = options.timeoutMs ?? githubApiTimeoutMs(); const maxBodyBytes = options.maxBodyBytes ?? DEFAULT_GITHUB_API_RESPONSE_BODY_MAX_BYTES; const controller = new AbortController(); let timeout; const timeoutPromise = new Promise((_, reject) => { timeout = setTimeout(() => { controller.abort(new DOMException("request timed out", "TimeoutError")); reject(new DOMException("request timed out", "TimeoutError")); }, timeoutMs); timeout.unref?.(); }); try { const response = await Promise.race([ (options.fetchImpl ?? fetch)(`https://api.github.com/${path}`, { signal: controller.signal, headers: { Accept: "application/vnd.github+json", Authorization: `Bearer ${token}`, "X-GitHub-Api-Version": "2022-11-28", }, }), timeoutPromise, ]); const text = await readBoundedResponseText(response, `GitHub API ${path}`, maxBodyBytes, { signal: controller.signal, timeoutPromise, }); if (!response.ok) { throw new Error(`GitHub API ${path} failed with ${response.status}: ${text}`); } return JSON.parse(text); } catch (error) { if (githubApiTimedOut(error)) { throw new Error(`GitHub API ${path} timed out after ${timeoutMs}ms`, { cause: error }); } throw error; } finally { clearTimeout(timeout); } } /** * Validates the immutable Windows source release contract for a stable candidate. */ export async function validateWindowsSourceRelease(tag, options = {}) { const release = await githubApi( `repos/${WINDOWS_NODE_REPO}/releases/tags/${encodeURIComponent(tag)}`, options, ); if (release.tag_name !== tag) { throw new Error( `Windows source release tag mismatch: expected ${tag}, got ${release.tag_name}`, ); } if (release.draft) { throw new Error(`Windows source release ${tag} must be published`); } if (release.prerelease) { throw new Error(`Windows source release ${tag} must not be a prerelease`); } const assets = WINDOWS_NODE_REQUIRED_ASSETS.map((name) => { const matches = (release.assets ?? []).filter((entry) => entry.name === name); if (matches.length !== 1) { throw new Error( `Windows source release ${tag} must contain exactly one required asset ${name}; found ${matches.length}`, ); } const [asset] = matches; if (!SHA256_DIGEST_PATTERN.test(asset.digest ?? "")) { throw new Error(`Windows source release ${tag} asset ${name} is missing its SHA-256 digest`); } return { name, digest: asset.digest }; }); return { tag, url: release.html_url, assets, }; } function gitRevParse(ref, cwd) { return run("git", ["rev-parse", ref], { capture: true, cwd }).trim(); } function gitTopLevel(cwd) { return run("git", ["rev-parse", "--show-toplevel"], { capture: true, cwd }).trim(); } function gitTrackedStatus(cwd) { return run("git", ["status", "--porcelain=v1", "--untracked-files=no"], { capture: true, cwd, }); } function fetchTrustedWorkflowSha(workflowRef, toolingRoot) { const remoteRef = `refs/remotes/origin/${workflowRef}`; run("git", ["fetch", "--no-tags", "origin", `+refs/heads/${workflowRef}:${remoteRef}`], { cwd: toolingRoot, }); return gitRevParse(`${remoteRef}^{commit}`, toolingRoot); } function runFromTrustedTooling(argv, { targetRoot, workflowRef }) { const trustedToolingSha = fetchTrustedWorkflowSha(workflowRef, targetRoot); const tempRoot = mkdtempSync(join(tmpdir(), "openclaw-release-tooling-")); const toolingRoot = join(tempRoot, "checkout"); let worktreeAdded = false; try { run("git", ["worktree", "add", "--detach", toolingRoot, trustedToolingSha], { cwd: targetRoot, }); worktreeAdded = true; const result = spawnSync( process.execPath, [join(toolingRoot, "scripts/release-candidate-checklist.mjs"), ...argv], { cwd: targetRoot, env: process.env, stdio: "inherit", }, ); if (result.status !== 0) { throw new Error( `trusted release candidate tooling failed with ${result.status ?? result.signal}`, ); } } finally { if (worktreeAdded) { const cleanup = spawnSync("git", ["worktree", "remove", "--force", toolingRoot], { cwd: targetRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], }); if (cleanup.status !== 0) { console.warn( `could not remove temporary trusted tooling worktree: ${cleanup.stderr?.trim() || cleanup.signal || cleanup.status}`, ); } } rmSync(tempRoot, { force: true, recursive: true }); } } export function validateCandidateCheckout({ targetSha, targetHeadSha, targetTrackedStatus, toolingSha, trustedToolingSha, toolingTrackedStatus, workflowRef, }) { if (targetHeadSha !== targetSha) { throw new Error( `release candidate tag resolves to ${targetSha}, but target worktree HEAD is ${targetHeadSha}`, ); } if (targetTrackedStatus.trim()) { throw new Error( "release candidate validation requires a clean tracked target worktree at the release tag", ); } if (toolingSha !== trustedToolingSha) { throw new Error( `release candidate tooling HEAD ${toolingSha} does not match trusted ${workflowRef} ${trustedToolingSha}`, ); } if (toolingTrackedStatus.trim()) { throw new Error( "release candidate validation requires a clean tracked tooling checkout at the trusted workflow ref", ); } return { status: "passed", targetSha, toolingSha, workflowRef }; } function gitIsAncestor(ancestor, target) { const result = spawnSync( "git", ["merge-base", "--is-ancestor", `${ancestor}^{commit}`, `${target}^{commit}`], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], }, ); if (result.status === 0) { return true; } if (result.status === 1) { return false; } throw new Error( `could not validate changelog provenance ${ancestor}..${target}: ${ result.stderr?.trim() || result.signal || result.status }`, ); } function candidateContributionRecordPullRequests( section, label, { requireExactProvenance = true } = {}, ) { const recordStart = section.search(/\n### Complete contribution record\r?$/m); if (recordStart < 0) { throw new Error(`${label} is missing ### Complete contribution record`); } const record = section.slice(recordStart); const rowNumbers = [...record.matchAll(/^- \*\*PR #(?[0-9]+)\*\*/gmu)].map((match) => Number(match.groups.number), ); const rows = new Set(rowNumbers); if (rows.size !== rowNumbers.length) { const seen = new Set(); const duplicates = rowNumbers.filter((number) => { if (seen.has(number)) { return true; } seen.add(number); return false; }); throw new Error( `${label} contains duplicate contribution record PR rows: ${[...new Set(duplicates)] .map((number) => `#${number}`) .join(", ")}`, ); } if (!requireExactProvenance) { return rows; } const provenance = record.match( /^This audited record covers the complete \S+\.\.[0-9a-f]{40} history: (?[0-9]+) merged PRs?\./mu, ); if (!provenance?.groups?.count) { throw new Error(`${label} is missing exact complete contribution record provenance`); } const declaredCount = Number(provenance.groups.count); if (rows.size !== declaredCount) { throw new Error( `${label} contribution record declares ${declaredCount} PRs but contains ${rows.size}`, ); } return rows; } export function candidateCumulativeShippedPullRequests(changelog, label) { const pullRequests = new Set(); for (const section of extractChangelogReleaseSections(changelog)) { if ( section.version === "Unreleased" || !section.source.includes("\n### Complete contribution record") ) { continue; } for (const number of candidateContributionRecordPullRequests( section.source, `${label} section ${section.version}`, { requireExactProvenance: false }, )) { pullRequests.add(number); } } return pullRequests; } function loadCandidateShippedBaseline(ref) { const tagRef = `refs/tags/${ref}`; gitRevParse(`${tagRef}^{commit}`); const changelog = run("git", ["show", `${tagRef}:CHANGELOG.md`], { capture: true }); const version = releaseNotesVersionForTag(ref); candidateContributionRecordPullRequests( extractChangelogSection(changelog, version), `shipped baseline ${ref}`, ); const pullRequests = candidateCumulativeShippedPullRequests(changelog, `shipped baseline ${ref}`); return { ref, pullRequests }; } export function validateCandidateReleaseNotes({ changelog, repository, tag }) { const rendered = renderGithubReleaseNotes({ changelog, version: releaseNotesVersionForTag(tag), tag, repository, }); return { status: "passed", mode: rendered.mode, characters: rendered.size.characters, bytes: rendered.size.bytes, }; } export function validateCandidateChangelogProvenance({ changelog, version, tag, targetSha, isAncestor = gitIsAncestor, loadShippedBaseline = loadCandidateShippedBaseline, }) { // Validate the same section the renderer publishes: alpha and correction // tags may carry their own heading, and alpha tags may fall back to // Unreleased. let section; let sectionVersion = version; let usesAlphaUnreleasedFallback = false; const dedicatedVersion = dedicatedSectionVersionForTag(tag); if (dedicatedVersion && dedicatedVersion !== version) { try { section = extractChangelogSection(changelog, dedicatedVersion); sectionVersion = dedicatedVersion; } catch { // No dedicated section; validate the base section. } } if (section === undefined) { try { section = extractChangelogSection(changelog, version); } catch (error) { if (!/-alpha\.[1-9][0-9]*$/u.test(tag)) { throw error; } section = releaseNotesSectionForTag(changelog, version, tag); usesAlphaUnreleasedFallback = true; } } const recordStart = section.search(/\n### Complete contribution record\r?$/m); if (recordStart < 0) { if (usesAlphaUnreleasedFallback) { return { status: "skipped", reason: "alpha release uses the explicit Unreleased fallback", shippedBaselines: [], }; } throw new Error( `CHANGELOG.md ## ${sectionVersion} is missing ### Complete contribution record`, ); } const record = section.slice(recordStart); const recordedPullRequests = candidateContributionRecordPullRequests( section, `CHANGELOG.md ## ${sectionVersion}`, ); const provenance = record.match( /^This audited record covers the complete (?\S+)\.\.(?[0-9a-f]{40}) history:/mu, ); const base = provenance?.groups?.base; const recordedTarget = provenance?.groups?.target; if (!base || !recordedTarget) { throw new Error( `CHANGELOG.md ## ${sectionVersion} is missing exact complete contribution record provenance`, ); } const shippedBaselines = parseShippedBaselineExclusions(record); const sectionShippedBaselines = parseShippedBaselineExclusions(section); if ( formatShippedBaselineExclusions(sectionShippedBaselines) !== formatShippedBaselineExclusions(shippedBaselines) ) { throw new Error( "shipped baseline exclusions must appear inside the complete contribution record", ); } if (!isAncestor(base, recordedTarget)) { throw new Error( `CHANGELOG.md contribution record base ${base} is not an ancestor of recorded target ${recordedTarget}`, ); } // The record is generated before its own changelog/finalization commit. Require // reachability so the tag can contain that bounded release-only follow-up. if (!isAncestor(recordedTarget, targetSha)) { throw new Error( `CHANGELOG.md contribution record target ${recordedTarget} is not reachable from release tag ${targetSha}`, ); } // The verifier persists associated and text-linked PR exclusions together. // Revalidate that exact inventory here instead of rediscovering a narrower set from git text. const excludedPullRequests = new Set(); for (const baseline of shippedBaselines) { const loaded = loadShippedBaseline(baseline.ref); if (!(loaded.pullRequests instanceof Set)) { throw new Error(`shipped baseline ${baseline.ref} did not provide a PR inventory`); } const duplicateExclusions = baseline.pullRequests.filter((number) => excludedPullRequests.has(number), ); if (duplicateExclusions.length > 0) { throw new Error( `release contribution record repeats shipped PR exclusions across baselines: ${duplicateExclusions.map((number) => `#${number}`).join(", ")}`, ); } const absent = baseline.pullRequests.filter((number) => !loaded.pullRequests.has(number)); if (absent.length > 0) { throw new Error( `release contribution record lists PRs absent from shipped baseline ${baseline.ref}: ${absent.map((number) => `#${number}`).join(", ")}`, ); } const retained = [...recordedPullRequests].filter((number) => loaded.pullRequests.has(number)); if (retained.length > 0) { throw new Error( `release contribution record still contains shipped PRs from ${baseline.ref}: ${retained.map((number) => `#${number}`).join(", ")}`, ); } for (const number of baseline.pullRequests) { excludedPullRequests.add(number); } } return { status: "passed", base, target: recordedTarget, shippedBaselines }; } async function runArtifacts(repo, runId) { const data = await githubApi(`repos/${repo}/actions/runs/${runId}/artifacts?per_page=100`); return (data.artifacts ?? []).map((artifact) => ({ name: artifact.name, expired: artifact.expired, })); } /** * Chooses the expected artifact name, allowing one same-prefix fallback per run. */ export function resolveArtifactName(artifacts, preferredName, prefix) { const available = artifacts .filter((artifact) => artifact.expired !== true) .map((artifact) => artifact.name); if (available.includes(preferredName)) { return preferredName; } const candidates = available.filter((name) => name.startsWith(prefix)); if (candidates.length === 1) { console.warn(`artifact ${preferredName} not found; using ${candidates[0]} from the same run`); return candidates[0]; } const candidateList = available.length > 0 ? available.map((name) => `- ${name}`).join("\n") : "- "; throw new Error( `artifact ${preferredName} not found in run. Expected ${preferredName} or exactly one ${prefix}* fallback.\nAvailable artifacts:\n${candidateList}`, ); } async function resolveRunArtifactName(repo, runId, preferredName, prefix) { return resolveArtifactName(await runArtifacts(repo, runId), preferredName, prefix); } function runAndEcho(command, args) { const result = spawnSync(command, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], }); if (result.stdout) { process.stdout.write(result.stdout); } if (result.stderr) { process.stderr.write(result.stderr); } if (result.status !== 0) { throw new Error( `${command} ${args.join(" ")} failed with ${result.status ?? result.signal}\n${ result.stderr ?? "" }`, ); } return `${result.stdout ?? ""}${result.stderr ?? ""}`; } function runLocalGeneratedCheckIfNeeded(options) { if (options.skipLocalGeneratedCheck) { return { status: "skipped", reason: "operator skipped --skip-local-generated-check" }; } run("pnpm", ["release:generated:check"]); return { status: "passed", command: "pnpm release:generated:check" }; } /** * Extracts a GitHub Actions run id from gh workflow dispatch output. */ export function parseRunIdFromDispatchOutput(output) { return output.match(/actions\/runs\/([0-9]+)/u)?.[1] ?? ""; } export function requireRunIdFromDispatchOutput(output, workflowFile) { const runId = parseRunIdFromDispatchOutput(output); if (!runId) { throw new Error( `gh workflow run ${workflowFile} did not return an Actions run URL; refusing to guess from recent workflow_dispatch runs`, ); } return runId; } async function wait(ms) { await new Promise((resolve) => { setTimeout(resolve, ms); }); } function dispatchWorkflow(repo, workflowFile, workflowRef, fields) { const args = ["workflow", "run", workflowFile, "--repo", repo, "--ref", workflowRef]; for (const [key, value] of Object.entries(fields)) { args.push("-f", `${key}=${String(value)}`); } return requireRunIdFromDispatchOutput(runAndEcho("gh", args), workflowFile); } async function runInfo(repo, runId) { const [runData, jobsData] = await Promise.all([ githubApi(`repos/${repo}/actions/runs/${runId}`), githubApi(`repos/${repo}/actions/runs/${runId}/jobs?per_page=100`), ]); return { databaseId: runData.id, runAttempt: runData.run_attempt, workflowName: runData.name, workflowPath: runData.path, repository: runData.repository?.full_name, headBranch: runData.head_branch, headSha: runData.head_sha, event: runData.event, status: runData.status, conclusion: runData.conclusion, url: runData.html_url, jobs: (jobsData.jobs ?? []).map((job) => ({ name: job.name, status: job.status, conclusion: job.conclusion, url: job.html_url, })), }; } async function pendingDeployments(repo, runId) { try { return await githubApi(`repos/${repo}/actions/runs/${runId}/pending_deployments`); } catch { return []; } } function summarizePendingDeployments(repo, runId, deployments) { if (!Array.isArray(deployments) || deployments.length === 0) { return ""; } return deployments .map((deployment) => { const environment = deployment.environment ?? {}; return [ `- pending approval: env=${environment.name ?? ""} canApprove=${String(deployment.current_user_can_approve ?? "")}`, ` approve: gh api -X POST repos/${repo}/actions/runs/${runId}/pending_deployments -F 'environment_ids[]=${environment.id ?? ""}' -f state=approved -f comment='Approve release gate'`, ].join("\n"); }) .join("\n"); } function summarizeFailedRun(info) { const failedJobs = (info.jobs ?? []).filter( (job) => job.conclusion && job.conclusion !== "success" && job.conclusion !== "skipped", ); return [ `${info.workflowName} ${info.databaseId} ended ${info.status}/${info.conclusion}: ${info.url}`, ...failedJobs.map((job) => `- ${job.name}: ${job.conclusion} ${job.url ?? ""}`), ].join("\n"); } async function waitForSuccessfulRun(repo, runId, expected) { let lastState = ""; for (;;) { const info = await runInfo(repo, runId); const state = `${info.status}:${info.conclusion ?? ""}`; if (state !== lastState) { console.log( `${info.workflowName} ${runId}: ${info.status}${info.conclusion ? `/${info.conclusion}` : ""} ${info.url}`, ); const pending = summarizePendingDeployments( repo, runId, await pendingDeployments(repo, runId), ); if (pending) { console.log(pending); } lastState = state; } if (info.status === "completed") { if (info.conclusion !== "success") { throw new Error(summarizeFailedRun(info)); } if (info.workflowName !== expected.workflowName) { throw new Error( `run ${runId} workflow mismatch: expected ${expected.workflowName}, got ${info.workflowName}`, ); } const acceptsPinnedWorkflow = expected.allowShaPinnedWorkflowRef && isShaPinnedReleaseValidationBranch(info.headBranch); if (info.headBranch !== expected.workflowRef && !acceptsPinnedWorkflow) { throw new Error( `run ${runId} branch mismatch: expected ${expected.workflowRef}, got ${info.headBranch}`, ); } return info; } await wait(30_000); } } function downloadArtifact(repo, runId, name, dir) { rmSync(dir, { force: true, recursive: true }); mkdirSync(dir, { recursive: true }); run("gh", ["run", "download", runId, "--repo", repo, "--name", name, "--dir", dir]); } async function downloadResolvedArtifact(repo, runId, preferredName, prefix, dir) { const name = await resolveRunArtifactName(repo, runId, preferredName, prefix); downloadArtifact(repo, runId, name, dir); return name; } function sha256(path) { return run("shasum", ["-a", "256", path], { capture: true }).trim().split(/\s+/u)[0] ?? ""; } function pluginPlanArgs(options) { const args = ["--selection-mode", options.pluginPublishScope]; if (options.pluginPublishScope === "selected") { args.push("--plugins", options.plugins); } return args; } function collectPluginPlan(script, options) { return JSON.parse( run("node", ["--import", "tsx", script, ...pluginPlanArgs(options)], { capture: true }), ); } async function collectPluginPlanWithRetry(script, options) { let lastError; for (let attempt = 1; attempt <= 3; attempt += 1) { try { return collectPluginPlan(script, options); } catch (error) { lastError = error; if (attempt === 3) { break; } console.warn( `${script} failed on attempt ${attempt}; retrying: ${ error instanceof Error ? error.message : String(error) }`, ); await wait(5_000 * attempt); } } throw lastError; } function shellQuote(value) { return `'${String(value).replace(/'/gu, "'\\''")}'`; } /** * Builds the final release publish workflow command once validation evidence is ready. */ export function buildPublishCommand(options) { const workflowRef = options.tag.includes("-alpha.") ? options.workflowRef : "main"; if (options.tag.includes("-alpha.") && !TIDECLAW_ALPHA_WORKFLOW_REF_PATTERN.test(workflowRef)) { throw new Error( "alpha release publish requires a matching tideclaw/alpha/YYYY-MM-DD-HHMMZ workflow ref", ); } const fields = [ ["tag", options.tag], ["preflight_run_id", options.npmPreflightRunId], ["full_release_validation_run_id", options.fullReleaseRunId], ["full_release_validation_run_attempt", options.fullReleaseRunAttempt], ["npm_dist_tag", options.npmDistTag], ["plugin_publish_scope", options.pluginPublishScope], ["publish_openclaw_npm", "true"], ["release_profile", "from-validation"], ["wait_for_clawhub", "false"], ]; if (options.npmTelegramRunId) { fields.push(["npm_telegram_run_id", options.npmTelegramRunId]); } if (options.windowsNodeTag) { fields.push(["windows_node_tag", options.windowsNodeTag]); } if (options.windowsNodeInstallerDigests) { fields.push(["windows_node_installer_digests", options.windowsNodeInstallerDigests]); } if (options.plugins.trim()) { fields.push(["plugins", options.plugins]); } return [ "gh", "workflow", "run", "openclaw-release-publish.yml", "--repo", options.repo, "--ref", workflowRef, ...fields.flatMap(([key, value]) => ["-f", `${key}=${value}`]), ] .map(shellQuote) .join(" "); } export function validatePreflightManifest(manifest, params) { if (manifest.releaseTag !== params.tag) { throw new Error( `npm preflight tag mismatch: expected ${params.tag}, got ${manifest.releaseTag}`, ); } if (manifest.releaseSha !== params.targetSha) { throw new Error( `npm preflight SHA mismatch: expected ${params.targetSha}, got ${manifest.releaseSha}`, ); } if (manifest.npmDistTag !== params.npmDistTag) { throw new Error( `npm preflight dist-tag mismatch: expected ${params.npmDistTag}, got ${manifest.npmDistTag}`, ); } if (!manifest.tarballName || !manifest.tarballSha256) { throw new Error("npm preflight manifest missing tarball metadata"); } if (!Array.isArray(manifest.dependencyTarballs)) { throw new Error("npm preflight manifest missing dependency tarball metadata"); } for (const dependency of manifest.dependencyTarballs) { if ( !dependency?.packageName || !dependency.packageVersion || !dependency.tarballName || !dependency.tarballSha256 || dependency.tarballName !== basename(dependency.tarballName) ) { throw new Error("npm preflight manifest contains invalid dependency tarball metadata"); } } } export function validateFullManifest(manifest, params) { if (manifest.workflowName !== "Full Release Validation") { throw new Error(`full validation workflow mismatch: ${manifest.workflowName}`); } if (manifest.targetSha !== params.targetSha) { throw new Error( `full validation SHA mismatch: expected ${params.targetSha}, got ${manifest.targetSha}`, ); } if (manifest.releaseProfile !== params.releaseProfile) { throw new Error( `full validation profile mismatch: expected ${params.releaseProfile}, got ${manifest.releaseProfile}`, ); } if (manifest.rerunGroup !== "all") { throw new Error(`full validation must use rerun_group=all, got ${manifest.rerunGroup}`); } if ( (params.releaseProfile === "stable" || params.releaseProfile === "full") && manifest.runReleaseSoak !== "true" ) { throw new Error( `full validation must record runReleaseSoak=true for ${params.releaseProfile} release candidates`, ); } if (manifest.controls?.performanceBlocking !== true) { throw new Error("full validation manifest must record blocking product performance evidence"); } } export function candidateParallelsArgs(tarballPath, dependencyTarballPaths = []) { return [ "test:parallels:npm-update", "--", "--target-tarball", tarballPath, ...dependencyTarballPaths.flatMap((dependency) => ["--dependency-tarball", dependency]), "--json", ]; } export function candidateParallelsShellCommand( tarballPath, timeoutBin, dependencyTarballPaths = [], ) { return [ 'set -a; source "$HOME/.profile" >/dev/null 2>&1 || true; set +a;', "exec", shellQuote(timeoutBin), "--foreground", "150m", "pnpm", ...candidateParallelsArgs(tarballPath, dependencyTarballPaths).map(shellQuote), ].join(" "); } async function runParallelsIfNeeded(options, tarballPath, dependencyTarballPaths) { if (options.skipParallels) { return { status: "skipped", reason: "operator skipped --skip-parallels" }; } const timeoutBin = run("bash", ["-lc", "command -v gtimeout || command -v timeout"], { capture: true, }).trim(); const command = candidateParallelsShellCommand(tarballPath, timeoutBin, dependencyTarballPaths); run("bash", ["-lc", command]); return { status: "passed", command, }; } async function runTelegramIfNeeded(options, artifactName) { if (options.skipTelegram) { return { status: "skipped" }; } const workflowFile = "npm-telegram-beta-e2e.yml"; const runId = dispatchWorkflow(options.repo, workflowFile, options.workflowRef, { package_spec: `openclaw@${options.tag.replace(/^v/u, "")}`, package_label: options.tag, package_artifact_name: artifactName, package_artifact_run_id: options.npmPreflightRunId, harness_ref: options.workflowRef, provider_mode: options.telegramProviderMode, }); const runLocal = await waitForSuccessfulRun(options.repo, runId, { workflowName: "NPM Telegram Beta E2E", workflowRef: options.workflowRef, }); return { status: "passed", runId, url: runLocal.url, artifactName, providerMode: options.telegramProviderMode, }; } async function main() { const options = parseArgs(process.argv.slice(2)); const targetRoot = gitTopLevel(process.cwd()); const toolingRoot = gitTopLevel(TOOLING_ROOT); if (targetRoot === toolingRoot) { runFromTrustedTooling(process.argv.slice(2), { targetRoot, workflowRef: options.workflowRef, }); return; } options.outputDir ||= join(".artifacts", "release-candidate", options.tag); const targetSha = gitRevParse(`${options.tag}^{}`, targetRoot); const toolingSha = gitRevParse("HEAD", TOOLING_ROOT); const trustedToolingSha = fetchTrustedWorkflowSha(options.workflowRef, TOOLING_ROOT); validateCandidateCheckout({ targetSha, targetHeadSha: gitRevParse("HEAD", targetRoot), targetTrackedStatus: gitTrackedStatus(targetRoot), toolingSha, trustedToolingSha, toolingTrackedStatus: gitTrackedStatus(TOOLING_ROOT), workflowRef: options.workflowRef, }); const statePath = join(options.outputDir, RELEASE_CANDIDATE_STATE_FILE); const expectedState = buildReleaseCandidateState(options, { targetSha, toolingSha }); let candidateState = reconcileReleaseCandidateState( existsSync(statePath) ? readJson(statePath, "release candidate state") : undefined, expectedState, ); options.fullReleaseRunId = candidateState.fullReleaseRunId; options.npmPreflightRunId = candidateState.npmPreflightRunId; writeReleaseCandidateState(statePath, candidateState); const releaseChangelog = run("git", ["show", `${targetSha}:CHANGELOG.md`], { capture: true }); const releaseNotesVersion = releaseNotesVersionForTag(options.tag); const releaseNotesCheck = validateCandidateReleaseNotes({ changelog: releaseChangelog, repository: options.repo, tag: options.tag, }); const releaseNotesProvenance = validateCandidateChangelogProvenance({ changelog: releaseChangelog, version: releaseNotesVersion, tag: options.tag, targetSha, }); const windowsNodeSourceRelease = options.windowsNodeTag ? await validateWindowsSourceRelease(options.windowsNodeTag) : undefined; options.windowsNodeInstallerDigests = windowsNodeSourceRelease ? JSON.stringify( Object.fromEntries( windowsNodeSourceRelease.assets.map((asset) => [asset.name, asset.digest]), ), ) : ""; const localGeneratedCheck = runLocalGeneratedCheckIfNeeded(options); if (!options.fullReleaseRunId && !options.skipDispatch) { const workflowFile = "full-release-validation.yml"; const targetContextRef = releaseBranchForTag(options.tag); options.fullReleaseRunId = dispatchWorkflow(options.repo, workflowFile, options.workflowRef, { ref: options.tag, ...(targetContextRef ? { target_context_ref: targetContextRef } : {}), provider: options.provider, mode: options.mode, release_profile: options.releaseProfile, run_release_soak: options.releaseProfile === "stable" || options.releaseProfile === "full" ? "true" : "false", rerun_group: "all", }); candidateState = updateReleaseCandidateState(statePath, candidateState, "dispatching", { fullReleaseRunId: options.fullReleaseRunId, }); } if (!options.npmPreflightRunId && !options.skipDispatch) { const workflowFile = "openclaw-npm-release.yml"; options.npmPreflightRunId = dispatchWorkflow(options.repo, workflowFile, options.workflowRef, { tag: options.tag, preflight_only: "true", npm_dist_tag: options.npmDistTag, }); candidateState = updateReleaseCandidateState(statePath, candidateState, "dispatching", { npmPreflightRunId: options.npmPreflightRunId, }); } candidateState = updateReleaseCandidateState(statePath, candidateState, "waiting", { fullReleaseRunId: options.fullReleaseRunId, npmPreflightRunId: options.npmPreflightRunId, }); const fullRun = await waitForSuccessfulRun(options.repo, options.fullReleaseRunId, { workflowName: "Full Release Validation", workflowRef: options.workflowRef, allowShaPinnedWorkflowRef: true, }); const npmRun = await waitForSuccessfulRun(options.repo, options.npmPreflightRunId, { workflowName: "OpenClaw NPM Release", workflowRef: options.workflowRef, }); if (npmRun.headSha !== targetSha) { throw new Error(`run SHA mismatch: tag=${targetSha} npm=${npmRun.headSha}`); } const npmDir = join(options.outputDir, "npm-preflight"); const fullDir = join(options.outputDir, "full-release-validation"); const npmArtifactName = await downloadResolvedArtifact( options.repo, options.npmPreflightRunId, `openclaw-npm-preflight-${options.tag}`, "openclaw-npm-preflight-", npmDir, ); if (!Number.isInteger(fullRun.runAttempt) || fullRun.runAttempt < 1) { throw new Error(`Full Release Validation run ${options.fullReleaseRunId} has invalid attempt.`); } const fullArtifactName = `full-release-validation-${options.fullReleaseRunId}-${fullRun.runAttempt}`; downloadArtifact(options.repo, options.fullReleaseRunId, fullArtifactName, fullDir); const npmManifest = readJson(join(npmDir, "preflight-manifest.json"), "npm preflight manifest"); const fullManifest = readJson( join(fullDir, "full-release-validation-manifest.json"), "full validation manifest", ); run("git", ["fetch", "--no-tags", "origin", "+refs/heads/main:refs/remotes/origin/main"], { capture: true, }); const fullValidationEvidence = validateFullReleaseValidationEvidence({ run: fullRun, manifest: fullManifest, expectedRepository: options.repo, expectedRunId: options.fullReleaseRunId, expectedTargetSha: targetSha, expectedWorkflowBranch: options.workflowRef, isTrustedMainAncestor: (sha) => gitIsAncestor(sha, "refs/remotes/origin/main"), validateEvidenceReuseStrictly: ({ repository, runId }) => runStrictReleaseEvidenceValidation({ repository, runId }), }); if (fullValidationEvidence.source === "direct" && fullRun.headSha !== targetSha) { throw new Error(`run SHA mismatch: tag=${targetSha} full=${fullRun.headSha}`); } validatePreflightManifest(npmManifest, { tag: options.tag, targetSha, npmDistTag: options.npmDistTag, }); validateFullManifest(fullManifest, { targetSha, releaseProfile: options.releaseProfile, }); const tarballPath = join(npmDir, npmManifest.tarballName); if (!existsSync(tarballPath)) { throw new Error(`prepared tarball missing: ${tarballPath}`); } const actualTarballSha = sha256(tarballPath); if (actualTarballSha !== npmManifest.tarballSha256) { throw new Error( `prepared tarball digest mismatch: expected ${npmManifest.tarballSha256}, got ${actualTarballSha}`, ); } const dependencyTarballPaths = npmManifest.dependencyTarballs.map((dependency) => { const dependencyPath = join(npmDir, dependency.tarballName); if (!existsSync(dependencyPath)) { throw new Error(`prepared dependency tarball missing: ${dependencyPath}`); } const actualDependencySha = sha256(dependencyPath); if (actualDependencySha !== dependency.tarballSha256) { throw new Error( `prepared dependency tarball digest mismatch for ${dependency.packageName}: expected ${dependency.tarballSha256}, got ${actualDependencySha}`, ); } return dependencyPath; }); const parallels = await runParallelsIfNeeded(options, tarballPath, dependencyTarballPaths); const npmTelegram = await runTelegramIfNeeded(options, npmArtifactName); options.npmTelegramRunId = npmTelegram.runId ?? ""; const pluginNpmPlan = await collectPluginPlanWithRetry( "scripts/plugin-npm-release-plan.ts", options, ); const pluginClawHubPlan = await collectPluginPlanWithRetry( "scripts/plugin-clawhub-release-plan.ts", options, ); const publishCommand = buildPublishCommand({ ...options, fullReleaseRunAttempt: fullRun.runAttempt, }); const evidence = { version: 1, tag: options.tag, targetSha, workflowRef: options.workflowRef, npmDistTag: options.npmDistTag, fullReleaseValidationRunId: options.fullReleaseRunId, fullReleaseValidationRunAttempt: fullRun.runAttempt, npmPreflightRunId: options.npmPreflightRunId, windowsNodeTag: options.windowsNodeTag || undefined, windowsNodeSourceRelease, fullReleaseValidationUrl: fullRun.url, fullReleaseValidationControls: fullManifest.controls, npmPreflightUrl: npmRun.url, artifacts: { npmPreflight: npmArtifactName, fullReleaseValidation: fullArtifactName, }, releaseNotesCheck, releaseNotesProvenance, localGeneratedCheck, tarball: { name: basename(tarballPath), sha256: actualTarballSha, path: tarballPath, }, parallels, npmTelegram, pluginNpmPlan, pluginClawHubPlan, publishCommand, }; mkdirSync(options.outputDir, { recursive: true }); const evidencePath = join(options.outputDir, "release-candidate-evidence.json"); const evidenceMarkdownPath = join(options.outputDir, "release-candidate-evidence.md"); writeFileSync(evidencePath, `${JSON.stringify(evidence, null, 2)}\n`); writeFileSync( evidenceMarkdownPath, [ `# ${options.tag} release candidate evidence`, "", `- target SHA: ${targetSha}`, `- full release validation: ${options.fullReleaseRunId} ${fullRun.url}`, `- npm preflight: ${options.npmPreflightRunId} ${npmRun.url}`, ...(windowsNodeSourceRelease ? [ `- Windows Node source release: ${windowsNodeSourceRelease.tag} ${windowsNodeSourceRelease.url}`, ...windowsNodeSourceRelease.assets.map( (asset) => `- Windows Node source asset: ${asset.name} ${asset.digest}`, ), ] : []), `- npm preflight artifact: ${npmArtifactName}`, `- full release artifact: ${fullArtifactName}`, `- GitHub release notes: ${releaseNotesCheck.status} (${releaseNotesCheck.mode}, ${releaseNotesCheck.characters} characters, ${releaseNotesCheck.bytes} bytes)`, releaseNotesProvenance.status === "passed" ? `- changelog provenance: passed (${releaseNotesProvenance.base}..${releaseNotesProvenance.target})` : `- changelog provenance: skipped (${releaseNotesProvenance.reason})`, `- ${ formatShippedBaselineExclusions(releaseNotesProvenance.shippedBaselines) || "Shipped baseline exclusions: none" }`, `- local generated release checks: ${localGeneratedCheck.status}${ localGeneratedCheck.reason ? ` (${localGeneratedCheck.reason})` : "" }`, `- tarball: ${basename(tarballPath)}`, `- tarball sha256: ${actualTarballSha}`, `- npm dist-tag: ${options.npmDistTag}`, `- plugin npm plan: ${pluginNpmPlan.packages?.length ?? 0} packages`, `- ClawHub plan: ${pluginClawHubPlan.packages?.length ?? 0} packages`, `- Parallels: ${parallels.status}${parallels.reason ? ` (${parallels.reason})` : ""}`, `- NPM Telegram E2E: ${npmTelegram.status}${ npmTelegram.runId ? ` ${npmTelegram.runId} ${npmTelegram.url}` : "" }`, "", "Publish command:", "", "```bash", publishCommand, "```", "", ].join("\n"), ); updateReleaseCandidateState(statePath, candidateState, "completed"); console.log(`release candidate evidence: ${evidencePath}`); console.log(`release candidate summary: ${evidenceMarkdownPath}`); console.log("publish command:"); console.log(publishCommand); } if (process.argv[1] === fileURLToPath(import.meta.url)) { await main().catch( /** @param {unknown} error */ (error) => { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); }, ); }