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:
730
scripts/lib/release-beta-verifier.ts
Normal file
730
scripts/lib/release-beta-verifier.ts
Normal file
@@ -0,0 +1,730 @@
|
||||
// Release Beta Verifier script supports OpenClaw repository automation.
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { readBoundedResponseText } from "./bounded-response.ts";
|
||||
import { collectClawHubPublishablePluginPackages } from "./plugin-clawhub-release.ts";
|
||||
import {
|
||||
collectPublishablePluginPackages,
|
||||
parsePluginReleaseSelection,
|
||||
} from "./plugin-npm-release.ts";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
export type ReleaseVerifyBetaArgs = {
|
||||
version: string;
|
||||
tag: string;
|
||||
distTag: string;
|
||||
repo: string;
|
||||
registry: string;
|
||||
workflowRef?: string;
|
||||
clawHubWorkflowRef?: string;
|
||||
pluginSelection: string[];
|
||||
evidenceOut?: string;
|
||||
skipPostpublish: boolean;
|
||||
skipGitHubRelease: boolean;
|
||||
skipClawHub: boolean;
|
||||
rerunFailedClawHub: boolean;
|
||||
workflowRuns: {
|
||||
fullReleaseValidation?: string;
|
||||
openclawNpm?: string;
|
||||
pluginNpm?: string;
|
||||
pluginClawHub?: string;
|
||||
pluginClawHubBootstrap?: string;
|
||||
npmTelegram?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type NpmViewFields = {
|
||||
version?: string;
|
||||
distTagVersion?: string;
|
||||
integrity?: string;
|
||||
tarball?: string;
|
||||
};
|
||||
|
||||
type FetchWithRetryResult = {
|
||||
response: Response;
|
||||
signal: AbortSignal;
|
||||
};
|
||||
|
||||
type WorkflowRunSummary = {
|
||||
id: string;
|
||||
label: string;
|
||||
url?: string;
|
||||
durationSeconds?: number;
|
||||
};
|
||||
|
||||
const DEFAULT_REPO = "openclaw/openclaw";
|
||||
const DEFAULT_CLAWHUB_REGISTRY = "https://clawhub.ai";
|
||||
const CLAWHUB_REQUEST_TIMEOUT_MS = 20_000;
|
||||
const CLAWHUB_RESPONSE_BODY_MAX_BYTES = 1024 * 1024;
|
||||
// Trusted publish can finish before npm registry metadata converges. Keep the
|
||||
// verifier on the same release train instead of forcing a republish/correction.
|
||||
const NPM_VIEW_ATTEMPTS = 30;
|
||||
const NPM_VIEW_RETRY_MAX_DELAY_MS = 10_000;
|
||||
|
||||
function isRecord(value: unknown): value is JsonRecord {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function readString(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
function requireString(value: unknown, label: string): string {
|
||||
const stringValue = readString(value);
|
||||
if (stringValue === undefined) {
|
||||
throw new Error(`${label} is missing.`);
|
||||
}
|
||||
return stringValue;
|
||||
}
|
||||
|
||||
function runCommand(command: string, args: string[], options: { cwd?: string } = {}): string {
|
||||
return execFileSync(command, args, {
|
||||
cwd: options.cwd,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
}).trim();
|
||||
}
|
||||
|
||||
function runCommandInherited(command: string, args: string[]): void {
|
||||
execFileSync(command, args, {
|
||||
stdio: "inherit",
|
||||
});
|
||||
}
|
||||
|
||||
export async function runNpmViewWithRetry(
|
||||
args: string[],
|
||||
options: {
|
||||
attempts?: number;
|
||||
delay?: (delayMs: number) => Promise<void>;
|
||||
run?: (args: string[]) => string;
|
||||
} = {},
|
||||
): Promise<string> {
|
||||
const attempts = options.attempts ?? NPM_VIEW_ATTEMPTS;
|
||||
const delay =
|
||||
options.delay ??
|
||||
((delayMs: number) =>
|
||||
new Promise((resolveDelay) => {
|
||||
setTimeout(resolveDelay, delayMs);
|
||||
}));
|
||||
const run = options.run ?? ((npmArgs: string[]) => runCommand("npm", npmArgs));
|
||||
let lastError: unknown;
|
||||
|
||||
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
||||
try {
|
||||
return run([...args, "--prefer-online"]);
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
}
|
||||
if (attempt < attempts) {
|
||||
await delay(Math.min(attempt * 1000, NPM_VIEW_RETRY_MAX_DELAY_MS));
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
function parseJson(raw: string, label: string): unknown {
|
||||
try {
|
||||
return JSON.parse(raw) as unknown;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`${label} returned invalid JSON: ${message}`, { cause: error });
|
||||
}
|
||||
}
|
||||
|
||||
export function parseNpmViewFields(raw: string, distTag: string): NpmViewFields {
|
||||
const parsed = parseJson(raw, "npm view");
|
||||
if (Array.isArray(parsed)) {
|
||||
return {
|
||||
version: readString(parsed[0]),
|
||||
distTagVersion: readString(parsed[1]),
|
||||
integrity: readString(parsed[2]),
|
||||
tarball: readString(parsed[3]),
|
||||
};
|
||||
}
|
||||
if (!isRecord(parsed)) {
|
||||
throw new Error("npm view returned an unsupported JSON shape.");
|
||||
}
|
||||
const distTags = isRecord(parsed["dist-tags"]) ? parsed["dist-tags"] : undefined;
|
||||
const dist = isRecord(parsed.dist) ? parsed.dist : undefined;
|
||||
return {
|
||||
version: readString(parsed.version),
|
||||
distTagVersion: readString(parsed[`dist-tags.${distTag}`]) ?? readString(distTags?.[distTag]),
|
||||
integrity: readString(parsed["dist.integrity"]) ?? readString(dist?.integrity),
|
||||
tarball: readString(parsed["dist.tarball"]) ?? readString(dist?.tarball),
|
||||
};
|
||||
}
|
||||
|
||||
export function parseReleaseVerifyBetaArgs(argv: string[]): ReleaseVerifyBetaArgs {
|
||||
const values = [...argv];
|
||||
if (values[0] === "--") {
|
||||
values.shift();
|
||||
}
|
||||
const version = values.shift();
|
||||
if (!version || version.startsWith("-")) {
|
||||
throw new Error(
|
||||
"Usage: pnpm release:verify-beta -- <version> [--workflow-ref REF] [--clawhub-workflow-ref REF] [--full-release-validation-run ID] [--openclaw-npm-run ID] [--plugin-npm-run ID] [--plugin-clawhub-run ID] [--plugin-clawhub-bootstrap-run ID] [--npm-telegram-run ID] [--skip-github-release] [--skip-clawhub]",
|
||||
);
|
||||
}
|
||||
|
||||
const parsed: ReleaseVerifyBetaArgs = {
|
||||
version,
|
||||
tag: `v${version}`,
|
||||
distTag: "beta",
|
||||
repo: DEFAULT_REPO,
|
||||
registry: DEFAULT_CLAWHUB_REGISTRY,
|
||||
workflowRef: undefined,
|
||||
clawHubWorkflowRef: undefined,
|
||||
pluginSelection: [],
|
||||
evidenceOut: undefined,
|
||||
skipPostpublish: false,
|
||||
skipGitHubRelease: false,
|
||||
skipClawHub: false,
|
||||
rerunFailedClawHub: false,
|
||||
workflowRuns: {},
|
||||
};
|
||||
|
||||
for (let index = 0; index < values.length; index += 1) {
|
||||
const arg = values[index];
|
||||
const next = () => {
|
||||
const value = values[index + 1];
|
||||
if (value === undefined || value.startsWith("-")) {
|
||||
throw new Error(`${arg} requires a value.`);
|
||||
}
|
||||
index += 1;
|
||||
return value;
|
||||
};
|
||||
|
||||
switch (arg) {
|
||||
case "--tag":
|
||||
parsed.tag = next();
|
||||
break;
|
||||
case "--dist-tag":
|
||||
parsed.distTag = next();
|
||||
break;
|
||||
case "--repo":
|
||||
parsed.repo = next();
|
||||
break;
|
||||
case "--registry":
|
||||
parsed.registry = next();
|
||||
break;
|
||||
case "--workflow-ref":
|
||||
parsed.workflowRef = next();
|
||||
break;
|
||||
case "--clawhub-workflow-ref":
|
||||
parsed.clawHubWorkflowRef = next();
|
||||
break;
|
||||
case "--plugins":
|
||||
parsed.pluginSelection = parsePluginReleaseSelection(next());
|
||||
if (parsed.pluginSelection.length === 0) {
|
||||
throw new Error("--plugins requires at least one plugin package name.");
|
||||
}
|
||||
break;
|
||||
case "--evidence-out":
|
||||
parsed.evidenceOut = next();
|
||||
break;
|
||||
case "--full-release-validation-run":
|
||||
parsed.workflowRuns.fullReleaseValidation = next();
|
||||
break;
|
||||
case "--openclaw-npm-run":
|
||||
parsed.workflowRuns.openclawNpm = next();
|
||||
break;
|
||||
case "--plugin-npm-run":
|
||||
parsed.workflowRuns.pluginNpm = next();
|
||||
break;
|
||||
case "--plugin-clawhub-run":
|
||||
parsed.workflowRuns.pluginClawHub = next();
|
||||
break;
|
||||
case "--plugin-clawhub-bootstrap-run":
|
||||
parsed.workflowRuns.pluginClawHubBootstrap = next();
|
||||
break;
|
||||
case "--npm-telegram-run":
|
||||
parsed.workflowRuns.npmTelegram = next();
|
||||
break;
|
||||
case "--skip-postpublish":
|
||||
parsed.skipPostpublish = true;
|
||||
break;
|
||||
case "--skip-github-release":
|
||||
parsed.skipGitHubRelease = true;
|
||||
break;
|
||||
case "--skip-clawhub":
|
||||
parsed.skipClawHub = true;
|
||||
break;
|
||||
case "--rerun-failed-clawhub":
|
||||
parsed.rerunFailedClawHub = true;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
async function fetchWithRetry(
|
||||
url: string,
|
||||
options: RequestInit,
|
||||
attempts: number,
|
||||
): Promise<FetchWithRetryResult> {
|
||||
let lastError: unknown;
|
||||
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
||||
try {
|
||||
const signal = AbortSignal.timeout(CLAWHUB_REQUEST_TIMEOUT_MS);
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
signal,
|
||||
});
|
||||
if (response.status !== 429 && response.status < 500) {
|
||||
return { response, signal };
|
||||
}
|
||||
await cancelResponseBody(response);
|
||||
lastError = new Error(`HTTP ${response.status}`);
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
}
|
||||
if (attempt < attempts) {
|
||||
await new Promise((resolveDelay) => {
|
||||
setTimeout(resolveDelay, attempt * 1000);
|
||||
});
|
||||
}
|
||||
}
|
||||
const message = lastError instanceof Error ? lastError.message : String(lastError);
|
||||
throw new Error(`${url} did not return a stable response: ${message}`);
|
||||
}
|
||||
|
||||
async function cancelResponseBody(response: Response): Promise<void> {
|
||||
await response.body?.cancel().catch(() => undefined);
|
||||
}
|
||||
|
||||
async function fetchJsonWithRetry(url: string): Promise<unknown> {
|
||||
const { response, signal } = await fetchWithRetry(
|
||||
url,
|
||||
{ headers: { accept: "application/json" } },
|
||||
5,
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(`${url} returned HTTP ${response.status}.`);
|
||||
}
|
||||
return await readBoundedJsonResponse(response, url, undefined, { signal });
|
||||
}
|
||||
|
||||
export async function readBoundedJsonResponse(
|
||||
response: Response,
|
||||
label: string,
|
||||
maxBytes = CLAWHUB_RESPONSE_BODY_MAX_BYTES,
|
||||
options: { signal?: AbortSignal } = {},
|
||||
): Promise<unknown> {
|
||||
return parseJson(await readBoundedResponseText(response, label, maxBytes, options), label);
|
||||
}
|
||||
|
||||
export async function fetchStatusWithRetry(url: string, method: "GET" | "HEAD"): Promise<number> {
|
||||
const { response } = await fetchWithRetry(url, { method, redirect: "manual" }, 5);
|
||||
try {
|
||||
return response.status;
|
||||
} finally {
|
||||
await cancelResponseBody(response);
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyNpmPackage(
|
||||
packageName: string,
|
||||
version: string,
|
||||
distTag: string,
|
||||
): Promise<NpmViewFields> {
|
||||
const raw = await runNpmViewWithRetry([
|
||||
"view",
|
||||
`${packageName}@${version}`,
|
||||
"version",
|
||||
`dist-tags.${distTag}`,
|
||||
"dist.integrity",
|
||||
"dist.tarball",
|
||||
"--json",
|
||||
]);
|
||||
const fields = parseNpmViewFields(raw, distTag);
|
||||
if (fields.version !== version) {
|
||||
throw new Error(
|
||||
`${packageName}: expected npm version ${version}, got ${fields.version ?? "<missing>"}.`,
|
||||
);
|
||||
}
|
||||
if (fields.distTagVersion !== version) {
|
||||
throw new Error(
|
||||
`${packageName}: npm dist-tag ${distTag} points to ${fields.distTagVersion ?? "<missing>"}, expected ${version}.`,
|
||||
);
|
||||
}
|
||||
if (fields.integrity === undefined) {
|
||||
throw new Error(`${packageName}: npm dist.integrity missing for ${version}.`);
|
||||
}
|
||||
if (fields.tarball === undefined) {
|
||||
throw new Error(`${packageName}: npm dist.tarball missing for ${version}.`);
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
function readClawHubTags(detail: unknown): Record<string, string> {
|
||||
if (!isRecord(detail)) {
|
||||
return {};
|
||||
}
|
||||
const packageDetail = isRecord(detail.package) ? detail.package : undefined;
|
||||
const tags = isRecord(packageDetail?.tags) ? packageDetail.tags : undefined;
|
||||
const result: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(tags ?? {})) {
|
||||
if (typeof value === "string") {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function verifyClawHubPackage(params: {
|
||||
registry: string;
|
||||
packageName: string;
|
||||
version: string;
|
||||
distTag: string;
|
||||
}): Promise<void> {
|
||||
const base = params.registry.replace(/\/+$/u, "");
|
||||
const encodedName = encodeURIComponent(params.packageName);
|
||||
const encodedVersion = encodeURIComponent(params.version);
|
||||
const detailUrl = `${base}/api/v1/packages/${encodedName}`;
|
||||
const versionUrl = `${detailUrl}/versions/${encodedVersion}`;
|
||||
const artifactUrl = `${versionUrl}/artifact/download`;
|
||||
|
||||
const detail = await fetchJsonWithRetry(detailUrl);
|
||||
const tags = readClawHubTags(detail);
|
||||
if (tags[params.distTag] !== params.version) {
|
||||
throw new Error(
|
||||
`${params.packageName}: ClawHub tag ${params.distTag} points to ${tags[params.distTag] ?? "<missing>"}, expected ${params.version}.`,
|
||||
);
|
||||
}
|
||||
|
||||
const versionStatus = await fetchStatusWithRetry(versionUrl, "GET");
|
||||
if (versionStatus < 200 || versionStatus >= 300) {
|
||||
throw new Error(`${params.packageName}: ClawHub exact version returned HTTP ${versionStatus}.`);
|
||||
}
|
||||
|
||||
const artifactStatus = await fetchStatusWithRetry(artifactUrl, "HEAD");
|
||||
if (artifactStatus < 200 || artifactStatus >= 400) {
|
||||
throw new Error(`${params.packageName}: ClawHub artifact returned HTTP ${artifactStatus}.`);
|
||||
}
|
||||
}
|
||||
|
||||
function verifyGitHubRelease(params: ReleaseVerifyBetaArgs): string {
|
||||
const raw = runCommand("gh", [
|
||||
"release",
|
||||
"view",
|
||||
params.tag,
|
||||
"--repo",
|
||||
params.repo,
|
||||
"--json",
|
||||
"tagName,isPrerelease,url",
|
||||
]);
|
||||
const release = parseJson(raw, "gh release view");
|
||||
if (!isRecord(release)) {
|
||||
throw new Error("GitHub release returned an unsupported JSON shape.");
|
||||
}
|
||||
if (release.tagName !== params.tag) {
|
||||
throw new Error(
|
||||
`GitHub release tag mismatch: expected ${params.tag}, got ${String(release.tagName)}.`,
|
||||
);
|
||||
}
|
||||
if (params.version.includes("-beta.") && release.isPrerelease !== true) {
|
||||
throw new Error(`${params.tag} is not marked as a GitHub prerelease.`);
|
||||
}
|
||||
return requireString(release.url, "GitHub release URL");
|
||||
}
|
||||
|
||||
function verifyWorkflowRun(params: {
|
||||
id: string;
|
||||
label: string;
|
||||
repo: string;
|
||||
expectedWorkflowName: string;
|
||||
expectedHeadBranch?: string;
|
||||
allowedHeadBranches?: string[];
|
||||
rerunFailed: boolean;
|
||||
}): WorkflowRunSummary {
|
||||
const raw = runCommand("gh", [
|
||||
"run",
|
||||
"view",
|
||||
params.id,
|
||||
"--repo",
|
||||
params.repo,
|
||||
"--json",
|
||||
"workflowName,headBranch,event,status,conclusion,url,createdAt,updatedAt,jobs",
|
||||
]);
|
||||
const run = parseJson(raw, `gh run view ${params.id}`);
|
||||
if (!isRecord(run)) {
|
||||
throw new Error(`${params.label}: workflow run returned an unsupported JSON shape.`);
|
||||
}
|
||||
const workflowName = readString(run.workflowName);
|
||||
if (workflowName !== params.expectedWorkflowName) {
|
||||
throw new Error(
|
||||
`${params.label}: run ${params.id} workflow is ${workflowName ?? "<missing>"}, expected ${params.expectedWorkflowName}.`,
|
||||
);
|
||||
}
|
||||
const event = readString(run.event);
|
||||
if (event !== "workflow_dispatch") {
|
||||
throw new Error(
|
||||
`${params.label}: run ${params.id} event is ${event ?? "<missing>"}, expected workflow_dispatch.`,
|
||||
);
|
||||
}
|
||||
const headBranch = readString(run.headBranch);
|
||||
const allowedHeadBranches =
|
||||
params.allowedHeadBranches ??
|
||||
(params.expectedHeadBranch !== undefined ? [params.expectedHeadBranch] : []);
|
||||
if (allowedHeadBranches.length > 0 && !allowedHeadBranches.includes(headBranch ?? "")) {
|
||||
throw new Error(
|
||||
`${params.label}: run ${params.id} branch is ${headBranch ?? "<missing>"}, expected ${allowedHeadBranches.join(" or ")}.`,
|
||||
);
|
||||
}
|
||||
const status = readString(run.status);
|
||||
const conclusion = readString(run.conclusion);
|
||||
const jobs = Array.isArray(run.jobs) ? run.jobs.filter(isRecord) : [];
|
||||
const failedJobs = jobs.filter((job) => {
|
||||
const jobConclusion = readString(job.conclusion);
|
||||
return (
|
||||
jobConclusion !== undefined && jobConclusion !== "success" && jobConclusion !== "skipped"
|
||||
);
|
||||
});
|
||||
if (failedJobs.length > 0 && params.rerunFailed) {
|
||||
runCommandInherited("gh", ["run", "rerun", params.id, "--repo", params.repo, "--failed"]);
|
||||
throw new Error(
|
||||
`${params.label}: reran ${failedJobs.length} failed job(s); rerun verifier after it finishes.`,
|
||||
);
|
||||
}
|
||||
if (status !== "completed" || conclusion !== "success" || failedJobs.length > 0) {
|
||||
const failedNames = failedJobs.map((job) => readString(job.name) ?? "<unnamed>").join(", ");
|
||||
throw new Error(
|
||||
`${params.label}: run ${params.id} is ${status ?? "<missing>"}/${conclusion ?? "<missing>"}${failedNames ? `; failed jobs: ${failedNames}` : ""}.`,
|
||||
);
|
||||
}
|
||||
const createdAt = readString(run.createdAt);
|
||||
const updatedAt = readString(run.updatedAt);
|
||||
const createdMs = createdAt === undefined ? Number.NaN : Date.parse(createdAt);
|
||||
const updatedMs = updatedAt === undefined ? Number.NaN : Date.parse(updatedAt);
|
||||
const durationSeconds =
|
||||
Number.isFinite(createdMs) && Number.isFinite(updatedMs)
|
||||
? Math.max(0, Math.round((updatedMs - createdMs) / 1000))
|
||||
: undefined;
|
||||
return {
|
||||
id: params.id,
|
||||
label: params.label,
|
||||
url: readString(run.url),
|
||||
durationSeconds,
|
||||
};
|
||||
}
|
||||
|
||||
function readRootPackageVersion(rootDir: string): string {
|
||||
const packageJson = parseJson(
|
||||
readFileSync(resolve(rootDir, "package.json"), "utf8"),
|
||||
"package.json",
|
||||
);
|
||||
if (!isRecord(packageJson)) {
|
||||
throw new Error("package.json returned an unsupported JSON shape.");
|
||||
}
|
||||
return requireString(packageJson.version, "package.json version");
|
||||
}
|
||||
|
||||
function formatDuration(seconds: number | undefined): string {
|
||||
if (seconds === undefined) {
|
||||
return "unknown";
|
||||
}
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainder = seconds % 60;
|
||||
return `${minutes}m${remainder.toString().padStart(2, "0")}s`;
|
||||
}
|
||||
|
||||
function assertSelectedPackagesResolved(params: {
|
||||
label: string;
|
||||
selection: readonly string[];
|
||||
packages: readonly { packageName: string }[];
|
||||
}): void {
|
||||
if (params.selection.length === 0) {
|
||||
return;
|
||||
}
|
||||
const resolved = new Set(params.packages.map((plugin) => plugin.packageName));
|
||||
const missing = params.selection.filter((packageName) => !resolved.has(packageName));
|
||||
if (missing.length > 0) {
|
||||
throw new Error(`Unknown or non-publishable ${params.label} selection: ${missing.join(", ")}.`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function verifyBetaRelease(
|
||||
args: ReleaseVerifyBetaArgs,
|
||||
options: { rootDir?: string } = {},
|
||||
): Promise<string[]> {
|
||||
const rootDir = options.rootDir ?? resolve(".");
|
||||
const rootVersion = readRootPackageVersion(rootDir);
|
||||
if (rootVersion !== args.version) {
|
||||
throw new Error(`package.json version is ${rootVersion}; expected ${args.version}.`);
|
||||
}
|
||||
|
||||
const lines: string[] = [];
|
||||
const releaseUrl = args.skipGitHubRelease ? undefined : verifyGitHubRelease(args);
|
||||
if (releaseUrl === undefined) {
|
||||
lines.push("GitHub release skipped: final release page is created after verification");
|
||||
} else {
|
||||
lines.push(`GitHub release OK: ${releaseUrl}`);
|
||||
}
|
||||
|
||||
const openclawNpm = await verifyNpmPackage("openclaw", args.version, args.distTag);
|
||||
lines.push(`openclaw npm OK: ${args.version} (${args.distTag})`);
|
||||
|
||||
if (!args.skipPostpublish) {
|
||||
runCommandInherited("node", [
|
||||
"--import",
|
||||
"tsx",
|
||||
"scripts/openclaw-npm-postpublish-verify.ts",
|
||||
args.version,
|
||||
]);
|
||||
lines.push("openclaw postpublish verifier OK");
|
||||
}
|
||||
|
||||
const npmPlugins = collectPublishablePluginPackages(rootDir, {
|
||||
packageNames: args.pluginSelection.length > 0 ? args.pluginSelection : undefined,
|
||||
});
|
||||
assertSelectedPackagesResolved({
|
||||
label: "npm plugin",
|
||||
selection: args.pluginSelection,
|
||||
packages: npmPlugins,
|
||||
});
|
||||
for (const plugin of npmPlugins) {
|
||||
await verifyNpmPackage(plugin.packageName, args.version, args.distTag);
|
||||
}
|
||||
lines.push(`plugin npm OK: ${npmPlugins.length}`);
|
||||
|
||||
const clawHubPlugins = args.skipClawHub
|
||||
? []
|
||||
: collectClawHubPublishablePluginPackages(rootDir, {
|
||||
packageNames: args.pluginSelection.length > 0 ? args.pluginSelection : undefined,
|
||||
});
|
||||
if (args.skipClawHub) {
|
||||
lines.push("ClawHub skipped");
|
||||
} else {
|
||||
assertSelectedPackagesResolved({
|
||||
label: "ClawHub plugin",
|
||||
selection: args.pluginSelection,
|
||||
packages: clawHubPlugins,
|
||||
});
|
||||
for (const plugin of clawHubPlugins) {
|
||||
await verifyClawHubPackage({
|
||||
registry: args.registry,
|
||||
packageName: plugin.packageName,
|
||||
version: args.version,
|
||||
distTag: args.distTag,
|
||||
});
|
||||
}
|
||||
lines.push(`ClawHub OK: ${clawHubPlugins.length}`);
|
||||
}
|
||||
|
||||
const workflowRuns: WorkflowRunSummary[] = [];
|
||||
if (args.workflowRuns.fullReleaseValidation !== undefined) {
|
||||
workflowRuns.push(
|
||||
verifyWorkflowRun({
|
||||
id: args.workflowRuns.fullReleaseValidation,
|
||||
label: "Full Release Validation",
|
||||
repo: args.repo,
|
||||
expectedWorkflowName: "Full Release Validation",
|
||||
allowedHeadBranches: ["main", args.workflowRef],
|
||||
rerunFailed: false,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (args.workflowRuns.pluginNpm !== undefined) {
|
||||
workflowRuns.push(
|
||||
verifyWorkflowRun({
|
||||
id: args.workflowRuns.pluginNpm,
|
||||
label: "Plugin NPM Release",
|
||||
repo: args.repo,
|
||||
expectedWorkflowName: "Plugin NPM Release",
|
||||
expectedHeadBranch: args.workflowRef,
|
||||
rerunFailed: false,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (args.workflowRuns.pluginClawHub !== undefined) {
|
||||
const clawHubWorkflowRef = args.clawHubWorkflowRef ?? args.workflowRef;
|
||||
workflowRuns.push(
|
||||
verifyWorkflowRun({
|
||||
id: args.workflowRuns.pluginClawHub,
|
||||
label: "Plugin ClawHub Release",
|
||||
repo: args.repo,
|
||||
expectedWorkflowName: "Plugin ClawHub Release",
|
||||
expectedHeadBranch: clawHubWorkflowRef,
|
||||
rerunFailed: args.rerunFailedClawHub,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (args.workflowRuns.pluginClawHubBootstrap !== undefined) {
|
||||
const clawHubWorkflowRef = args.clawHubWorkflowRef ?? args.workflowRef;
|
||||
workflowRuns.push(
|
||||
verifyWorkflowRun({
|
||||
id: args.workflowRuns.pluginClawHubBootstrap,
|
||||
label: "Plugin ClawHub New",
|
||||
repo: args.repo,
|
||||
expectedWorkflowName: "Plugin ClawHub New",
|
||||
expectedHeadBranch: clawHubWorkflowRef,
|
||||
rerunFailed: false,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (args.workflowRuns.openclawNpm !== undefined) {
|
||||
workflowRuns.push(
|
||||
verifyWorkflowRun({
|
||||
id: args.workflowRuns.openclawNpm,
|
||||
label: "OpenClaw NPM Release",
|
||||
repo: args.repo,
|
||||
expectedWorkflowName: "OpenClaw NPM Release",
|
||||
expectedHeadBranch: args.workflowRef,
|
||||
rerunFailed: false,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (args.workflowRuns.npmTelegram !== undefined) {
|
||||
workflowRuns.push(
|
||||
verifyWorkflowRun({
|
||||
id: args.workflowRuns.npmTelegram,
|
||||
label: "NPM Telegram Beta E2E",
|
||||
repo: args.repo,
|
||||
expectedWorkflowName: "NPM Telegram Beta E2E",
|
||||
allowedHeadBranches: ["main", args.workflowRef],
|
||||
rerunFailed: false,
|
||||
}),
|
||||
);
|
||||
}
|
||||
for (const run of workflowRuns) {
|
||||
lines.push(
|
||||
`${run.label} OK: ${run.id} (${formatDuration(run.durationSeconds)})${run.url ? ` ${run.url}` : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (args.evidenceOut !== undefined) {
|
||||
const evidencePath = resolve(rootDir, args.evidenceOut);
|
||||
mkdirSync(dirname(evidencePath), { recursive: true });
|
||||
writeFileSync(
|
||||
evidencePath,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
version: 1,
|
||||
releaseVersion: args.version,
|
||||
releaseTag: args.tag,
|
||||
npmDistTag: args.distTag,
|
||||
pluginSelection: args.pluginSelection,
|
||||
openclawNpmIntegrity: openclawNpm.integrity,
|
||||
openclawNpmTarball: openclawNpm.tarball,
|
||||
npmRegistrySignaturesVerified: args.skipPostpublish ? null : true,
|
||||
npmProvenanceAttestationMatched: args.skipPostpublish ? null : true,
|
||||
githubReleaseUrl: releaseUrl ?? null,
|
||||
pluginNpmPackageCount: npmPlugins.length,
|
||||
clawHubPackageCount: clawHubPlugins.length,
|
||||
workflowRuns,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
lines.push(`release evidence written: ${args.evidenceOut}`);
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
Reference in New Issue
Block a user