ci(release): harden clawhub plugin publish

This commit is contained in:
Peter Steinberger
2026-05-04 10:08:09 +01:00
parent 5b528f4dfe
commit b37fba7c07
6 changed files with 252 additions and 18 deletions

View File

@@ -60,6 +60,12 @@ type PluginReleasePlan = {
skippedPublished: PluginReleasePlanItem[];
};
type ClawHubPackageOwnerDetail = {
owner?: {
handle?: unknown;
} | null;
};
type ClawHubPublishablePluginPackageFilters = {
extensionIds?: readonly string[];
packageNames?: readonly string[];
@@ -76,6 +82,7 @@ const CLAWHUB_SHARED_RELEASE_INPUT_PATHS = [
"scripts/lib/npm-publish-plan.mjs",
"scripts/lib/plugin-npm-release.ts",
"scripts/lib/plugin-clawhub-release.ts",
"scripts/plugin-clawhub-owner-preflight.ts",
"scripts/openclaw-npm-release-check.ts",
"scripts/plugin-clawhub-publish.sh",
"scripts/plugin-clawhub-release-check.ts",
@@ -343,6 +350,59 @@ async function isPluginVersionPublishedOnClawHub(
);
}
export async function collectClawHubOpenClawOwnerErrors(params: {
plugins: readonly Pick<PublishablePluginPackage, "packageName">[];
requiredOwnerHandle?: string;
registryBaseUrl?: string;
fetchImpl?: typeof fetch;
}): Promise<string[]> {
const fetchImpl = params.fetchImpl ?? fetch;
const requiredOwnerHandle = params.requiredOwnerHandle ?? "openclaw";
const errors: string[] = [];
await Promise.all(
params.plugins.map(async (plugin) => {
if (!plugin.packageName.startsWith("@openclaw/")) {
return;
}
const url = new URL(
`/api/v1/packages/${encodeURIComponent(plugin.packageName)}`,
getRegistryBaseUrl(params.registryBaseUrl),
);
const response = await fetchImpl(url, {
method: "GET",
headers: {
Accept: "application/json",
},
});
if (response.status === 404) {
errors.push(
`${plugin.packageName}: ClawHub package row must already exist under @${requiredOwnerHandle} before OpenClaw release publish.`,
);
return;
}
if (!response.ok) {
errors.push(
`${plugin.packageName}: failed to query ClawHub owner: ${response.status} ${response.statusText}`,
);
return;
}
const detail = (await response.json()) as ClawHubPackageOwnerDetail;
const ownerHandle = typeof detail.owner?.handle === "string" ? detail.owner.handle : null;
if (ownerHandle !== requiredOwnerHandle) {
errors.push(
`${plugin.packageName}: ClawHub package owner must be @${requiredOwnerHandle}; got ${ownerHandle ? `@${ownerHandle}` : "<missing>"}.`,
);
}
}),
);
return errors.toSorted();
}
export async function collectPluginClawHubReleasePlan(params?: {
rootDir?: string;
selection?: string[];

View File

@@ -0,0 +1,44 @@
#!/usr/bin/env -S node --import tsx
import { readFileSync } from "node:fs";
import { pathToFileURL } from "node:url";
import { collectClawHubOpenClawOwnerErrors } from "./lib/plugin-clawhub-release.ts";
type ReleasePlanFile = {
candidates?: Array<{
packageName?: unknown;
}>;
};
export async function runClawHubOwnerPreflight(argv: string[]) {
const planPath = argv[0];
if (!planPath) {
throw new Error("usage: plugin-clawhub-owner-preflight.ts <release-plan.json>");
}
const parsed = JSON.parse(readFileSync(planPath, "utf8")) as ReleasePlanFile;
const candidates = (parsed.candidates ?? [])
.filter(
(candidate): candidate is { packageName: string } =>
typeof candidate.packageName === "string",
)
.map((candidate) => ({ packageName: candidate.packageName }));
const errors = await collectClawHubOpenClawOwnerErrors({ plugins: candidates });
if (errors.length > 0) {
throw new Error(
`ClawHub OpenClaw package ownership preflight failed:\n${errors.map((error) => `- ${error}`).join("\n")}`,
);
}
console.log(`ClawHub OpenClaw owner preflight passed for ${candidates.length} candidate(s).`);
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
try {
await runClawHubOwnerPreflight(process.argv.slice(2));
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
}
}

View File

@@ -152,4 +152,17 @@ if [[ "${mode}" == "--dry-run" ]]; then
exit 0
fi
CLAWHUB_WORKDIR="${clawhub_workdir}" "${publish_cmd[@]}"
publish_log="${pack_dir}/publish.log"
for attempt in $(seq 1 "${OPENCLAW_CLAWHUB_PUBLISH_ATTEMPTS:-8}"); do
if CLAWHUB_WORKDIR="${clawhub_workdir}" "${publish_cmd[@]}" > >(tee "${publish_log}") 2>&1; then
exit 0
fi
if ! grep -Eqi "rate limit|too many requests|\\b429\\b" "${publish_log}"; then
exit 1
fi
echo "ClawHub publish hit a rate limit; retrying (${attempt}/${OPENCLAW_CLAWHUB_PUBLISH_ATTEMPTS:-8})." >&2
sleep "${OPENCLAW_CLAWHUB_PUBLISH_RETRY_DELAY_SECONDS:-60}"
done
echo "ClawHub publish failed after ${OPENCLAW_CLAWHUB_PUBLISH_ATTEMPTS:-8} attempts." >&2
exit 1

View File

@@ -124,8 +124,8 @@ function sleep(ms) {
}
async function packPublishedPackage(spec, destinationDir) {
const attempts = Number.parseInt(process.env.OPENCLAW_PLUGIN_NPM_VERIFY_ATTEMPTS ?? "6", 10);
const delayMs = Number.parseInt(process.env.OPENCLAW_PLUGIN_NPM_VERIFY_DELAY_MS ?? "5000", 10);
const attempts = Number.parseInt(process.env.OPENCLAW_PLUGIN_NPM_VERIFY_ATTEMPTS ?? "90", 10);
const delayMs = Number.parseInt(process.env.OPENCLAW_PLUGIN_NPM_VERIFY_DELAY_MS ?? "10000", 10);
let lastError;
for (let attempt = 1; attempt <= attempts; attempt += 1) {
try {
@@ -133,6 +133,9 @@ async function packPublishedPackage(spec, destinationDir) {
} catch (error) {
lastError = error;
if (attempt < attempts) {
console.error(
`npm pack ${spec} not visible yet (attempt ${attempt}/${attempts}); retrying in ${delayMs}ms...`,
);
await sleep(delayMs);
}
}