mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix: rely on ClawHub plugin publish checks
This commit is contained in:
@@ -197,12 +197,6 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Verify OpenClaw ClawHub package ownership
|
||||
if: steps.plan.outputs.has_candidates == 'true'
|
||||
env:
|
||||
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
|
||||
run: node --import tsx scripts/plugin-clawhub-owner-preflight.ts .local/plugin-clawhub-release-plan.json
|
||||
|
||||
validate_release_publish_approval:
|
||||
name: Validate release publish approval
|
||||
needs: preview_plugins_clawhub
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { resolve } from "node:path";
|
||||
import { validateExternalCodePluginPackageJson } from "../../packages/plugin-package-contract/src/index.ts";
|
||||
import { readBoundedResponseText } from "./bounded-response.ts";
|
||||
import {
|
||||
collectExtensionPackageJsonCandidates,
|
||||
collectChangedPathsFromGitRange,
|
||||
@@ -64,19 +63,12 @@ type PluginReleasePlan = {
|
||||
skippedPublished: PluginReleasePlanItem[];
|
||||
};
|
||||
|
||||
type ClawHubPackageOwnerDetail = {
|
||||
owner?: {
|
||||
handle?: unknown;
|
||||
} | null;
|
||||
};
|
||||
|
||||
type ClawHubPublishablePluginPackageFilters = {
|
||||
extensionIds?: readonly string[];
|
||||
packageNames?: readonly string[];
|
||||
};
|
||||
|
||||
const CLAWHUB_DEFAULT_REGISTRY = "https://clawhub.ai";
|
||||
const CLAWHUB_RESPONSE_BODY_MAX_BYTES = 1024 * 1024;
|
||||
const SAFE_EXTENSION_ID_RE = /^[a-z0-9][a-z0-9._-]*$/;
|
||||
const CLAWHUB_SHARED_RELEASE_INPUT_PATHS = [
|
||||
".github/workflows/plugin-clawhub-release.yml",
|
||||
@@ -88,7 +80,6 @@ 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",
|
||||
@@ -114,19 +105,6 @@ function formatClawHubPackageArtifactName(
|
||||
return `clawhub-package-${safeName}-${plugin.version}`;
|
||||
}
|
||||
|
||||
async function readClawHubPackageOwnerDetail(
|
||||
response: Response,
|
||||
packageName: string,
|
||||
): Promise<ClawHubPackageOwnerDetail> {
|
||||
return JSON.parse(
|
||||
await readBoundedResponseText(
|
||||
response,
|
||||
`ClawHub package owner response for ${packageName}`,
|
||||
CLAWHUB_RESPONSE_BODY_MAX_BYTES,
|
||||
),
|
||||
) as ClawHubPackageOwnerDetail;
|
||||
}
|
||||
|
||||
export function collectClawHubPublishablePluginPackages(
|
||||
rootDir = resolve("."),
|
||||
filters: ClawHubPublishablePluginPackageFilters = {},
|
||||
@@ -379,59 +357,6 @@ 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 readClawHubPackageOwnerDetail(response, plugin.packageName);
|
||||
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[];
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
#!/usr/bin/env -S node --import tsx
|
||||
// Plugin Clawhub Owner Preflight script supports OpenClaw repository automation.
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@ import { delimiter, join } from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
collectClawHubPublishablePluginPackages,
|
||||
collectClawHubOpenClawOwnerErrors,
|
||||
collectClawHubVersionGateErrors,
|
||||
collectPluginClawHubReleasePathsFromGitRange,
|
||||
collectPluginClawHubReleasePlan,
|
||||
@@ -381,78 +380,6 @@ describe("collectPluginClawHubReleasePlan", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("collectClawHubOpenClawOwnerErrors", () => {
|
||||
it("requires OpenClaw-scoped release candidates to already belong to the OpenClaw publisher", async () => {
|
||||
const errors = await collectClawHubOpenClawOwnerErrors({
|
||||
plugins: [
|
||||
{ packageName: "@openclaw/demo-plugin" },
|
||||
{ packageName: "@openclaw/missing-plugin" },
|
||||
{ packageName: "@other/safe-plugin" },
|
||||
],
|
||||
registryBaseUrl: "https://clawhub.ai",
|
||||
fetchImpl: async (url) => {
|
||||
const pathname = new URL(url instanceof Request ? url.url : url).pathname;
|
||||
if (pathname.includes("%40openclaw%2Fmissing-plugin")) {
|
||||
return new Response("not found", { status: 404 });
|
||||
}
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
owner: { handle: "steipete" },
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
expect(errors).toEqual([
|
||||
"@openclaw/demo-plugin: ClawHub package owner must be @openclaw; got @steipete.",
|
||||
"@openclaw/missing-plugin: ClawHub package row must already exist under @openclaw before OpenClaw release publish.",
|
||||
]);
|
||||
});
|
||||
|
||||
it("passes when OpenClaw-scoped release candidates belong to the OpenClaw publisher", async () => {
|
||||
const errors = await collectClawHubOpenClawOwnerErrors({
|
||||
plugins: [{ packageName: "@openclaw/demo-plugin" }],
|
||||
registryBaseUrl: "https://clawhub.ai",
|
||||
fetchImpl: async () =>
|
||||
new Response(JSON.stringify({ owner: { handle: "openclaw" } }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
});
|
||||
|
||||
expect(errors).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("bounds ClawHub owner metadata response bodies", async () => {
|
||||
await expect(
|
||||
collectClawHubOpenClawOwnerErrors({
|
||||
plugins: [{ packageName: "@openclaw/demo-plugin" }],
|
||||
registryBaseUrl: "https://clawhub.ai",
|
||||
fetchImpl: async () =>
|
||||
new Response("{}", {
|
||||
status: 200,
|
||||
headers: { "content-length": String(1024 * 1024 + 1) },
|
||||
}),
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
"ClawHub package owner response for @openclaw/demo-plugin response body exceeded 1048576 bytes.",
|
||||
);
|
||||
});
|
||||
|
||||
it("bounds streamed ClawHub owner metadata bodies", async () => {
|
||||
await expect(
|
||||
collectClawHubOpenClawOwnerErrors({
|
||||
plugins: [{ packageName: "@openclaw/demo-plugin" }],
|
||||
registryBaseUrl: "https://clawhub.ai",
|
||||
fetchImpl: async () => new Response("x".repeat(1024 * 1024 + 1), { status: 200 }),
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
"ClawHub package owner response for @openclaw/demo-plugin response body exceeded 1048576 bytes.",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("plugin-clawhub-publish.sh", () => {
|
||||
it("previews the publish command through the ClawHub CLI dry-run preflight", () => {
|
||||
const repoDir = createTempPluginRepo();
|
||||
|
||||
Reference in New Issue
Block a user