Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
929
scripts/release-candidate-checklist.mjs
Normal file
929
scripts/release-candidate-checklist.mjs
Normal file
@@ -0,0 +1,929 @@
|
||||
#!/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, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { basename, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { stripLeadingPackageManagerSeparator } from "./lib/arg-utils.mjs";
|
||||
import { readBoundedResponseText } from "./lib/bounded-response.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 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;
|
||||
|
||||
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 <tag> Release tag to validate.
|
||||
--workflow-ref <ref> Workflow branch/ref. Default: current branch.
|
||||
--repo <owner/repo> GitHub repo. Default: ${DEFAULT_REPO}
|
||||
--full-release-run <id> Reuse successful Full Release Validation run.
|
||||
--npm-preflight-run <id> Reuse successful OpenClaw NPM Release preflight run.
|
||||
--windows-node-tag <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 <mode> mock-openai|live-frontier. Default: ${DEFAULT_TELEGRAM_PROVIDER_MODE}
|
||||
--provider <provider> Full validation provider. Default: ${DEFAULT_PROVIDER}
|
||||
--mode <fresh|upgrade|both> Full validation cross-OS mode. Default: ${DEFAULT_MODE}
|
||||
--release-profile <beta|stable|full> Default: beta for prereleases; stable otherwise.
|
||||
--npm-dist-tag <alpha|beta|latest> Default: ${DEFAULT_NPM_DIST_TAG}
|
||||
--plugin-publish-scope <scope> selected|all-publishable. Default: ${DEFAULT_PLUGIN_SCOPE}
|
||||
--plugins <names> Required when plugin scope is selected.
|
||||
--output-dir <dir> Evidence output dir. Default: .artifacts/release-candidate/<tag>
|
||||
`;
|
||||
}
|
||||
|
||||
function requireValue(argv, index, flag) {
|
||||
const value = argv[index];
|
||||
if (!value || value.startsWith("-")) {
|
||||
throw new Error(`${flag} requires a value`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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");
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
function run(command, args, options = {}) {
|
||||
const result = spawnSync(command, args, {
|
||||
cwd: options.cwd,
|
||||
encoding: "utf8",
|
||||
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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 currentBranch() {
|
||||
return run("git", ["branch", "--show-current"], { capture: true }).trim();
|
||||
}
|
||||
|
||||
function gitRevParse(ref) {
|
||||
return run("git", ["rev-parse", ref], { capture: true }).trim();
|
||||
}
|
||||
|
||||
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") : "- <none>";
|
||||
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,
|
||||
workflowName: runData.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 ?? "<unknown>"} canApprove=${String(deployment.current_user_can_approve ?? "<unknown>")}`,
|
||||
` approve: gh api -X POST repos/${repo}/actions/runs/${runId}/pending_deployments -F 'environment_ids[]=${environment.id ?? "<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}`,
|
||||
);
|
||||
}
|
||||
if (info.headBranch !== expected.workflowRef) {
|
||||
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 fields = [
|
||||
["tag", options.tag],
|
||||
["preflight_run_id", options.npmPreflightRunId],
|
||||
["full_release_validation_run_id", options.fullReleaseRunId],
|
||||
["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",
|
||||
options.workflowRef,
|
||||
...fields.flatMap(([key, value]) => ["-f", `${key}=${value}`]),
|
||||
]
|
||||
.map(shellQuote)
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
return ["test:parallels:npm-update", "--", "--target-tarball", tarballPath, "--json"];
|
||||
}
|
||||
|
||||
export function candidateParallelsShellCommand(tarballPath, timeoutBin) {
|
||||
return [
|
||||
'set -a; source "$HOME/.profile" >/dev/null 2>&1 || true; set +a;',
|
||||
"exec",
|
||||
shellQuote(timeoutBin),
|
||||
"--foreground",
|
||||
"150m",
|
||||
"pnpm",
|
||||
...candidateParallelsArgs(tarballPath).map(shellQuote),
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
async function runParallelsIfNeeded(options, tarballPath) {
|
||||
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);
|
||||
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));
|
||||
options.workflowRef ||= currentBranch();
|
||||
options.outputDir ||= join(".artifacts", "release-candidate", options.tag);
|
||||
const targetSha = gitRevParse(`${options.tag}^{}`);
|
||||
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";
|
||||
options.fullReleaseRunId = dispatchWorkflow(options.repo, workflowFile, options.workflowRef, {
|
||||
ref: options.tag,
|
||||
provider: options.provider,
|
||||
mode: options.mode,
|
||||
release_profile: options.releaseProfile,
|
||||
run_release_soak:
|
||||
options.releaseProfile === "stable" || options.releaseProfile === "full" ? "true" : "false",
|
||||
rerun_group: "all",
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
const fullRun = await waitForSuccessfulRun(options.repo, options.fullReleaseRunId, {
|
||||
workflowName: "Full Release Validation",
|
||||
workflowRef: options.workflowRef,
|
||||
});
|
||||
const npmRun = await waitForSuccessfulRun(options.repo, options.npmPreflightRunId, {
|
||||
workflowName: "OpenClaw NPM Release",
|
||||
workflowRef: options.workflowRef,
|
||||
});
|
||||
if (fullRun.headSha !== targetSha || npmRun.headSha !== targetSha) {
|
||||
throw new Error(
|
||||
`run SHA mismatch: tag=${targetSha} full=${fullRun.headSha} 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,
|
||||
);
|
||||
const fullArtifactName = await downloadResolvedArtifact(
|
||||
options.repo,
|
||||
options.fullReleaseRunId,
|
||||
`full-release-validation-${options.fullReleaseRunId}`,
|
||||
"full-release-validation-",
|
||||
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",
|
||||
);
|
||||
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 parallels = await runParallelsIfNeeded(options, tarballPath);
|
||||
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);
|
||||
const evidence = {
|
||||
version: 1,
|
||||
tag: options.tag,
|
||||
targetSha,
|
||||
workflowRef: options.workflowRef,
|
||||
npmDistTag: options.npmDistTag,
|
||||
fullReleaseValidationRunId: options.fullReleaseRunId,
|
||||
npmPreflightRunId: options.npmPreflightRunId,
|
||||
windowsNodeTag: options.windowsNodeTag || undefined,
|
||||
windowsNodeSourceRelease,
|
||||
fullReleaseValidationUrl: fullRun.url,
|
||||
fullReleaseValidationControls: fullManifest.controls,
|
||||
npmPreflightUrl: npmRun.url,
|
||||
artifacts: {
|
||||
npmPreflight: npmArtifactName,
|
||||
fullReleaseValidation: fullArtifactName,
|
||||
},
|
||||
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}`,
|
||||
`- 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"),
|
||||
);
|
||||
|
||||
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);
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user