import { execFileSync } from "node:child_process"; import { createHash, randomUUID } from "node:crypto"; import { linkSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { lt as semverLt, valid as validSemver } from "semver"; import { z } from "zod"; import { isRecord as isJsonRecord } from "../../packages/normalization-core/src/record-coerce.ts"; import { normalizeOptionalString } from "../../packages/normalization-core/src/string-coerce.ts"; import { readPublicationArtifactArchive, sha256Digest } from "./actions-artifact-archive.mjs"; import { readBoundedResponseText } from "./bounded-response.mjs"; import { collectPublishableCorePackages } from "./npm-core-release-packages.mjs"; import { resolveNpmJsonEntries } from "./npm-json-output.mts"; import { npmRegistryReadbackDeadline } from "./npm-publish-plan.mjs"; import { collectClawHubPublishablePluginPackages } from "./plugin-clawhub-release.ts"; import { collectPublishablePluginPackages, parsePluginReleaseSelection, } from "./plugin-npm-release.ts"; import { DIAGNOSTIC_MAX_PACKAGES, diagnosticStates, diagnosticError, diagnosticPackage, diagnosticStage, diagnosticStageNames, } from "./release-postpublish-diagnostic-schema.mts"; type JsonRecord = Record; type ReleaseVerifyBetaArgs = { version: string; tag: string; distTag: string; repo: string; registry: string; releaseSha?: string; workflowRef?: string; clawHubWorkflowRef?: string; pluginSelection: string[]; clawHubBootstrapPlugins: string[]; evidenceOut?: string; postpublishVerifier?: string; skipPostpublish: boolean; skipGitHubRelease: boolean; skipClawHub: boolean; rerunFailedClawHub: boolean; workflowRuns: { fullReleaseValidation?: string; openclawNpm?: string; pluginNpm?: string; pluginClawHub?: string; pluginClawHubBootstrap?: string; npmTelegram?: string; }; }; type NpmViewFields = { version?: string; distTagVersion?: string; integrity?: string; tarball?: string; }; type FetchWithRetryResult = { response: Response; signal: AbortSignal; }; type WorkflowRunSummary = { id: string; runAttempt?: number; label: string; url?: string; durationSeconds?: number; bootstrapEvidence?: { targetSha: string; workflowSha: string; workflowPath: string; producerRunAttempt: string; terminalRunAttempt: string; readbackArtifactId: string; readbackArtifactDigest: string; packageArtifactId: string; packageArtifactDigest: string; packageCount: number; clawhubToolchainIntegrity: string; clawhubToolchainSha256: string; clawhubToolchainVersion: string; }; }; const DEFAULT_REPO = "openclaw/openclaw"; const DEFAULT_CLAWHUB_REGISTRY = "https://clawhub.ai"; const CLAWHUB_BOOTSTRAP_WORKFLOW_PATH = ".github/workflows/plugin-clawhub-new.yml"; const CLAWHUB_BOOTSTRAP_READBACK_FILE = "clawhub-bootstrap-readback.json"; const CLAWHUB_REQUEST_TIMEOUT_MS = 20_000; const CLAWHUB_RESPONSE_BODY_MAX_BYTES = 1024 * 1024; const CLAWHUB_BOOTSTRAP_READBACK_ARCHIVE_MAX_BYTES = 2 * 1024 * 1024; const COMMIT_SHA_PATTERN = /^[a-f0-9]{40}$/u; const SHA256_PATTERN = /^[a-f0-9]{64}$/u; const POSITIVE_INTEGER_PATTERN = /^[1-9][0-9]*$/u; const SHA512_INTEGRITY_PATTERN = /^sha512-[A-Za-z0-9+/]+={0,2}$/u; const TRUSTED_TOOLING_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); const RELEASE_COMMAND_TIMEOUT_MS = 120_000; const RELEASE_COMMAND_MAX_BUFFER_BYTES = 4 * 1024 * 1024; const DIAGNOSTIC_FILE = "release-postpublish-diagnostics.json"; const DIAGNOSTIC_MAX_BYTES = 128 * 1024; const diagnosticChildNames = [ "fullReleaseValidation", "openclawNpm", "pluginNpm", "pluginClawHub", "pluginClawHubBootstrap", "npmTelegram", ] as const; type DiagnosticStageName = (typeof diagnosticStageNames)[number]; type NpmDiagnosticScope = { stage: "coreNpm" } | { stage: "pluginNpm"; packageName: string }; type DiagnosticChildName = (typeof diagnosticChildNames)[number]; const diagnosticId = z.string().max(20).regex(POSITIVE_INTEGER_PATTERN).nullable(); const diagnosticSha = z.string().regex(COMMIT_SHA_PATTERN).nullable(); const diagnosticRef = z .string() .max(200) .regex(/^(?:refs\/(?:heads|tags)\/)?[A-Za-z0-9][A-Za-z0-9._/-]*$/u) .nullable(); const diagnosticOutcome = z.enum([ "success", "failure", "cancelled", "skipped", "timed_out", "action_required", "neutral", "stale", "unknown", ]); const diagnosticSchema = z.object({ schemaVersion: z.literal(1), kind: z.literal("release-postpublish-diagnostics"), invocationId: z.string().uuid(), context: z.object({ repository: z .string() .max(200) .regex(/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u) .nullable(), releaseVersion: z .string() .max(80) .regex(/^[0-9]+(?:\.[0-9]+){2}(?:-[a-z0-9.-]+)?$/u) .nullable(), releaseTag: z .string() .max(81) .regex(/^v[0-9]+(?:\.[0-9]+){2}(?:-[a-z0-9.-]+)?$/u) .nullable(), npmDistTag: z.enum(["latest", "beta", "alpha", "extended-stable"]).nullable(), requestedSourceSha: diagnosticSha, toolingSha: diagnosticSha, suppliedToolingSha: diagnosticSha, suppliedToolingRef: diagnosticRef, parentRunId: diagnosticId, parentRunAttempt: diagnosticId, validationEvidence: z.object({ mode: z.enum(["full-release-validation", "authorized-beta-focused-v1"]).nullable(), runId: diagnosticId, runAttempt: diagnosticId, }), }), selection: z.object({ plugins: z.array(diagnosticPackage.shape.name).max(DIAGNOSTIC_MAX_PACKAGES), pluginsTruncated: z.boolean(), workflowRef: diagnosticRef, clawHubWorkflowRef: diagnosticRef, }), verification: diagnosticStates, currentStage: z.enum(diagnosticStageNames).nullable(), stages: z.record(z.enum(diagnosticStageNames), diagnosticStage), children: z.record( z.enum(diagnosticChildNames), z.object({ suppliedRunId: diagnosticId, runAttempt: diagnosticId, producerRunAttempt: diagnosticId, status: z.enum([ "queued", "in_progress", "completed", "waiting", "pending", "requested", "unknown", ]), conclusion: diagnosticOutcome, failedJobCount: z.number().int().min(0).max(10000).nullable(), readbackArtifactId: diagnosticId, packageArtifactId: diagnosticId, }), ), jobOutcomeBeforeArtifactUploads: diagnosticOutcome, stepOutcomes: z.object({ coreStart: diagnosticOutcome, completion: diagnosticOutcome, }), }); type PostpublishDiagnostic = z.infer; function diagnosticValue(schema: z.ZodType, value: unknown): T | null { const parsed = schema.safeParse(value); return parsed.success ? parsed.data : null; } function diagnosticFailure( error: unknown, stage: DiagnosticStageName, ): z.infer { const code = isJsonRecord(error) ? error.code : undefined; const status = isJsonRecord(error) ? error.status : undefined; // Classify locally, but never retain command text, URLs, stderr, or stack traces. const message = error instanceof Error ? error.message : ""; const failureClass = stage === "evidence" || stage === "binding" ? "evidence-write-failure" : isNpmPropagationError(error) ? "registry-not-visible" : code === "ETIMEDOUT" || code === "ECONNRESET" || code === "ENOTFOUND" ? "transport" : /invalid JSON|unsupported JSON|response body/u.test(message) ? "malformed-response" : /dist-tag|ClawHub tag/u.test(message) ? "selector-mismatch" : /expected|mismatch|Unknown or non-publishable/u.test(message) ? "identity-mismatch" : "command-failure"; return { class: failureClass, status: typeof status === "number" && Number.isInteger(status) && status >= 0 && status <= 255 ? status : null, }; } class PostpublishDiagnostics { readonly data: PostpublishDiagnostic; private path: string | undefined; private warned = false; private packageName: string | undefined; constructor( args: ReleaseVerifyBetaArgs, rootDir: string, mode: "initialize" | "verify" | "observe", ) { let toolingSha: string | undefined; try { toolingSha = runReleaseVerifierCommand("git", ["rev-parse", "HEAD"], { cwd: TRUSTED_TOOLING_ROOT, }); } catch { // A diagnostic never substitutes a supplied workflow SHA for unread tooling. } const env = process.env; const emptyStage = (): z.infer => ({ state: "unattempted", publication: "unknown", error: null, packages: [], packagesTruncated: false, }); this.data = { schemaVersion: 1, kind: "release-postpublish-diagnostics", invocationId: randomUUID(), context: { repository: diagnosticValue(diagnosticSchema.shape.context.shape.repository, args.repo), releaseVersion: diagnosticValue( diagnosticSchema.shape.context.shape.releaseVersion, args.version, ), releaseTag: diagnosticValue(diagnosticSchema.shape.context.shape.releaseTag, args.tag), npmDistTag: diagnosticValue(diagnosticSchema.shape.context.shape.npmDistTag, args.distTag), requestedSourceSha: diagnosticValue(diagnosticSha, args.releaseSha), toolingSha: diagnosticValue(diagnosticSha, toolingSha), suppliedToolingSha: diagnosticValue(diagnosticSha, env.GITHUB_WORKFLOW_SHA), suppliedToolingRef: diagnosticValue(diagnosticRef, env.GITHUB_REF), parentRunId: diagnosticValue(diagnosticId, env.GITHUB_RUN_ID), parentRunAttempt: diagnosticValue(diagnosticId, env.GITHUB_RUN_ATTEMPT), validationEvidence: { mode: diagnosticValue( diagnosticSchema.shape.context.shape.validationEvidence.shape.mode, env.RELEASE_EVIDENCE_MODE, ), runId: diagnosticValue( diagnosticId, env.RELEASE_EVIDENCE_MODE === "authorized-beta-focused-v1" ? env.FOCUSED_RELEASE_EVIDENCE_RUN_ID : env.FULL_RELEASE_VALIDATION_RUN_ID, ), runAttempt: diagnosticValue( diagnosticId, env.RELEASE_EVIDENCE_MODE === "authorized-beta-focused-v1" ? env.FOCUSED_RELEASE_EVIDENCE_RUN_ATTEMPT : env.FULL_RELEASE_VALIDATION_RUN_ATTEMPT, ), }, }, selection: { plugins: args.pluginSelection .slice(0, DIAGNOSTIC_MAX_PACKAGES) .filter((name) => diagnosticPackage.shape.name.safeParse(name).success), pluginsTruncated: args.pluginSelection.length > DIAGNOSTIC_MAX_PACKAGES || args.pluginSelection.some( (name) => !diagnosticPackage.shape.name.safeParse(name).success, ), workflowRef: diagnosticValue(diagnosticRef, args.workflowRef), clawHubWorkflowRef: diagnosticValue(diagnosticRef, args.clawHubWorkflowRef), }, verification: "unattempted", currentStage: null, stages: Object.fromEntries( diagnosticStageNames.map((name) => [name, emptyStage()]), ) as PostpublishDiagnostic["stages"], children: Object.fromEntries( diagnosticChildNames.map((name) => [ name, { suppliedRunId: diagnosticValue(diagnosticId, args.workflowRuns[name]), runAttempt: null, producerRunAttempt: null, status: "unknown", conclusion: "unknown", failedJobCount: null, readbackArtifactId: null, packageArtifactId: null, }, ]), ) as PostpublishDiagnostic["children"], jobOutcomeBeforeArtifactUploads: "unknown", stepOutcomes: { coreStart: "unknown", completion: "unknown" }, }; if (!args.evidenceOut) { return; } this.path = resolve(dirname(resolve(rootDir, args.evidenceOut)), DIAGNOSTIC_FILE); let existing: PostpublishDiagnostic | undefined; try { const stat = lstatSync(this.path); if (!stat.isFile() || stat.size > DIAGNOSTIC_MAX_BYTES) { throw new Error("Invalid diagnostic file."); } existing = diagnosticSchema.parse(JSON.parse(readFileSync(this.path, "utf8"))); if ( mode === "initialize" || !this.data.context.parentRunId || !this.data.context.parentRunAttempt || JSON.stringify(existing.context) !== JSON.stringify(this.data.context) || (mode === "verify" && existing.verification !== "unattempted") ) { throw new Error("Diagnostic belongs to another invocation."); } const selection = this.data.selection; const children = this.data.children; this.data = existing; if (mode === "verify") { this.data.selection = selection; } for (const name of diagnosticChildNames) { const supplied = children[name].suppliedRunId; if ( supplied && this.data.children[name].suppliedRunId && supplied !== this.data.children[name].suppliedRunId ) { throw new Error("Diagnostic child identity changed."); } this.data.children[name].suppliedRunId ??= supplied; } } catch (error) { if (!isJsonRecord(error) || error.code !== "ENOENT") { this.warn(); this.path = undefined; return; } if (mode === "observe") { this.warn(); this.path = undefined; return; } } if (mode !== "observe") { for (const [stage, skipped] of [ ["githubRelease", args.skipGitHubRelease], ["postpublish", args.skipPostpublish], ["clawHub", args.skipClawHub], ["evidence", false], ] as const) { this.data.stages[stage].state = skipped ? "skipped" : "unattempted"; } for (const name of diagnosticChildNames) { this.data.stages[this.workflowStage(name)].state = args.workflowRuns[name] ? "unattempted" : "skipped"; } } this.save(existing === undefined); } private warn() { if (!this.warned) { console.error( "Warning: postpublish diagnostics unavailable or incomplete; inspect the primary result.", ); this.warned = true; } } save(create = false) { if (!this.path) { return; } const temporary = `${this.path}.${randomUUID()}.tmp`; try { const bytes = `${JSON.stringify(diagnosticSchema.parse(this.data), null, 2)}\n`; if (Buffer.byteLength(bytes) > DIAGNOSTIC_MAX_BYTES) { throw new Error("Diagnostic exceeds byte bound."); } mkdirSync(dirname(this.path), { recursive: true }); writeFileSync(temporary, bytes, { flag: "wx", mode: 0o600 }); if (create) { linkSync(temporary, this.path); } else { renameSync(temporary, this.path); } } catch { this.warn(); if (create) { this.path = undefined; } } finally { try { rmSync(temporary, { force: true }); } catch { this.warn(); } } } workflowStage(name: DiagnosticChildName): DiagnosticStageName { return name === "pluginNpm" ? "pluginNpmRun" : name === "pluginClawHub" ? "pluginClawHubRun" : name; } observeRun(name: DiagnosticChildName, run: JsonRecord, failedJobCount: number) { const child = this.data.children[name]; child.status = diagnosticValue(diagnosticSchema.shape.children.valueType.shape.status, run.status) ?? "unknown"; child.conclusion = diagnosticValue(diagnosticOutcome, run.conclusion) ?? "unknown"; child.runAttempt = diagnosticValue( diagnosticId, typeof run.attempt === "number" ? String(run.attempt) : run.attempt, ) ?? child.runAttempt; child.failedJobCount = Math.min(10000, failedJobCount); this.save(); } start(stage: DiagnosticStageName) { this.packageName = undefined; this.data.currentStage = stage; this.data.stages[stage].state = "started"; // Collecting later observations must not erase an already observed failure. if ( !["evidence", "binding", "assets"].includes(stage) && this.data.verification !== "failure" ) { this.data.verification = "started"; } this.save(); } success(stage: DiagnosticStageName, publication = false) { this.data.stages[stage].state = "success"; if (publication) { this.data.stages[stage].publication = "observed"; } this.packageName = undefined; this.save(); } observeNpmPublication(scope: NpmDiagnosticScope) { const stage = this.data.stages[scope.stage]; const entry = scope.stage === "coreNpm" ? stage : stage.packages.find((item) => item.name === scope.packageName); if (entry) { entry.publication = "observed"; this.save(); } } packages(stage: "pluginNpm" | "clawHub", packages: readonly { packageName: string }[]) { this.data.stages[stage].packages = packages .slice(0, DIAGNOSTIC_MAX_PACKAGES) .filter((plugin) => diagnosticPackage.shape.name.safeParse(plugin.packageName).success) .map((plugin) => ({ name: plugin.packageName, state: "unattempted", publication: "unknown", error: null, })); this.data.stages[stage].packagesTruncated = this.data.stages[stage].packages.length !== packages.length; this.save(); } package(stage: "pluginNpm" | "clawHub", name: string, state: "started" | "success") { this.packageName = name; const entry = this.data.stages[stage].packages.find((item) => item.name === name); if (entry) { entry.state = state; if (state === "success") { entry.publication = "observed"; } } this.save(); } fail(error: unknown, scope?: NpmDiagnosticScope) { if (scope) { this.data.currentStage = scope.stage; this.packageName = scope.stage === "pluginNpm" ? scope.packageName : undefined; } const stage = this.data.currentStage; if (!stage) { return; } const entry = this.data.stages[stage]; entry.state = "failure"; entry.error = diagnosticFailure(error, stage); const item = entry.packages.find((candidate) => candidate.name === this.packageName); if (item) { item.state = "failure"; item.error = entry.error; } if (!["evidence", "binding", "assets"].includes(stage)) { this.data.verification = "failure"; } this.save(); } } // Called only by the existing publish helper. This records local observations; // it cannot invoke a registry, GitHub mutation, retry, or publication command. function recordReleasePublishDiagnostics(event: string): void { try { const env = process.env; if (!env.POSTPUBLISH_EVIDENCE_DIR) { return; } const args = parseReleaseVerifyBetaArgs([ env.RELEASE_TAG?.replace(/^v/u, "") ?? "", "--evidence-out", resolve(env.POSTPUBLISH_EVIDENCE_DIR, "release-postpublish-evidence.json"), "--skip-github-release", ]); args.repo = env.GITHUB_REPOSITORY ?? DEFAULT_REPO; args.distTag = env.RELEASE_NPM_DIST_TAG ?? ""; args.releaseSha = env.TARGET_SHA; args.pluginSelection = parsePluginReleaseSelection(env.PLUGINS); args.workflowRef = env.CHILD_WORKFLOW_REF; args.skipClawHub = env.WAIT_FOR_CLAWHUB === "false"; args.workflowRuns = { openclawNpm: env.CHILD_OPENCLAW_NPM_RUN_ID, pluginNpm: env.CHILD_PLUGIN_NPM_RUN_ID, pluginClawHub: env.CHILD_PLUGIN_CLAWHUB_RUN_ID, pluginClawHubBootstrap: env.CHILD_PLUGIN_CLAWHUB_BOOTSTRAP_RUN_ID, npmTelegram: env.NPM_TELEGRAM_RUN_ID, }; const diagnostic = new PostpublishDiagnostics( args, ".", event === "initialize" ? "initialize" : "observe", ); if (event === "binding-start") { diagnostic.start("binding"); } else if (event === "binding-success") { diagnostic.success("binding"); } else if (event === "assets-start") { diagnostic.start("assets"); } else if (event === "assets-success") { diagnostic.success("assets"); } else if (event === "terminal") { diagnostic.data.stepOutcomes = { coreStart: diagnosticValue(diagnosticOutcome, env.CORE_START_OUTCOME) ?? "unknown", completion: diagnosticValue(diagnosticOutcome, env.COMPLETION_OUTCOME) ?? "unknown", }; diagnostic.data.jobOutcomeBeforeArtifactUploads = diagnosticValue(diagnosticOutcome, env.PUBLISH_JOB_STATUS) ?? "unknown"; const stage = diagnostic.data.currentStage; if (stage && diagnostic.data.stages[stage].state === "started") { // A failed parent cannot prove a registry check or publication failed. if ( (stage === "binding" || stage === "assets") && diagnostic.data.stepOutcomes.completion === "failure" ) { diagnostic.data.stages[stage].state = "failure"; diagnostic.data.stages[stage].error = { class: "command-failure", status: null }; } else { diagnostic.data.stages[stage].state = "unknown"; } if (diagnostic.data.verification === "started") { diagnostic.data.verification = "unknown"; } } diagnostic.save(); } } catch { console.error("Warning: postpublish diagnostics unavailable; primary result unchanged."); } } function compareCodeUnits(left: string, right: string): number { return left < right ? -1 : left > right ? 1 : 0; } function requireString(value: unknown, label: string): string { const stringValue = normalizeOptionalString(value); if (stringValue === undefined) { throw new Error(`${label} is missing.`); } return stringValue; } function readTrustedClawHubToolchainIdentity(): { clawhubToolchainIntegrity: string; clawhubToolchainSha256: string; clawhubToolchainVersion: string; } { const lockPath = resolve(TRUSTED_TOOLING_ROOT, ".github/release/clawhub-cli/package-lock.json"); const lockBytes = readFileSync(lockPath); const lock = parseJson(lockBytes.toString("utf8"), "trusted ClawHub CLI package-lock.json"); if (!isJsonRecord(lock) || !isJsonRecord(lock.packages)) { throw new Error("Trusted ClawHub CLI package-lock.json is invalid."); } const clawhub = lock.packages["node_modules/clawhub"]; if (!isJsonRecord(clawhub)) { throw new Error("Trusted ClawHub CLI package-lock.json is missing clawhub."); } const clawhubToolchainIntegrity = requireString( clawhub.integrity, "trusted ClawHub CLI integrity", ); if (!SHA512_INTEGRITY_PATTERN.test(clawhubToolchainIntegrity)) { throw new Error("Trusted ClawHub CLI integrity is invalid."); } return { clawhubToolchainIntegrity, clawhubToolchainSha256: createHash("sha256").update(lockBytes).digest("hex"), clawhubToolchainVersion: requireString(clawhub.version, "trusted ClawHub CLI version"), }; } export function runReleaseVerifierCommand( command: string, args: string[], options: { cwd?: string; maxBufferBytes?: number; timeoutMs?: number } = {}, ): string { return execFileSync(command, args, { cwd: options.cwd, encoding: "utf8", killSignal: "SIGKILL", maxBuffer: options.maxBufferBytes ?? RELEASE_COMMAND_MAX_BUFFER_BYTES, stdio: ["ignore", "pipe", "pipe"], timeout: options.timeoutMs ?? RELEASE_COMMAND_TIMEOUT_MS, }).trim(); } function isNpmPropagationError(error: unknown): boolean { if (!isJsonRecord(error)) { return false; } const details = [error.code, error.message, error.stderr] .map((value) => Buffer.isBuffer(value) ? value.toString("utf8") : typeof value === "string" ? value : "", ) .join("\n"); return /\b(?:E404|ETARGET)\b|No match found for version/u.test(details); } export async function runNpmViewWithRetry( args: string[], options: { attempts?: number; delay?: (delayMs: number) => Promise; run?: (args: string[]) => string; } = {}, ): Promise { const deadlineMs = npmRegistryReadbackDeadline(); const attempts = options.attempts ?? Infinity; const delay = options.delay ?? ((delayMs: number) => new Promise((resolveDelay) => { setTimeout(resolveDelay, delayMs); })); const run = options.run ?? ((npmArgs: string[]) => runReleaseVerifierCommand("npm", npmArgs, { timeoutMs: Math.min(RELEASE_COMMAND_TIMEOUT_MS, Math.max(1, deadlineMs - Date.now())), })); let lastError: unknown; for (let attempt = 1; attempt <= attempts; attempt += 1) { try { return run([...args, "--prefer-online"]); } catch (error) { if (!isNpmPropagationError(error)) { throw error; } lastError = error; } if (attempt < attempts) { const remainingMs = deadlineMs - Date.now(); if (remainingMs <= 0) { break; } await delay(Math.min(10_000, remainingMs)); if (Date.now() >= deadlineMs) { break; } } } if (lastError instanceof Error) { lastError.message += "; verification pending. Retry readback, not publication."; } 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 value = parseJson(raw, "npm view"); const entries = resolveNpmJsonEntries(value); const parsed = entries.length === 1 && isJsonRecord(entries[0]) ? entries[0] : value; if (Array.isArray(parsed)) { return { version: normalizeOptionalString(parsed[0]), distTagVersion: normalizeOptionalString(parsed[1]), integrity: normalizeOptionalString(parsed[2]), tarball: normalizeOptionalString(parsed[3]), }; } if (!isJsonRecord(parsed)) { throw new Error("npm view returned an unsupported JSON shape."); } const distTags = isJsonRecord(parsed["dist-tags"]) ? parsed["dist-tags"] : undefined; const dist = isJsonRecord(parsed.dist) ? parsed.dist : undefined; return { version: normalizeOptionalString(parsed.version), distTagVersion: normalizeOptionalString(parsed[`dist-tags.${distTag}`]) ?? normalizeOptionalString(distTags?.[distTag]), integrity: normalizeOptionalString(parsed["dist.integrity"]) ?? normalizeOptionalString(dist?.integrity), tarball: normalizeOptionalString(parsed["dist.tarball"]) ?? normalizeOptionalString(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 -- [--release-sha SHA] [--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 --clawhub-bootstrap-plugins NAMES] [--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, releaseSha: undefined, workflowRef: undefined, clawHubWorkflowRef: undefined, pluginSelection: [], clawHubBootstrapPlugins: [], evidenceOut: undefined, postpublishVerifier: 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 "--release-sha": parsed.releaseSha = next(); if (!COMMIT_SHA_PATTERN.test(parsed.releaseSha)) { throw new Error("--release-sha must be a full 40-character lowercase commit SHA."); } 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 "--clawhub-bootstrap-plugins": parsed.clawHubBootstrapPlugins = parsePluginReleaseSelection(next()); if (parsed.clawHubBootstrapPlugins.length === 0) { throw new Error("--clawhub-bootstrap-plugins requires at least one package name."); } break; case "--evidence-out": parsed.evidenceOut = next(); break; case "--postpublish-verifier": parsed.postpublishVerifier = 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}`); } } if (parsed.skipPostpublish && parsed.postpublishVerifier !== undefined) { throw new Error("--postpublish-verifier cannot be combined with --skip-postpublish."); } if (parsed.workflowRuns.pluginClawHubBootstrap !== undefined) { if (parsed.releaseSha === undefined) { throw new Error("--plugin-clawhub-bootstrap-run requires --release-sha."); } if (parsed.clawHubBootstrapPlugins.length === 0) { throw new Error("--plugin-clawhub-bootstrap-run requires --clawhub-bootstrap-plugins."); } } else if (parsed.clawHubBootstrapPlugins.length > 0) { throw new Error("--clawhub-bootstrap-plugins requires --plugin-clawhub-bootstrap-run."); } return parsed; } export function resolveOpenClawNpmPostpublishVerifier(rootDir: string, override?: string): string { if (override === undefined) { return resolve(rootDir, "scripts/openclaw-npm-postpublish-verify.ts"); } const verifier = resolve(override); if (verifier !== resolve(TRUSTED_TOOLING_ROOT, "scripts/openclaw-npm-postpublish-verify.ts")) { throw new Error("--postpublish-verifier must select the trusted tooling verifier."); } return verifier; } async function fetchWithRetry( url: string, options: RequestInit, attempts: number, ): Promise { 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 { await response.body?.cancel().catch(() => undefined); } export async function fetchJsonWithRetry( url: string, options: { attempts?: number; delay?: (delayMs: number) => Promise; fetchImpl?: typeof fetch; timeoutMs?: number; } = {}, ): Promise { const attempts = options.attempts ?? 5; const delay = options.delay ?? ((delayMs: number) => new Promise((resolveDelay) => { setTimeout(resolveDelay, delayMs); })); const fetchImpl = options.fetchImpl ?? fetch; const timeoutMs = options.timeoutMs ?? CLAWHUB_REQUEST_TIMEOUT_MS; let lastError: unknown; for (let attempt = 1; attempt <= attempts; attempt += 1) { let response: Response | undefined; let attemptError: unknown; try { const signal = AbortSignal.timeout(timeoutMs); response = await fetchImpl(url, { headers: { accept: "application/json" }, signal, }); if (response.status !== 429 && response.status < 500) { if (!response.ok) { await cancelResponseBody(response); throw new Error(`${url} returned HTTP ${response.status}.`); } return await readBoundedJsonResponse(response, url, undefined, { signal }); } attemptError = new Error(`HTTP ${response.status}`); lastError = attemptError; } catch (error) { if ( response !== undefined && response.status !== 429 && response.status < 500 && !response.ok ) { throw error; } attemptError = error; lastError = error; } finally { if (response !== undefined && attemptError !== undefined) { await cancelResponseBody(response); } } if (attempt < attempts) { await delay(attempt * 1000); } } const message = lastError instanceof Error ? lastError.message : String(lastError); throw new Error(`${url} did not return stable JSON: ${message}`); } export async function readBoundedJsonResponse( response: Response, label: string, maxBytes = CLAWHUB_RESPONSE_BODY_MAX_BYTES, options: { signal?: AbortSignal } = {}, ): Promise { return parseJson(await readBoundedResponseText(response, label, maxBytes, options), label); } export async function fetchStatusWithRetry(url: string, method: "GET" | "HEAD"): Promise { const { response } = await fetchWithRetry(url, { method, redirect: "manual" }, 5); try { return response.status; } finally { await cancelResponseBody(response); } } async function readNpmBetaFloorError( packageName: string, version: string, ): Promise { const entries = resolveNpmJsonEntries( parseJson( await runNpmViewWithRetry(["view", `${packageName}@${version}`, "dist-tags", "--json"]), `npm view ${packageName}@${version} dist-tags`, ), ); const tags = entries.length === 1 ? entries[0] : undefined; if (!isJsonRecord(tags)) { throw new Error(`${packageName}: npm dist-tags returned an unsupported JSON shape.`); } // A package published only to beta has no stable floor yet. if (tags.latest === undefined) { return undefined; } const latest = normalizeOptionalString(tags.latest); const beta = normalizeOptionalString(tags.beta); const observed = `${packageName}: beta=${beta ?? JSON.stringify(tags.beta) ?? ""}, latest=${latest ?? JSON.stringify(tags.latest)}`; if ( latest === undefined || !validSemver(latest) || (tags.beta !== undefined && (beta === undefined || !validSemver(beta))) ) { return `${observed} (invalid semver dist-tag)`; } return beta === undefined || semverLt(beta, latest) ? observed : undefined; } function createNpmBetaFloorError(errors: readonly string[]): Error { return new Error( `npm beta must be at or above latest; release verification failed:\n${errors.join("\n")}\nFor each listed stale package, run:\nnpm dist-tag add @ beta\nUse that package's current latest version, preserve newer beta tags, then verify again.`, ); } async function verifyNpmPackage( packageName: string, version: string, distTag: string, ): Promise { 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 ?? ""}.`, ); } if (fields.distTagVersion !== version) { throw new Error( `${packageName}: npm dist-tag ${distTag} points to ${fields.distTagVersion ?? ""}, 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 { if (!isJsonRecord(detail)) { return {}; } const packageDetail = isJsonRecord(detail.package) ? detail.package : undefined; const tags = isJsonRecord(packageDetail?.tags) ? packageDetail.tags : undefined; const result: Record = {}; 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 { 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] ?? ""}, 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 = runReleaseVerifierCommand("gh", [ "release", "view", params.tag, "--repo", params.repo, "--json", "tagName,isPrerelease,url", ]); const release = parseJson(raw, "gh release view"); if (!isJsonRecord(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; expectedRunAttempt?: number; expectedHeadSha?: string; allowedHeadBranches?: string[]; rerunFailed?: boolean; observe?: (run: JsonRecord, failedJobCount: number) => void; }): WorkflowRunSummary { const raw = runReleaseVerifierCommand("gh", [ "run", "view", params.id, "--repo", params.repo, "--json", "workflowName,headBranch,headSha,databaseId,attempt,event,status,conclusion,url,createdAt,updatedAt,jobs", ...(params.expectedRunAttempt === undefined ? [] : ["--attempt", String(params.expectedRunAttempt)]), ]); const run = parseJson(raw, `gh run view ${params.id}`); if (!isJsonRecord(run)) { throw new Error(`${params.label}: workflow run returned an unsupported JSON shape.`); } if ( params.expectedRunAttempt !== undefined && (run.attempt !== params.expectedRunAttempt || run.databaseId !== Number(params.id) || run.headSha !== params.expectedHeadSha) ) { throw new Error( `${params.label}: historical publisher identity does not match its signed attempt.`, ); } const workflowName = normalizeOptionalString(run.workflowName); if (workflowName !== params.expectedWorkflowName) { throw new Error( `${params.label}: run ${params.id} workflow is ${workflowName ?? ""}, expected ${params.expectedWorkflowName}.`, ); } const event = normalizeOptionalString(run.event); if (event !== "workflow_dispatch") { throw new Error( `${params.label}: run ${params.id} event is ${event ?? ""}, expected workflow_dispatch.`, ); } const headBranch = normalizeOptionalString(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 ?? ""}, expected ${allowedHeadBranches.join(" or ")}.`, ); } const status = normalizeOptionalString(run.status); const conclusion = normalizeOptionalString(run.conclusion); const jobs = Array.isArray(run.jobs) ? run.jobs.filter(isJsonRecord) : []; const failedJobs = jobs.filter((job) => { const jobConclusion = normalizeOptionalString(job.conclusion); return ( jobConclusion !== undefined && jobConclusion !== "success" && jobConclusion !== "skipped" ); }); params.observe?.(run, failedJobs.length); if (failedJobs.length > 0 && params.rerunFailed) { runReleaseVerifierCommand("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) => normalizeOptionalString(job.name) ?? "") .join(", "); throw new Error( `${params.label}: run ${params.id} is ${status ?? ""}/${conclusion ?? ""}${failedNames ? `; failed jobs: ${failedNames}` : ""}.`, ); } const createdAt = normalizeOptionalString(run.createdAt); const updatedAt = normalizeOptionalString(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, ...(params.expectedRunAttempt === undefined ? {} : { runAttempt: params.expectedRunAttempt }), label: params.label, url: normalizeOptionalString(run.url), durationSeconds, }; } function requirePositiveIntegerString(value: unknown, label: string): string { if (typeof value === "number" && Number.isSafeInteger(value) && value > 0) { return String(value); } const stringValue = normalizeOptionalString(value); if (stringValue === undefined || !POSITIVE_INTEGER_PATTERN.test(stringValue)) { throw new Error(`${label} must be a positive integer.`); } return stringValue; } function requirePositiveSafeInteger(value: unknown, label: string): number { const stringValue = requirePositiveIntegerString(value, label); const numberValue = Number(stringValue); if (!Number.isSafeInteger(numberValue)) { throw new Error(`${label} must be a positive safe integer.`); } return numberValue; } function requireCommitSha(value: unknown, label: string): string { const sha = requireString(value, label); if (!COMMIT_SHA_PATTERN.test(sha)) { throw new Error(`${label} must be a full 40-character lowercase commit SHA.`); } return sha; } function requireSha256(value: unknown, label: string): string { const sha = requireString(value, label); if (!SHA256_PATTERN.test(sha)) { throw new Error(`${label} must be a lowercase SHA-256 digest.`); } return sha; } function requireArtifactDigest(value: unknown, label: string): string { const digest = requireString(value, label); const match = /^sha256:([a-f0-9]{64})$/u.exec(digest); if (!match?.[1]) { throw new Error(`${label} must be a sha256 artifact digest.`); } return match[1]; } function requireStringArray(value: unknown, label: string): string[] { if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) { throw new Error(`${label} must be a string array.`); } return value .map((entry) => entry.trim()) .filter(Boolean) .toSorted(compareCodeUnits); } function requireArtifactWorkflowRun( artifact: JsonRecord, params: { label: string; runId: string; headSha: string }, ): void { if (artifact.expired !== false) { throw new Error(`${params.label} is expired or missing its immutable state.`); } const workflowRun = artifact.workflow_run; if (!isJsonRecord(workflowRun)) { throw new Error(`${params.label} is missing workflow_run metadata.`); } if ( requirePositiveIntegerString(workflowRun.id, `${params.label} workflow run id`) !== params.runId ) { throw new Error(`${params.label} belongs to a different workflow run.`); } if ( requireCommitSha(workflowRun.head_sha, `${params.label} workflow head SHA`) !== params.headSha ) { throw new Error(`${params.label} belongs to a different workflow head SHA.`); } if (requireString(workflowRun.head_branch, `${params.label} workflow head branch`) !== "main") { throw new Error(`${params.label} was not produced by trusted main.`); } } function requireClawHubBootstrapRunBinding( run: unknown, expectedRunId: string, ): { headSha: string; run: JsonRecord; runAttempt: number; runId: number; terminalRunAttempt: string; workflowPath: string; } { if (!isJsonRecord(run)) { throw new Error("Plugin ClawHub New run metadata is invalid."); } const runId = requirePositiveSafeInteger(run.id, "Plugin ClawHub New run id"); if (String(runId) !== expectedRunId) { throw new Error(`Plugin ClawHub New run id is ${runId}, expected ${expectedRunId}.`); } if (run.name !== "Plugin ClawHub New") { throw new Error("Plugin ClawHub New run has an unexpected workflow name."); } if (run.event !== "workflow_dispatch") { throw new Error("Plugin ClawHub New run was not workflow_dispatch."); } if (run.head_branch !== "main") { throw new Error("Plugin ClawHub New run was not dispatched from trusted main."); } const headSha = requireCommitSha(run.head_sha, "Plugin ClawHub New head SHA"); const runAttempt = requirePositiveSafeInteger(run.run_attempt, "Plugin ClawHub New run attempt"); const workflowPath = requireString(run.path, "Plugin ClawHub New workflow path").replace( /@.*$/u, "", ); if (workflowPath !== CLAWHUB_BOOTSTRAP_WORKFLOW_PATH) { throw new Error("Plugin ClawHub New run has an unexpected workflow path."); } if (run.status !== "completed" || run.conclusion !== "success") { throw new Error("Plugin ClawHub New run is not completed/success."); } return { headSha, run, runAttempt, runId, terminalRunAttempt: String(runAttempt), workflowPath, }; } function requireClawHubReadbackArtifactBinding( artifact: unknown, run: ReturnType, ): { artifactDigest: string; artifactId: number; artifactName: string; artifactSizeBytes: number; } { if (!isJsonRecord(artifact)) { throw new Error("Plugin ClawHub New readback artifact metadata is invalid."); } const artifactName = `clawhub-bootstrap-readback-${run.runId}-${run.terminalRunAttempt}`; if (artifact.name !== artifactName) { throw new Error("Plugin ClawHub New readback artifact name does not bind the run attempt."); } requireArtifactWorkflowRun(artifact, { label: "Plugin ClawHub New readback artifact", runId: String(run.runId), headSha: run.headSha, }); return { artifactDigest: `sha256:${requireArtifactDigest( artifact.digest, "Plugin ClawHub New readback artifact digest", )}`, artifactId: requirePositiveSafeInteger(artifact.id, "Plugin ClawHub New readback artifact id"), artifactName, artifactSizeBytes: requirePositiveSafeInteger( artifact.size_in_bytes, "Plugin ClawHub New readback artifact size", ), }; } function validateBootstrapPackageEvidence( value: unknown, params: { packageName: string; version: string }, ): void { if (!isJsonRecord(value)) { throw new Error(`${params.packageName} bootstrap evidence is invalid.`); } if ( requireString(value.packageName, `${params.packageName} package name`) !== params.packageName ) { throw new Error(`${params.packageName} bootstrap evidence package mismatch.`); } if (requireString(value.version, `${params.packageName} version`) !== params.version) { throw new Error(`${params.packageName} bootstrap evidence version mismatch.`); } const expectedSha256 = requireSha256( value.expectedSha256, `${params.packageName} expected sha256`, ); if ( requireSha256(value.registrySha256, `${params.packageName} registry sha256`) !== expectedSha256 ) { throw new Error( `${params.packageName} registry artifact digest differs from the packed artifact.`, ); } const expectedSize = value.expectedSize; const registrySize = value.registrySize; if ( typeof expectedSize !== "number" || !Number.isSafeInteger(expectedSize) || expectedSize <= 0 || registrySize !== expectedSize ) { throw new Error( `${params.packageName} registry artifact size differs from the packed artifact.`, ); } const npmIntegrity = requireString(value.npmIntegrity, `${params.packageName} npm integrity`); const npmShasum = requireString(value.npmShasum, `${params.packageName} npm shasum`); const metadata = value.artifactMetadata; if (!isJsonRecord(metadata)) { throw new Error(`${params.packageName} artifact metadata evidence is invalid.`); } if (metadata.kind !== "npm-pack") { throw new Error(`${params.packageName} artifact metadata is not npm-pack.`); } if ( requireString(metadata.packageName, `${params.packageName} metadata package name`) !== params.packageName || requireString(metadata.version, `${params.packageName} metadata version`) !== params.version ) { throw new Error(`${params.packageName} artifact metadata identity mismatch.`); } if ( requireSha256(metadata.sha256, `${params.packageName} metadata sha256`) !== expectedSha256 || metadata.size !== expectedSize || metadata.npmIntegrity !== npmIntegrity || metadata.npmShasum !== npmShasum ) { throw new Error(`${params.packageName} artifact metadata does not match downloaded bytes.`); } } export function validateClawHubBootstrapEvidence(params: { repo: string; runId: string; releaseSha: string; expectedVersion: string; expectedPackages: string[]; run: unknown; readbackArtifact: unknown; readbackArchiveSha256: string; packageArtifact: unknown; evidence: unknown; }): WorkflowRunSummary { const runBinding = requireClawHubBootstrapRunBinding(params.run, params.runId); const { headSha, terminalRunAttempt, workflowPath } = runBinding; const runId = String(runBinding.runId); const readbackBinding = requireClawHubReadbackArtifactBinding( params.readbackArtifact, runBinding, ); const readbackArtifactId = String(readbackBinding.artifactId); const readbackArtifactDigest = requireArtifactDigest( readbackBinding.artifactDigest, "Plugin ClawHub New readback artifact digest", ); if ( requireSha256(params.readbackArchiveSha256, "Downloaded readback artifact sha256") !== readbackArtifactDigest ) { throw new Error("Downloaded Plugin ClawHub New readback artifact digest mismatch."); } if (!isJsonRecord(params.evidence)) { throw new Error("Plugin ClawHub New readback evidence is invalid."); } if (params.evidence.schemaVersion !== 2 || params.evidence.verificationMode !== "postpublish") { throw new Error("Plugin ClawHub New readback evidence schema or mode is invalid."); } if (params.evidence.repository !== params.repo) { throw new Error("Plugin ClawHub New readback evidence repository mismatch."); } if ( requireCommitSha(params.evidence.targetSha, "Plugin ClawHub New target SHA") !== params.releaseSha ) { throw new Error("Plugin ClawHub New readback evidence target SHA mismatch."); } if ( requireCommitSha(params.evidence.workflowSha, "Plugin ClawHub New workflow SHA") !== headSha ) { throw new Error("Plugin ClawHub New readback evidence workflow SHA mismatch."); } const evidenceRunId = requirePositiveIntegerString( params.evidence.runId, "Plugin ClawHub New evidence run id", ); const producerRunAttempt = requirePositiveIntegerString( params.evidence.producerRunAttempt, "Plugin ClawHub New evidence producer run attempt", ); const evidenceTerminalRunAttempt = requirePositiveIntegerString( params.evidence.terminalRunAttempt, "Plugin ClawHub New evidence terminal run attempt", ); if (evidenceRunId !== runId || evidenceTerminalRunAttempt !== terminalRunAttempt) { throw new Error("Plugin ClawHub New readback evidence run tuple mismatch."); } if (BigInt(producerRunAttempt) > BigInt(terminalRunAttempt)) { throw new Error("Plugin ClawHub New producer attempt is newer than its terminal attempt."); } const expectedToolchain = readTrustedClawHubToolchainIdentity(); for (const [key, expected] of Object.entries(expectedToolchain)) { if (params.evidence[key] !== expected) { throw new Error(`Plugin ClawHub New ${key} mismatch.`); } } const expectedPackages = [...new Set(params.expectedPackages)].toSorted(compareCodeUnits); if (expectedPackages.length === 0) { throw new Error("Plugin ClawHub New expected package set is empty."); } if ( JSON.stringify( requireStringArray(params.evidence.requestedPlugins, "requested bootstrap plugins"), ) !== JSON.stringify(expectedPackages) ) { throw new Error("Plugin ClawHub New requested package set mismatch."); } if (!Array.isArray(params.evidence.packages)) { throw new Error("Plugin ClawHub New package evidence is invalid."); } const evidencePackages = params.evidence.packages .map((entry) => isJsonRecord(entry) ? requireString(entry.packageName, "bootstrap package name") : "", ) .toSorted(compareCodeUnits); if (JSON.stringify(evidencePackages) !== JSON.stringify(expectedPackages)) { throw new Error("Plugin ClawHub New terminal package set mismatch."); } for (const packageName of expectedPackages) { validateBootstrapPackageEvidence( params.evidence.packages.find( (entry) => isJsonRecord(entry) && entry.packageName === packageName, ), { packageName, version: params.expectedVersion }, ); } if (!isJsonRecord(params.packageArtifact)) { throw new Error("Plugin ClawHub New package artifact metadata is invalid."); } const packageArtifactId = requirePositiveIntegerString( params.packageArtifact.id, "Plugin ClawHub New package artifact id", ); if ( packageArtifactId !== requirePositiveIntegerString( params.evidence.artifactId, "Plugin ClawHub New evidence package artifact id", ) ) { throw new Error("Plugin ClawHub New package artifact id mismatch."); } if ( params.packageArtifact.name !== requireString(params.evidence.artifactName, "Plugin ClawHub New package artifact name") ) { throw new Error("Plugin ClawHub New package artifact name mismatch."); } requireArtifactWorkflowRun(params.packageArtifact, { label: "Plugin ClawHub New package artifact", runId, headSha, }); const packageArtifactDigest = requireArtifactDigest( params.packageArtifact.digest, "Plugin ClawHub New package artifact digest", ); if ( packageArtifactDigest !== requireSha256( params.evidence.artifactDigest, "Plugin ClawHub New evidence package artifact digest", ) ) { throw new Error("Plugin ClawHub New package artifact digest mismatch."); } const expectedPackageArtifactName = `clawhub-bootstrap-${params.releaseSha.slice(0, 12)}-${runId}-${producerRunAttempt}`; if (params.packageArtifact.name !== expectedPackageArtifactName) { throw new Error( "Plugin ClawHub New package artifact name does not bind the target and attempt.", ); } const createdAt = normalizeOptionalString(runBinding.run.created_at); const updatedAt = normalizeOptionalString(runBinding.run.updated_at); const createdMs = createdAt === undefined ? Number.NaN : Date.parse(createdAt); const updatedMs = updatedAt === undefined ? Number.NaN : Date.parse(updatedAt); return { id: runId, label: "Plugin ClawHub New", url: normalizeOptionalString(runBinding.run.html_url), durationSeconds: Number.isFinite(createdMs) && Number.isFinite(updatedMs) ? Math.max(0, Math.round((updatedMs - createdMs) / 1000)) : undefined, bootstrapEvidence: { targetSha: params.releaseSha, workflowSha: headSha, workflowPath, producerRunAttempt, terminalRunAttempt, readbackArtifactId, readbackArtifactDigest, packageArtifactId, packageArtifactDigest, packageCount: expectedPackages.length, ...expectedToolchain, }, }; } function readGitHubApiJson(repo: string, endpoint: string, label: string): unknown { return parseJson(runReleaseVerifierCommand("gh", ["api", `repos/${repo}/${endpoint}`]), label); } function readGitHubToken(): string { return requireString( process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? runReleaseVerifierCommand("gh", ["auth", "token"]), "GitHub token", ); } function decodeUtf8Exact(bytes: Uint8Array, label: string): string { let value: string; try { value = new TextDecoder("utf-8", { fatal: true }).decode(bytes); } catch (error) { throw new Error(`${label} is not valid UTF-8.`, { cause: error }); } if (!Buffer.from(value, "utf8").equals(bytes)) { throw new Error(`${label} is not canonically encoded UTF-8.`); } return value; } export async function downloadClawHubBootstrapReadback(params: { repo: string; runId: string; run: unknown; readbackArtifact: unknown; token: string; fetchImpl?: typeof fetch; retryAttempts?: number; retryDelayMs?: number; timeoutMs?: number; }): Promise<{ value: unknown; archiveSha256: string }> { const runBinding = requireClawHubBootstrapRunBinding(params.run, params.runId); const artifactBinding = requireClawHubReadbackArtifactBinding( params.readbackArtifact, runBinding, ); const downloaded = await readPublicationArtifactArchive({ archivePolicy: { expectedEntries: [CLAWHUB_BOOTSTRAP_READBACK_FILE], maxArchiveBytes: CLAWHUB_BOOTSTRAP_READBACK_ARCHIVE_MAX_BYTES, maxExpandedBytes: CLAWHUB_RESPONSE_BODY_MAX_BYTES, maxCompressedEntryBytes: () => CLAWHUB_BOOTSTRAP_READBACK_ARCHIVE_MAX_BYTES, maxEntryBytes: () => CLAWHUB_RESPONSE_BODY_MAX_BYTES, }, expected: { artifactDigest: artifactBinding.artifactDigest, artifactId: artifactBinding.artifactId, artifactName: artifactBinding.artifactName, artifactSizeBytes: artifactBinding.artifactSizeBytes, repository: params.repo, runStatePolicy: "completed-success", runAttempt: runBinding.runAttempt, runId: runBinding.runId, workflowEvent: "workflow_dispatch", workflowHeadBranch: "main", workflowPath: runBinding.workflowPath, workflowSha: runBinding.headSha, }, fetchImpl: params.fetchImpl, maxArchiveBytes: CLAWHUB_BOOTSTRAP_READBACK_ARCHIVE_MAX_BYTES, retryAttempts: params.retryAttempts, retryDelayMs: params.retryDelayMs, timeoutMs: params.timeoutMs, token: params.token, }); const bytes = downloaded.files.get(CLAWHUB_BOOTSTRAP_READBACK_FILE); if (!bytes) { throw new Error("Plugin ClawHub New readback artifact is missing its evidence file."); } return { value: parseJson( decodeUtf8Exact(bytes, "Plugin ClawHub New readback artifact"), "Plugin ClawHub New readback artifact", ), archiveSha256: requireArtifactDigest( sha256Digest(downloaded.archiveBytes), "Downloaded Plugin ClawHub New readback artifact digest", ), }; } async function verifyClawHubBootstrapRun(params: { repo: string; runId: string; releaseSha: string; version: string; expectedPackages: string[]; observeAttempt?: (run: ReturnType) => void; }): Promise { const run = readGitHubApiJson( params.repo, `actions/runs/${params.runId}`, "Plugin ClawHub New run", ); const runBinding = requireClawHubBootstrapRunBinding(run, params.runId); params.observeAttempt?.(runBinding); const terminalRunAttempt = runBinding.terminalRunAttempt; const readbackName = `clawhub-bootstrap-readback-${params.runId}-${terminalRunAttempt}`; const artifactList = readGitHubApiJson( params.repo, `actions/runs/${params.runId}/artifacts?per_page=100&name=${encodeURIComponent(readbackName)}`, "Plugin ClawHub New readback artifact list", ); if (!isJsonRecord(artifactList) || !Array.isArray(artifactList.artifacts)) { throw new Error("Plugin ClawHub New readback artifact list is invalid."); } const readbackArtifacts = artifactList.artifacts.filter( (artifact) => isJsonRecord(artifact) && artifact.name === readbackName, ); if (readbackArtifacts.length !== 1 || !isJsonRecord(readbackArtifacts[0])) { throw new Error( `Plugin ClawHub New run must have exactly one ${readbackName} artifact; found ${readbackArtifacts.length}.`, ); } const readbackArtifact = readbackArtifacts[0]; const downloaded = await downloadClawHubBootstrapReadback({ repo: params.repo, runId: params.runId, run, readbackArtifact, token: readGitHubToken(), }); if (!isJsonRecord(downloaded.value)) { throw new Error("Plugin ClawHub New readback evidence is invalid."); } const packageArtifactId = requirePositiveIntegerString( downloaded.value.artifactId, "Plugin ClawHub New package artifact id", ); const packageArtifact = readGitHubApiJson( params.repo, `actions/artifacts/${packageArtifactId}`, "Plugin ClawHub New package artifact", ); return validateClawHubBootstrapEvidence({ repo: params.repo, runId: params.runId, releaseSha: params.releaseSha, expectedVersion: params.version, expectedPackages: params.expectedPackages, run, readbackArtifact, readbackArchiveSha256: downloaded.archiveSha256, packageArtifact, evidence: downloaded.value, }); } function readRootPackageVersion(rootDir: string): string { const packageJson = parseJson( readFileSync(resolve(rootDir, "package.json"), "utf8"), "package.json", ); if (!isJsonRecord(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; pluginNpmReadback?: { // Resolves to an informational note for a superseded published version. verify: ( packageName: string, version: string, distTag: string, ) => Promise; evidence: Record[]; }; } = {}, ): Promise { const rootDir = options.rootDir ?? resolve("."); const diagnostic = new PostpublishDiagnostics(args, rootDir, "verify"); const betaFloorErrors: { scope: NpmDiagnosticScope; message: string }[] = []; let betaFloorFailureScope: NpmDiagnosticScope | undefined; try { diagnostic.start("checkout"); const rootVersion = readRootPackageVersion(rootDir); if (rootVersion !== args.version) { throw new Error(`package.json version is ${rootVersion}; expected ${args.version}.`); } if (args.releaseSha !== undefined) { const checkedOutSha = runReleaseVerifierCommand("git", ["rev-parse", "HEAD"], { cwd: rootDir, }); if (checkedOutSha !== args.releaseSha) { throw new Error(`release checkout SHA is ${checkedOutSha}; expected ${args.releaseSha}.`); } } diagnostic.success("checkout"); const lines: string[] = []; if (!args.skipGitHubRelease) { diagnostic.start("githubRelease"); } const releaseUrl = args.skipGitHubRelease ? undefined : verifyGitHubRelease(args); if (!args.skipGitHubRelease) { diagnostic.success("githubRelease", true); } if (releaseUrl === undefined) { lines.push("GitHub release skipped: final release page is created after verification"); } else { lines.push(`GitHub release OK: ${releaseUrl}`); } diagnostic.start("coreNpm"); const openclawNpm = await verifyNpmPackage("openclaw", args.version, args.distTag); diagnostic.observeNpmPublication({ stage: "coreNpm" }); const corePackages = collectPublishableCorePackages(rootDir); // Core versions can be reused without retagging; only enforce their beta floor. for (const name of ["openclaw", ...corePackages.map((pkg) => pkg.name)]) { const error = await readNpmBetaFloorError(name, args.version); if (error !== undefined) { betaFloorErrors.push({ scope: { stage: "coreNpm" }, message: error }); diagnostic.fail(createNpmBetaFloorError([error])); } } const coreBetaFloorFailed = betaFloorErrors.length > 0; if (!coreBetaFloorFailed) { diagnostic.success("coreNpm"); lines.push(`openclaw npm OK: ${args.version} (${args.distTag})`); lines.push(`core npm beta floors OK: ${corePackages.length}`); } if (!args.skipPostpublish && !coreBetaFloorFailed) { diagnostic.start("postpublish"); const postpublishVerifier = resolveOpenClawNpmPostpublishVerifier( rootDir, args.postpublishVerifier, ); execFileSync("node", ["--import", "tsx", postpublishVerifier, args.version], { stdio: "inherit", }); lines.push("openclaw postpublish verifier OK"); diagnostic.success("postpublish"); } diagnostic.start("pluginNpm"); const npmPlugins = collectPublishablePluginPackages(rootDir, { packageNames: args.pluginSelection.length > 0 ? args.pluginSelection : undefined, }); diagnostic.packages("pluginNpm", npmPlugins); assertSelectedPackagesResolved({ label: "npm plugin", selection: args.pluginSelection, packages: npmPlugins, }); for (const plugin of npmPlugins) { diagnostic.package("pluginNpm", plugin.packageName, "started"); // Full publication owns tarball readback, including prior-parent publishes. // Only standalone health checks retain metadata verification. if (options.pluginNpmReadback) { const note = await options.pluginNpmReadback.verify( plugin.packageName, args.version, args.distTag, ); if (note !== undefined) { lines.push(`plugin npm WARN: ${note}`); } } else { await verifyNpmPackage(plugin.packageName, args.version, args.distTag); } const scope: NpmDiagnosticScope = { stage: "pluginNpm", packageName: plugin.packageName }; diagnostic.observeNpmPublication(scope); const betaFloorError = await readNpmBetaFloorError(plugin.packageName, args.version); if (betaFloorError !== undefined) { betaFloorErrors.push({ scope, message: betaFloorError }); diagnostic.fail(createNpmBetaFloorError([betaFloorError])); } else { diagnostic.package("pluginNpm", plugin.packageName, "success"); } } if (!betaFloorErrors.some(({ scope }) => scope.stage === "pluginNpm")) { diagnostic.success("pluginNpm"); } const firstBetaFloorError = betaFloorErrors[0]; if (firstBetaFloorError) { // The final catch must not overwrite the last healthy package or stage. betaFloorFailureScope = firstBetaFloorError.scope; throw createNpmBetaFloorError(betaFloorErrors.map(({ message }) => message)); } lines.push(`plugin npm OK: ${npmPlugins.length}`); if (!args.skipClawHub) { diagnostic.start("clawHub"); } const clawHubPlugins = args.skipClawHub ? [] : collectClawHubPublishablePluginPackages(rootDir, { packageNames: args.pluginSelection.length > 0 ? args.pluginSelection : undefined, }); if (args.skipClawHub) { lines.push("ClawHub skipped"); } else { diagnostic.packages("clawHub", clawHubPlugins); assertSelectedPackagesResolved({ label: "ClawHub plugin", selection: args.pluginSelection, packages: clawHubPlugins, }); for (const plugin of clawHubPlugins) { diagnostic.package("clawHub", plugin.packageName, "started"); await verifyClawHubPackage({ registry: args.registry, packageName: plugin.packageName, version: args.version, distTag: args.distTag, }); diagnostic.package("clawHub", plugin.packageName, "success"); } diagnostic.success("clawHub"); lines.push(`ClawHub OK: ${clawHubPlugins.length}`); } const workflowRuns: WorkflowRunSummary[] = []; const verifyChild = ( name: Exclude, label: string, workflowOptions: () => Omit< Parameters[0], "id" | "label" | "repo" | "expectedWorkflowName" | "observe" >, ) => { const id = args.workflowRuns[name]; if (id === undefined) { return; } const stage = diagnostic.workflowStage(name); diagnostic.start(stage); workflowRuns.push( verifyWorkflowRun({ ...workflowOptions(), id, label, repo: args.repo, expectedWorkflowName: label, observe: (run, count) => diagnostic.observeRun(name, run, count), }), ); diagnostic.success(stage); }; const allowedReleaseWorkflowHeadBranches = args.workflowRef ? ["main", args.workflowRef] : ["main"]; verifyChild("fullReleaseValidation", "Full Release Validation", () => ({ allowedHeadBranches: allowedReleaseWorkflowHeadBranches, })); verifyChild("pluginNpm", "Plugin NPM Release", () => ({ expectedHeadBranch: args.workflowRef, })); verifyChild("pluginClawHub", "Plugin ClawHub Release", () => ({ expectedHeadBranch: args.clawHubWorkflowRef ?? args.workflowRef, rerunFailed: args.rerunFailedClawHub, })); if (args.workflowRuns.pluginClawHubBootstrap !== undefined) { diagnostic.start("pluginClawHubBootstrap"); workflowRuns.push( await verifyClawHubBootstrapRun({ repo: args.repo, runId: args.workflowRuns.pluginClawHubBootstrap, releaseSha: requireCommitSha(args.releaseSha, "release SHA"), version: args.version, expectedPackages: args.clawHubBootstrapPlugins, observeAttempt: (run) => { Object.assign(diagnostic.data.children.pluginClawHubBootstrap, { runAttempt: run.terminalRunAttempt, status: "completed", conclusion: "success", }); diagnostic.save(); }, }), ); const bootstrap = workflowRuns.at(-1)?.bootstrapEvidence; if (bootstrap) { Object.assign(diagnostic.data.children.pluginClawHubBootstrap, { runAttempt: bootstrap.terminalRunAttempt, producerRunAttempt: bootstrap.producerRunAttempt, readbackArtifactId: bootstrap.readbackArtifactId, packageArtifactId: bootstrap.packageArtifactId, status: "completed", conclusion: "success", }); } diagnostic.success("pluginClawHubBootstrap"); } verifyChild("openclawNpm", "OpenClaw NPM Release", () => { const originalAttempt = normalizeOptionalString( process.env.OPENCLAW_NPM_EXPECTED_RUN_ATTEMPT, ); return { expectedRunAttempt: originalAttempt === undefined ? undefined : requirePositiveSafeInteger(originalAttempt, "original npm publisher attempt"), expectedHeadSha: process.env.OPENCLAW_NPM_EXPECTED_WORKFLOW_SHA, expectedHeadBranch: process.env.OPENCLAW_NPM_EXPECTED_WORKFLOW_REF?.replace(/^refs\/(?:tags|heads)\//u, "") ?? args.workflowRef, }; }); verifyChild("npmTelegram", "NPM Telegram Beta E2E", () => ({ allowedHeadBranches: allowedReleaseWorkflowHeadBranches, })); for (const run of workflowRuns) { lines.push( `${run.label} OK: ${run.id} (${formatDuration(run.durationSeconds)})${run.url ? ` ${run.url}` : ""}`, ); } diagnostic.data.verification = "success"; diagnostic.save(); if (args.evidenceOut !== undefined) { diagnostic.start("evidence"); 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, ...(options.pluginNpmReadback ? { pluginNpmPublicationReadbacks: options.pluginNpmReadback.evidence } : {}), clawHubPackageCount: clawHubPlugins.length, workflowRuns, clawHubBootstrapEvidence: workflowRuns.find((run) => run.bootstrapEvidence)?.bootstrapEvidence ?? null, }, null, 2, )}\n`, { flag: "wx" }, ); diagnostic.success("evidence"); lines.push(`release evidence written: ${args.evidenceOut}`); } return lines; } catch (error) { diagnostic.fail(error, betaFloorFailureScope); throw error; } } if (import.meta.main) { recordReleasePublishDiagnostics(process.argv[2] ?? ""); }