Files
openclaw/scripts/lib/openclaw-release-clawhub-plan.ts
Patrick Erichsen 6cf06e8e7e ci: split plugin ClawHub publishing paths
* feat: partition clawhub plugin release candidates

* fix: read clawhub trusted publisher config endpoint

* feat: split clawhub plugin bootstrap workflow

* ci: split plugin clawhub publish paths

* ci: pin clawhub package publish workflow

* ci: keep clawhub bootstrap token out of builds

* ci: fix clawhub release dry-run gating

* ci: align clawhub oidc publish refs

* ci: make clawhub bootstrap recovery idempotent

* ci: route clawhub repair candidates through bootstrap

* ci: preserve tideclaw alpha clawhub guards

* ci: simplify clawhub release ref handling

* ci: extract clawhub release routing plan

* ci: extract clawhub release runtime state

* test: guard clawhub release helper executability

* ci: pin ClawHub CLI for plugin publishing

* ci: allow historical ClawHub dry-run validation

* ci: fix ClawHub bootstrap token handoff
2026-06-12 20:16:06 -07:00

315 lines
9.9 KiB
TypeScript

// OpenClaw release ClawHub plan script supports release workflow routing.
import { resolve } from "node:path";
import {
collectPluginClawHubReleasePlan,
type PublishablePluginPackage,
} from "./plugin-clawhub-release.ts";
import {
parsePluginReleaseSelection,
parsePluginReleaseSelectionMode,
type PluginReleaseSelectionMode,
} from "./plugin-npm-release.ts";
type ClawHubPlanPackage = Pick<PublishablePluginPackage, "packageName">;
type ClawHubDispatchInputs = Record<string, string>;
type ClawHubDispatchTarget = {
workflow: "plugin-clawhub-release.yml" | "plugin-clawhub-new.yml";
ref: string;
shouldDispatch: boolean;
packages: string[];
inputs: ClawHubDispatchInputs;
};
export type OpenClawReleaseClawHubPlanArgs = {
releaseTag: string;
releasePublishBranch: string;
releasePublishRunId: string;
pluginPublishScope: PluginReleaseSelectionMode;
plugins: string[];
};
export type OpenClawReleaseClawHubPlan = {
clawHubWorkflowRef: string;
releasePublishBranch: string;
normal: ClawHubDispatchTarget;
bootstrap: ClawHubDispatchTarget;
summary: {
normalCount: number;
bootstrapCount: number;
missingTrustedPublisherCount: number;
normalPlugins: string;
bootstrapPlugins: string;
missingTrustedPlugins: string;
};
verifier: {
clawHubWorkflowRef: string;
};
};
export type OpenClawReleaseClawHubRuntimeStateArgs = {
repository: string;
waitForClawHub: boolean;
forceSkipClawHub: boolean;
normalRunId?: string;
bootstrapRunId?: string;
bootstrapCompleted: boolean;
};
export type OpenClawReleaseClawHubRuntimeState = {
verifierArgs: string[];
proofLines: {
normal: string;
bootstrap: string;
};
};
function requireArg(value: string | undefined, label: string): string {
const trimmed = value?.trim();
if (!trimmed) {
throw new Error(`${label} is required.`);
}
return trimmed;
}
function packageNames(packages: readonly ClawHubPlanPackage[]): string[] {
return packages.map((plugin) => plugin.packageName);
}
function joinPackageNames(packages: readonly string[]): string {
return packages.join(",");
}
function optionalArg(value: string | undefined): string | undefined {
const trimmed = value?.trim();
return trimmed ? trimmed : undefined;
}
function runUrl(repository: string, runId: string): string {
return `https://github.com/${repository}/actions/runs/${runId}`;
}
function assertNoPackageOverlap(
normalPackages: readonly string[],
bootstrapPackages: readonly string[],
) {
const normalPackageSet = new Set(normalPackages);
const overlap = bootstrapPackages.filter((packageName) => normalPackageSet.has(packageName));
if (overlap.length > 0) {
throw new Error(
`ClawHub release plan routed package(s) to both normal and bootstrap workflows: ${overlap.join(", ")}.`,
);
}
}
function createDispatchTarget(params: {
workflow: ClawHubDispatchTarget["workflow"];
ref: string;
packages: readonly string[];
releasePublishRunId: string;
releasePublishBranch: string;
includePublishScope: boolean;
}): ClawHubDispatchTarget {
if (params.packages.length === 0) {
return {
workflow: params.workflow,
ref: params.ref,
shouldDispatch: false,
packages: [],
inputs: {},
};
}
const plugins = joinPackageNames(params.packages);
return {
workflow: params.workflow,
ref: params.ref,
shouldDispatch: true,
packages: [...params.packages],
inputs: {
...(params.includePublishScope ? { publish_scope: "selected" } : {}),
plugins,
release_publish_run_id: params.releasePublishRunId,
release_publish_branch: params.releasePublishBranch,
},
};
}
export function buildOpenClawReleaseClawHubRuntimeState(
args: OpenClawReleaseClawHubRuntimeStateArgs,
): OpenClawReleaseClawHubRuntimeState {
const repository = requireArg(args.repository, "repository");
const normalRunId = optionalArg(args.normalRunId);
const bootstrapRunId = optionalArg(args.bootstrapRunId);
const shouldIncludeNormalRun =
!args.forceSkipClawHub && normalRunId !== undefined && args.waitForClawHub;
const shouldIncludeBootstrapRun =
!args.forceSkipClawHub && bootstrapRunId !== undefined && args.bootstrapCompleted;
const shouldVerifyClawHubPackages =
bootstrapRunId !== undefined &&
args.bootstrapCompleted &&
(normalRunId === undefined || args.waitForClawHub);
const shouldSkipClawHubPackages =
args.forceSkipClawHub || !(shouldIncludeNormalRun || shouldVerifyClawHubPackages);
const verifierArgs = shouldSkipClawHubPackages ? ["--skip-clawhub"] : [];
if (shouldIncludeNormalRun) {
verifierArgs.push("--plugin-clawhub-run", normalRunId);
}
if (shouldIncludeBootstrapRun) {
verifierArgs.push("--plugin-clawhub-bootstrap-run", bootstrapRunId);
}
let normalProofLine = "- plugin ClawHub publish: no normal OIDC candidates";
if (normalRunId !== undefined && args.waitForClawHub) {
normalProofLine = `- plugin ClawHub publish: ${runUrl(repository, normalRunId)}`;
} else if (normalRunId !== undefined) {
normalProofLine = `- plugin ClawHub publish: dispatched separately, not awaited by this proof: ${runUrl(repository, normalRunId)}`;
}
let bootstrapProofLine = "- plugin ClawHub bootstrap: not needed";
if (bootstrapRunId !== undefined && (args.bootstrapCompleted || args.waitForClawHub)) {
bootstrapProofLine = `- plugin ClawHub bootstrap: ${runUrl(repository, bootstrapRunId)}`;
} else if (bootstrapRunId !== undefined) {
bootstrapProofLine = `- plugin ClawHub bootstrap: dispatched separately, not awaited by this proof: ${runUrl(repository, bootstrapRunId)}`;
}
return {
verifierArgs,
proofLines: {
normal: normalProofLine,
bootstrap: bootstrapProofLine,
},
};
}
export function parseOpenClawReleaseClawHubPlanArgs(
argv: string[],
): OpenClawReleaseClawHubPlanArgs {
const values = [...argv];
if (values[0] === "--") {
values.shift();
}
let releaseTag: string | undefined;
let releasePublishBranch: string | undefined;
let releasePublishRunId: string | undefined;
let pluginPublishScope: PluginReleaseSelectionMode | undefined;
let plugins: string[] = [];
let pluginsFlagProvided = false;
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 "--release-tag":
releaseTag = next();
break;
case "--release-publish-branch":
releasePublishBranch = next();
break;
case "--release-publish-run-id":
releasePublishRunId = next();
break;
case "--plugin-publish-scope":
pluginPublishScope = parsePluginReleaseSelectionMode(next());
break;
case "--plugins":
plugins = parsePluginReleaseSelection(next());
pluginsFlagProvided = true;
break;
default:
throw new Error(`Unknown argument: ${arg}`);
}
}
const resolvedPluginPublishScope = pluginPublishScope ?? "all-publishable";
if (pluginsFlagProvided && plugins.length === 0) {
throw new Error("--plugins must include at least one package name.");
}
if (resolvedPluginPublishScope === "selected" && !pluginsFlagProvided) {
throw new Error("plugin-publish-scope=selected requires --plugins.");
}
if (resolvedPluginPublishScope === "all-publishable" && pluginsFlagProvided) {
throw new Error("plugin-publish-scope=all-publishable must not be combined with --plugins.");
}
return {
releaseTag: requireArg(releaseTag, "--release-tag"),
releasePublishBranch: requireArg(releasePublishBranch, "--release-publish-branch"),
releasePublishRunId: requireArg(releasePublishRunId, "--release-publish-run-id"),
pluginPublishScope: resolvedPluginPublishScope,
plugins,
};
}
export async function buildOpenClawReleaseClawHubPlan(
args: OpenClawReleaseClawHubPlanArgs,
options: {
rootDir?: string;
fetchImpl?: typeof fetch;
registryBaseUrl?: string;
} = {},
): Promise<OpenClawReleaseClawHubPlan> {
const releaseTag = requireArg(args.releaseTag, "releaseTag");
const releasePublishBranch = requireArg(args.releasePublishBranch, "releasePublishBranch");
const releasePublishRunId = requireArg(args.releasePublishRunId, "releasePublishRunId");
const plan = await collectPluginClawHubReleasePlan({
rootDir: options.rootDir ?? resolve("."),
selection: args.plugins,
selectionMode: args.pluginPublishScope,
fetchImpl: options.fetchImpl,
registryBaseUrl: options.registryBaseUrl,
});
const normalPackages = packageNames(plan.candidates);
const bootstrapPackages = [
...packageNames(plan.bootstrapCandidates),
...packageNames(plan.missingTrustedPublisher),
];
const missingTrustedPlugins = packageNames(plan.missingTrustedPublisher);
assertNoPackageOverlap(normalPackages, bootstrapPackages);
return {
clawHubWorkflowRef: releaseTag,
releasePublishBranch,
normal: createDispatchTarget({
workflow: "plugin-clawhub-release.yml",
ref: releaseTag,
packages: normalPackages,
releasePublishRunId,
releasePublishBranch,
includePublishScope: true,
}),
bootstrap: createDispatchTarget({
workflow: "plugin-clawhub-new.yml",
ref: releaseTag,
packages: bootstrapPackages,
releasePublishRunId,
releasePublishBranch,
includePublishScope: false,
}),
summary: {
normalCount: normalPackages.length,
bootstrapCount: bootstrapPackages.length,
missingTrustedPublisherCount: missingTrustedPlugins.length,
normalPlugins: joinPackageNames(normalPackages),
bootstrapPlugins: joinPackageNames(bootstrapPackages),
missingTrustedPlugins: joinPackageNames(missingTrustedPlugins),
},
verifier: {
clawHubWorkflowRef: releaseTag,
},
};
}