mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 08:54:45 +00:00
ci: harden release publish evidence
This commit is contained in:
@@ -2,7 +2,10 @@ import { execFileSync } from "node:child_process";
|
||||
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { collectClawHubPublishablePluginPackages } from "./plugin-clawhub-release.ts";
|
||||
import { collectPublishablePluginPackages } from "./plugin-npm-release.ts";
|
||||
import {
|
||||
collectPublishablePluginPackages,
|
||||
parsePluginReleaseSelection,
|
||||
} from "./plugin-npm-release.ts";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
@@ -12,6 +15,8 @@ export type ReleaseVerifyBetaArgs = {
|
||||
distTag: string;
|
||||
repo: string;
|
||||
registry: string;
|
||||
workflowRef?: string;
|
||||
pluginSelection: string[];
|
||||
evidenceOut?: string;
|
||||
skipPostpublish: boolean;
|
||||
rerunFailedClawHub: boolean;
|
||||
@@ -108,7 +113,7 @@ export function parseReleaseVerifyBetaArgs(argv: string[]): ReleaseVerifyBetaArg
|
||||
const version = values.shift();
|
||||
if (!version || version.startsWith("-")) {
|
||||
throw new Error(
|
||||
"Usage: pnpm release:verify-beta -- <version> [--full-release-validation-run ID] [--openclaw-npm-run ID] [--plugin-npm-run ID] [--plugin-clawhub-run ID] [--npm-telegram-run ID]",
|
||||
"Usage: pnpm release:verify-beta -- <version> [--workflow-ref REF] [--full-release-validation-run ID] [--openclaw-npm-run ID] [--plugin-npm-run ID] [--plugin-clawhub-run ID] [--npm-telegram-run ID]",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -118,6 +123,8 @@ export function parseReleaseVerifyBetaArgs(argv: string[]): ReleaseVerifyBetaArg
|
||||
distTag: "beta",
|
||||
repo: DEFAULT_REPO,
|
||||
registry: DEFAULT_CLAWHUB_REGISTRY,
|
||||
workflowRef: undefined,
|
||||
pluginSelection: [],
|
||||
evidenceOut: undefined,
|
||||
skipPostpublish: false,
|
||||
rerunFailedClawHub: false,
|
||||
@@ -148,6 +155,15 @@ export function parseReleaseVerifyBetaArgs(argv: string[]): ReleaseVerifyBetaArg
|
||||
case "--registry":
|
||||
parsed.registry = next();
|
||||
break;
|
||||
case "--workflow-ref":
|
||||
parsed.workflowRef = 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;
|
||||
@@ -319,6 +335,8 @@ function verifyWorkflowRun(params: {
|
||||
id: string;
|
||||
label: string;
|
||||
repo: string;
|
||||
expectedWorkflowName: string;
|
||||
expectedHeadBranch?: string;
|
||||
rerunFailed: boolean;
|
||||
}): WorkflowRunSummary {
|
||||
const raw = runCommand("gh", [
|
||||
@@ -328,12 +346,30 @@ function verifyWorkflowRun(params: {
|
||||
"--repo",
|
||||
params.repo,
|
||||
"--json",
|
||||
"status,conclusion,url,createdAt,updatedAt,jobs",
|
||||
"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);
|
||||
if (params.expectedHeadBranch !== undefined && headBranch !== params.expectedHeadBranch) {
|
||||
throw new Error(
|
||||
`${params.label}: run ${params.id} branch is ${headBranch ?? "<missing>"}, expected ${params.expectedHeadBranch}.`,
|
||||
);
|
||||
}
|
||||
const status = readString(run.status);
|
||||
const conclusion = readString(run.conclusion);
|
||||
const jobs = Array.isArray(run.jobs) ? run.jobs.filter(isRecord) : [];
|
||||
@@ -391,6 +427,21 @@ function formatDuration(seconds: number | undefined): string {
|
||||
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 } = {},
|
||||
@@ -418,13 +469,27 @@ export async function verifyBetaRelease(
|
||||
lines.push("openclaw postpublish verifier OK");
|
||||
}
|
||||
|
||||
const npmPlugins = collectPublishablePluginPackages(rootDir);
|
||||
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) {
|
||||
verifyNpmPackage(plugin.packageName, args.version, args.distTag);
|
||||
}
|
||||
lines.push(`plugin npm OK: ${npmPlugins.length}`);
|
||||
|
||||
const clawHubPlugins = collectClawHubPublishablePluginPackages(rootDir);
|
||||
const clawHubPlugins = collectClawHubPublishablePluginPackages(rootDir, {
|
||||
packageNames: args.pluginSelection.length > 0 ? args.pluginSelection : undefined,
|
||||
});
|
||||
assertSelectedPackagesResolved({
|
||||
label: "ClawHub plugin",
|
||||
selection: args.pluginSelection,
|
||||
packages: clawHubPlugins,
|
||||
});
|
||||
for (const plugin of clawHubPlugins) {
|
||||
await verifyClawHubPackage({
|
||||
registry: args.registry,
|
||||
@@ -442,6 +507,8 @@ export async function verifyBetaRelease(
|
||||
id: args.workflowRuns.fullReleaseValidation,
|
||||
label: "Full Release Validation",
|
||||
repo: args.repo,
|
||||
expectedWorkflowName: "Full Release Validation",
|
||||
expectedHeadBranch: args.workflowRef,
|
||||
rerunFailed: false,
|
||||
}),
|
||||
);
|
||||
@@ -452,6 +519,8 @@ export async function verifyBetaRelease(
|
||||
id: args.workflowRuns.pluginNpm,
|
||||
label: "Plugin NPM Release",
|
||||
repo: args.repo,
|
||||
expectedWorkflowName: "Plugin NPM Release",
|
||||
expectedHeadBranch: args.workflowRef,
|
||||
rerunFailed: false,
|
||||
}),
|
||||
);
|
||||
@@ -462,6 +531,8 @@ export async function verifyBetaRelease(
|
||||
id: args.workflowRuns.pluginClawHub,
|
||||
label: "Plugin ClawHub Release",
|
||||
repo: args.repo,
|
||||
expectedWorkflowName: "Plugin ClawHub Release",
|
||||
expectedHeadBranch: args.workflowRef,
|
||||
rerunFailed: args.rerunFailedClawHub,
|
||||
}),
|
||||
);
|
||||
@@ -472,6 +543,8 @@ export async function verifyBetaRelease(
|
||||
id: args.workflowRuns.openclawNpm,
|
||||
label: "OpenClaw NPM Release",
|
||||
repo: args.repo,
|
||||
expectedWorkflowName: "OpenClaw NPM Release",
|
||||
expectedHeadBranch: args.workflowRef,
|
||||
rerunFailed: false,
|
||||
}),
|
||||
);
|
||||
@@ -482,6 +555,8 @@ export async function verifyBetaRelease(
|
||||
id: args.workflowRuns.npmTelegram,
|
||||
label: "NPM Telegram Beta E2E",
|
||||
repo: args.repo,
|
||||
expectedWorkflowName: "NPM Telegram Beta E2E",
|
||||
expectedHeadBranch: args.workflowRef,
|
||||
rerunFailed: false,
|
||||
}),
|
||||
);
|
||||
@@ -503,6 +578,7 @@ export async function verifyBetaRelease(
|
||||
releaseVersion: args.version,
|
||||
releaseTag: args.tag,
|
||||
npmDistTag: args.distTag,
|
||||
pluginSelection: args.pluginSelection,
|
||||
openclawNpmIntegrity: openclawNpm.integrity,
|
||||
githubReleaseUrl: releaseUrl,
|
||||
pluginNpmPackageCount: npmPlugins.length,
|
||||
|
||||
@@ -161,7 +161,7 @@ export function collectInstalledBundledRuntimeSidecarPaths(packageRoot: string):
|
||||
|
||||
export function normalizeInstalledBinaryVersion(output: string): string {
|
||||
const trimmed = output.trim();
|
||||
const versionMatch = /\b\d{4}\.\d{1,2}\.\d{1,2}(?:-\d+|-beta\.\d+)?\b/u.exec(trimmed);
|
||||
const versionMatch = /\b\d{4}\.\d{1,2}\.\d{1,2}(?:-\d+|-(?:alpha|beta)\.\d+)?\b/u.exec(trimmed);
|
||||
return versionMatch?.[0] ?? trimmed;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ Options:
|
||||
--full-release-run <id> Reuse successful Full Release Validation run.
|
||||
--npm-preflight-run <id> Reuse successful OpenClaw NPM Release preflight run.
|
||||
--skip-dispatch Require both run ids; do not dispatch workflows.
|
||||
--skip-local-generated-check Do not run local generated release baseline checks before dispatch.
|
||||
--skip-parallels Do not run local Parallels fresh/update beta smoke.
|
||||
--skip-telegram Do not run NPM Telegram E2E against the prepared tarball.
|
||||
--telegram-provider-mode <mode> mock-openai|live-frontier. Default: ${DEFAULT_TELEGRAM_PROVIDER_MODE}
|
||||
@@ -57,6 +58,7 @@ export function parseArgs(argv) {
|
||||
pluginPublishScope: DEFAULT_PLUGIN_SCOPE,
|
||||
plugins: "",
|
||||
skipDispatch: false,
|
||||
skipLocalGeneratedCheck: false,
|
||||
skipParallels: false,
|
||||
skipTelegram: false,
|
||||
telegramProviderMode: DEFAULT_TELEGRAM_PROVIDER_MODE,
|
||||
@@ -89,6 +91,9 @@ export function parseArgs(argv) {
|
||||
case "--skip-dispatch":
|
||||
options.skipDispatch = true;
|
||||
break;
|
||||
case "--skip-local-generated-check":
|
||||
options.skipLocalGeneratedCheck = true;
|
||||
break;
|
||||
case "--skip-parallels":
|
||||
options.skipParallels = true;
|
||||
break;
|
||||
@@ -256,6 +261,14 @@ function runAndEcho(command, args) {
|
||||
return `${result.stdout ?? ""}${result.stderr ?? ""}`;
|
||||
}
|
||||
|
||||
function runLocalGeneratedCheckIfNeeded(options) {
|
||||
if (options.skipLocalGeneratedCheck) {
|
||||
return { status: "skipped", reason: "operator skipped --skip-local-generated-check" };
|
||||
}
|
||||
run("pnpm", ["release:generated:check"]);
|
||||
return { status: "passed", command: "pnpm release:generated:check" };
|
||||
}
|
||||
|
||||
export function parseRunIdFromDispatchOutput(output) {
|
||||
return output.match(/actions\/runs\/([0-9]+)/u)?.[1] ?? "";
|
||||
}
|
||||
@@ -446,6 +459,9 @@ export function buildPublishCommand(options) {
|
||||
["release_profile", options.releaseProfile],
|
||||
["wait_for_clawhub", "false"],
|
||||
];
|
||||
if (options.npmTelegramRunId) {
|
||||
fields.push(["npm_telegram_run_id", options.npmTelegramRunId]);
|
||||
}
|
||||
if (options.plugins.trim()) {
|
||||
fields.push(["plugins", options.plugins]);
|
||||
}
|
||||
@@ -506,7 +522,7 @@ function validateFullManifest(manifest, params) {
|
||||
|
||||
async function runParallelsIfNeeded(options) {
|
||||
if (options.skipParallels) {
|
||||
return { status: "skipped" };
|
||||
return { status: "skipped", reason: "operator skipped --skip-parallels" };
|
||||
}
|
||||
const version = options.tag.replace(/^v/u, "");
|
||||
run("pnpm", [
|
||||
@@ -559,6 +575,7 @@ async function main() {
|
||||
options.workflowRef ||= currentBranch();
|
||||
options.outputDir ||= join(".artifacts", "release-candidate", options.tag);
|
||||
const targetSha = gitRevParse(`${options.tag}^{}`);
|
||||
const localGeneratedCheck = runLocalGeneratedCheckIfNeeded(options);
|
||||
|
||||
if (!options.fullReleaseRunId && !options.skipDispatch) {
|
||||
const workflowFile = "full-release-validation.yml";
|
||||
@@ -647,6 +664,7 @@ async function main() {
|
||||
|
||||
const parallels = await runParallelsIfNeeded(options);
|
||||
const npmTelegram = await runTelegramIfNeeded(options, npmArtifactName);
|
||||
options.npmTelegramRunId = npmTelegram.runId ?? "";
|
||||
const pluginNpmPlan = await collectPluginPlanWithRetry(
|
||||
"scripts/plugin-npm-release-plan.ts",
|
||||
options,
|
||||
@@ -670,6 +688,7 @@ async function main() {
|
||||
npmPreflight: npmArtifactName,
|
||||
fullReleaseValidation: fullArtifactName,
|
||||
},
|
||||
localGeneratedCheck,
|
||||
tarball: {
|
||||
name: basename(tarballPath),
|
||||
sha256: actualTarballSha,
|
||||
@@ -695,12 +714,15 @@ async function main() {
|
||||
`- npm preflight: ${options.npmPreflightRunId} ${npmRun.url}`,
|
||||
`- npm preflight artifact: ${npmArtifactName}`,
|
||||
`- full release artifact: ${fullArtifactName}`,
|
||||
`- local generated release checks: ${localGeneratedCheck.status}${
|
||||
localGeneratedCheck.reason ? ` (${localGeneratedCheck.reason})` : ""
|
||||
}`,
|
||||
`- tarball: ${basename(tarballPath)}`,
|
||||
`- tarball sha256: ${actualTarballSha}`,
|
||||
`- npm dist-tag: ${options.npmDistTag}`,
|
||||
`- plugin npm plan: ${pluginNpmPlan.packages?.length ?? 0} packages`,
|
||||
`- ClawHub plan: ${pluginClawHubPlan.packages?.length ?? 0} packages`,
|
||||
`- Parallels: ${parallels.status}`,
|
||||
`- Parallels: ${parallels.status}${parallels.reason ? ` (${parallels.reason})` : ""}`,
|
||||
`- NPM Telegram E2E: ${npmTelegram.status}${
|
||||
npmTelegram.runId ? ` ${npmTelegram.runId} ${npmTelegram.url}` : ""
|
||||
}`,
|
||||
|
||||
Reference in New Issue
Block a user