Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
693
scripts/lib/plugin-clawhub-release.ts
Normal file
693
scripts/lib/plugin-clawhub-release.ts
Normal file
@@ -0,0 +1,693 @@
|
||||
// Plugin Clawhub Release script supports OpenClaw repository automation.
|
||||
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 {
|
||||
assertPluginReleaseDependencyFreshness,
|
||||
collectExtensionPackageJsonCandidates,
|
||||
collectChangedPathsFromGitRange,
|
||||
collectChangedExtensionIdsFromPaths,
|
||||
collectPublishablePluginPackageErrors,
|
||||
collectRequiredLatestDependencies,
|
||||
assertPluginReleaseVersionFloors,
|
||||
parsePluginReleaseArgs,
|
||||
resolvePublishablePluginVersion,
|
||||
resolveGitCommitSha,
|
||||
resolveChangedPublishablePluginPackages,
|
||||
resolveSelectedPublishablePluginPackages,
|
||||
type GitRangeSelection,
|
||||
type NpmLatestVersionResolver,
|
||||
type PluginReleaseSelectionMode,
|
||||
type RequiredLatestDependency,
|
||||
} from "./plugin-npm-release.ts";
|
||||
|
||||
export {
|
||||
assertPluginReleaseDependencyFreshness,
|
||||
assertPluginReleaseVersionFloors,
|
||||
parsePluginReleaseArgs,
|
||||
};
|
||||
|
||||
type PluginPackageJson = {
|
||||
name?: string;
|
||||
version?: string;
|
||||
private?: boolean;
|
||||
openclaw?: {
|
||||
extensions?: string[];
|
||||
install?: {
|
||||
npmSpec?: string;
|
||||
};
|
||||
compat?: {
|
||||
pluginApi?: string;
|
||||
minGatewayVersion?: string;
|
||||
};
|
||||
build?: {
|
||||
openclawVersion?: string;
|
||||
pluginSdkVersion?: string;
|
||||
};
|
||||
release?: {
|
||||
publishToClawHub?: boolean;
|
||||
publishToNpm?: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type PublishablePluginPackage = {
|
||||
extensionId: string;
|
||||
packageDir: string;
|
||||
packageName: string;
|
||||
version: string;
|
||||
channel: "stable" | "alpha" | "beta";
|
||||
publishTag: "latest" | "alpha" | "beta";
|
||||
requiredLatestDependencies?: RequiredLatestDependency[];
|
||||
};
|
||||
|
||||
type PluginReleasePlanItem = PublishablePluginPackage & {
|
||||
alreadyPublished: boolean;
|
||||
artifactName: string;
|
||||
};
|
||||
|
||||
type PluginReleasePlan = {
|
||||
all: PluginReleasePlanItem[];
|
||||
candidates: PluginReleasePlanItem[];
|
||||
bootstrapCandidates: PluginReleasePlanItem[];
|
||||
missingTrustedPublisher: PluginReleasePlanItem[];
|
||||
skippedPublished: PluginReleasePlanItem[];
|
||||
};
|
||||
|
||||
type ClawHubTrustedPublisherDetail = {
|
||||
trustedPublisher?: unknown;
|
||||
};
|
||||
|
||||
type ClawHubTrustedPublisherConfig = {
|
||||
repository?: unknown;
|
||||
workflowFilename?: unknown;
|
||||
environment?: unknown;
|
||||
};
|
||||
|
||||
type PluginReleasePlanItemWithPackageState = PluginReleasePlanItem & {
|
||||
packageExists: boolean;
|
||||
hasTrustedPublisher: boolean;
|
||||
};
|
||||
|
||||
type ClawHubPublishablePluginPackageFilters = {
|
||||
extensionIds?: readonly string[];
|
||||
packageNames?: readonly string[];
|
||||
};
|
||||
|
||||
const CLAWHUB_DEFAULT_REGISTRY = "https://clawhub.ai";
|
||||
const CLAWHUB_REQUEST_TIMEOUT_MS = 30_000;
|
||||
const CLAWHUB_RESPONSE_BODY_MAX_BYTES = 64 * 1024;
|
||||
const CLAWHUB_RATE_LIMIT_RETRY_DELAYS_MS = [1_000, 3_000, 10_000] as const;
|
||||
const CLAWHUB_MAX_RETRY_AFTER_MS = 60_000;
|
||||
const OPENCLAW_PLUGIN_CLAWHUB_REPOSITORY = "openclaw/openclaw";
|
||||
const OPENCLAW_PLUGIN_CLAWHUB_WORKFLOW_FILENAME = "plugin-clawhub-release.yml";
|
||||
const SAFE_EXTENSION_ID_RE = /^[a-z0-9][a-z0-9._-]*$/;
|
||||
const CLAWHUB_SHARED_RELEASE_INPUT_PATHS = [
|
||||
".github/workflows/plugin-clawhub-release.yml",
|
||||
".github/actions/setup-node-env",
|
||||
"package.json",
|
||||
"pnpm-lock.yaml",
|
||||
"packages/plugin-package-contract/src/index.ts",
|
||||
"scripts/lib/bounded-response.ts",
|
||||
"scripts/lib/npm-publish-plan.mjs",
|
||||
"scripts/lib/plugin-npm-release.ts",
|
||||
"scripts/lib/plugin-clawhub-release.ts",
|
||||
"scripts/openclaw-npm-release-check.ts",
|
||||
"scripts/plugin-clawhub-publish.sh",
|
||||
"scripts/plugin-clawhub-release-check.ts",
|
||||
"scripts/plugin-clawhub-release-plan.ts",
|
||||
] as const;
|
||||
|
||||
function getRegistryBaseUrl(explicit?: string) {
|
||||
return (
|
||||
explicit?.trim() ||
|
||||
process.env.CLAWHUB_REGISTRY?.trim() ||
|
||||
process.env.CLAWHUB_SITE?.trim() ||
|
||||
CLAWHUB_DEFAULT_REGISTRY
|
||||
);
|
||||
}
|
||||
|
||||
type ClawHubRequestOptions = {
|
||||
fetchImpl?: typeof fetch;
|
||||
requestTimeoutMs?: number;
|
||||
};
|
||||
|
||||
async function fetchClawHubRequest(
|
||||
url: URL,
|
||||
options: ClawHubRequestOptions = {},
|
||||
): Promise<{
|
||||
clearTimeout: () => void;
|
||||
response: Response;
|
||||
signal: AbortSignal;
|
||||
timeoutPromise: Promise<never>;
|
||||
}> {
|
||||
const timeoutMs = options.requestTimeoutMs ?? CLAWHUB_REQUEST_TIMEOUT_MS;
|
||||
const controller = new AbortController();
|
||||
const timeoutError = Object.assign(
|
||||
new Error(`ClawHub request timed out after ${timeoutMs}ms: ${url.href}`),
|
||||
{ code: "ETIMEDOUT" },
|
||||
);
|
||||
let timeout: ReturnType<typeof setTimeout> | undefined;
|
||||
const timeoutPromise = new Promise<never>((_resolve, reject) => {
|
||||
timeout = setTimeout(() => {
|
||||
controller.abort(timeoutError);
|
||||
reject(timeoutError);
|
||||
}, timeoutMs);
|
||||
timeout.unref?.();
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await Promise.race([
|
||||
(options.fetchImpl ?? fetch)(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
signal: controller.signal,
|
||||
}),
|
||||
timeoutPromise,
|
||||
]);
|
||||
return {
|
||||
clearTimeout: () => clearTimeout(timeout),
|
||||
response,
|
||||
signal: controller.signal,
|
||||
timeoutPromise,
|
||||
};
|
||||
} catch (error) {
|
||||
clearTimeout(timeout);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelClawHubResponseBody(response: Response): Promise<void> {
|
||||
await response.body?.cancel().catch(() => undefined);
|
||||
}
|
||||
|
||||
function formatClawHubPackageArtifactName(
|
||||
plugin: Pick<PublishablePluginPackage, "packageName" | "version">,
|
||||
) {
|
||||
const safeName = plugin.packageName
|
||||
.replace(/^@/u, "")
|
||||
.replace(/[^A-Za-z0-9_.-]+/gu, "-")
|
||||
.replace(/^-+|-+$/gu, "");
|
||||
return `clawhub-package-${safeName}-${plugin.version}`;
|
||||
}
|
||||
|
||||
export function collectClawHubPublishablePluginPackages(
|
||||
rootDir = resolve("."),
|
||||
filters: ClawHubPublishablePluginPackageFilters = {},
|
||||
): PublishablePluginPackage[] {
|
||||
const publishable: PublishablePluginPackage[] = [];
|
||||
const validationErrors: string[] = [];
|
||||
const selectedExtensionIds = new Set(filters.extensionIds ?? []);
|
||||
const selectedPackageNames = new Set(filters.packageNames ?? []);
|
||||
const hasSelectedExtensionIds = Array.isArray(filters.extensionIds);
|
||||
const hasSelectedPackageNames = Array.isArray(filters.packageNames);
|
||||
|
||||
for (const candidate of collectExtensionPackageJsonCandidates(rootDir)) {
|
||||
const { extensionId, packageDir, packageJson } = candidate;
|
||||
if (hasSelectedExtensionIds && !selectedExtensionIds.has(extensionId)) {
|
||||
continue;
|
||||
}
|
||||
const packageName = packageJson.name?.trim() ?? "";
|
||||
if (hasSelectedPackageNames && !selectedPackageNames.has(packageName)) {
|
||||
continue;
|
||||
}
|
||||
if (packageJson.openclaw?.release?.publishToClawHub !== true) {
|
||||
continue;
|
||||
}
|
||||
if (!SAFE_EXTENSION_ID_RE.test(extensionId)) {
|
||||
validationErrors.push(
|
||||
`${extensionId}: extension directory name must match ^[a-z0-9][a-z0-9._-]*$ for ClawHub publish.`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const errors = collectPublishablePluginPackageErrors(candidate);
|
||||
if (errors.length > 0) {
|
||||
validationErrors.push(...errors.map((error) => `${extensionId}: ${error}`));
|
||||
continue;
|
||||
}
|
||||
const contractValidation = validateExternalCodePluginPackageJson(packageJson);
|
||||
if (contractValidation.issues.length > 0) {
|
||||
validationErrors.push(
|
||||
...contractValidation.issues.map((issue) => `${extensionId}: ${issue.message}`),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const resolvedVersion = resolvePublishablePluginVersion({
|
||||
extensionId,
|
||||
packageJson,
|
||||
validationErrors,
|
||||
});
|
||||
if (!resolvedVersion) {
|
||||
continue;
|
||||
}
|
||||
const { version, parsedVersion } = resolvedVersion;
|
||||
const requiredLatestDependencies = collectRequiredLatestDependencies(packageJson).dependencies;
|
||||
|
||||
publishable.push({
|
||||
extensionId,
|
||||
packageDir,
|
||||
packageName,
|
||||
version,
|
||||
channel: parsedVersion.channel,
|
||||
publishTag:
|
||||
parsedVersion.channel === "alpha"
|
||||
? "alpha"
|
||||
: parsedVersion.channel === "beta"
|
||||
? "beta"
|
||||
: "latest",
|
||||
...(requiredLatestDependencies.length > 0 ? { requiredLatestDependencies } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
if (validationErrors.length > 0) {
|
||||
throw new Error(
|
||||
`Publishable ClawHub plugin metadata validation failed:\n${validationErrors.map((error) => `- ${error}`).join("\n")}`,
|
||||
);
|
||||
}
|
||||
|
||||
return publishable.toSorted((left, right) => left.packageName.localeCompare(right.packageName));
|
||||
}
|
||||
|
||||
export function collectPluginClawHubReleasePathsFromGitRange(params: {
|
||||
rootDir?: string;
|
||||
gitRange: GitRangeSelection;
|
||||
}): string[] {
|
||||
return collectPluginClawHubReleasePathsFromGitRangeForPathspecs(params, ["extensions"]);
|
||||
}
|
||||
|
||||
function collectPluginClawHubRelevantPathsFromGitRange(params: {
|
||||
rootDir?: string;
|
||||
gitRange: GitRangeSelection;
|
||||
}): string[] {
|
||||
return collectPluginClawHubReleasePathsFromGitRangeForPathspecs(params, [
|
||||
"extensions",
|
||||
...CLAWHUB_SHARED_RELEASE_INPUT_PATHS,
|
||||
]);
|
||||
}
|
||||
|
||||
function collectPluginClawHubReleasePathsFromGitRangeForPathspecs(
|
||||
params: {
|
||||
rootDir?: string;
|
||||
gitRange: GitRangeSelection;
|
||||
},
|
||||
pathspecs: readonly string[],
|
||||
): string[] {
|
||||
return collectChangedPathsFromGitRange({
|
||||
rootDir: params.rootDir,
|
||||
gitRange: params.gitRange,
|
||||
pathspecs,
|
||||
});
|
||||
}
|
||||
|
||||
function hasSharedClawHubReleaseInputChanges(changedPaths: readonly string[]) {
|
||||
return changedPaths.some((path) =>
|
||||
CLAWHUB_SHARED_RELEASE_INPUT_PATHS.some(
|
||||
(sharedPath) => path === sharedPath || path.startsWith(`${sharedPath}/`),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveChangedClawHubPublishablePluginPackages(params: {
|
||||
plugins: PublishablePluginPackage[];
|
||||
changedPaths: readonly string[];
|
||||
}): PublishablePluginPackage[] {
|
||||
return resolveChangedPublishablePluginPackages({
|
||||
plugins: params.plugins,
|
||||
changedExtensionIds: collectChangedExtensionIdsFromPaths(params.changedPaths),
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveSelectedClawHubPublishablePluginPackages(params: {
|
||||
plugins: PublishablePluginPackage[];
|
||||
selection?: string[];
|
||||
selectionMode?: PluginReleaseSelectionMode;
|
||||
gitRange?: GitRangeSelection;
|
||||
rootDir?: string;
|
||||
}): PublishablePluginPackage[] {
|
||||
if (params.selectionMode === "all-publishable") {
|
||||
return params.plugins;
|
||||
}
|
||||
if (params.selection && params.selection.length > 0) {
|
||||
return resolveSelectedPublishablePluginPackages({
|
||||
plugins: params.plugins,
|
||||
selection: params.selection,
|
||||
});
|
||||
}
|
||||
if (params.gitRange) {
|
||||
const changedPaths = collectPluginClawHubRelevantPathsFromGitRange({
|
||||
rootDir: params.rootDir,
|
||||
gitRange: params.gitRange,
|
||||
});
|
||||
if (hasSharedClawHubReleaseInputChanges(changedPaths)) {
|
||||
return params.plugins;
|
||||
}
|
||||
return resolveChangedClawHubPublishablePluginPackages({
|
||||
plugins: params.plugins,
|
||||
changedPaths,
|
||||
});
|
||||
}
|
||||
return params.plugins;
|
||||
}
|
||||
|
||||
function readPackageManifestAtGitRef(params: {
|
||||
rootDir?: string;
|
||||
ref: string;
|
||||
packageDir: string;
|
||||
}): PluginPackageJson | null {
|
||||
const rootDir = params.rootDir ?? resolve(".");
|
||||
const commitSha = resolveGitCommitSha(rootDir, params.ref, "ref");
|
||||
try {
|
||||
const raw = execFileSync("git", ["show", `${commitSha}:${params.packageDir}/package.json`], {
|
||||
cwd: rootDir,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
return JSON.parse(raw) as PluginPackageJson;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function collectClawHubVersionGateErrors(params: {
|
||||
plugins: PublishablePluginPackage[];
|
||||
gitRange: GitRangeSelection;
|
||||
rootDir?: string;
|
||||
}): string[] {
|
||||
const changedPaths = collectPluginClawHubReleasePathsFromGitRange({
|
||||
rootDir: params.rootDir,
|
||||
gitRange: params.gitRange,
|
||||
});
|
||||
const changedPlugins = resolveChangedClawHubPublishablePluginPackages({
|
||||
plugins: params.plugins,
|
||||
changedPaths,
|
||||
});
|
||||
|
||||
const errors: string[] = [];
|
||||
for (const plugin of changedPlugins) {
|
||||
const baseManifest = readPackageManifestAtGitRef({
|
||||
rootDir: params.rootDir,
|
||||
ref: params.gitRange.baseRef,
|
||||
packageDir: plugin.packageDir,
|
||||
});
|
||||
if (baseManifest?.openclaw?.release?.publishToClawHub !== true) {
|
||||
continue;
|
||||
}
|
||||
const baseVersion =
|
||||
typeof baseManifest.version === "string" && baseManifest.version.trim()
|
||||
? baseManifest.version.trim()
|
||||
: null;
|
||||
if (baseVersion === null || baseVersion !== plugin.version) {
|
||||
continue;
|
||||
}
|
||||
errors.push(
|
||||
`${plugin.packageName}@${plugin.version}: changed publishable plugin still has the same version in package.json.`,
|
||||
);
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
async function isPluginVersionPublishedOnClawHub(
|
||||
packageName: string,
|
||||
version: string,
|
||||
options: {
|
||||
fetchImpl?: typeof fetch;
|
||||
registryBaseUrl?: string;
|
||||
requestTimeoutMs?: number;
|
||||
} = {},
|
||||
): Promise<boolean> {
|
||||
const url = new URL(
|
||||
`/api/v1/packages/${encodeURIComponent(packageName)}/versions/${encodeURIComponent(version)}`,
|
||||
getRegistryBaseUrl(options.registryBaseUrl),
|
||||
);
|
||||
const request = await fetchClawHubRequest(url, {
|
||||
fetchImpl: options.fetchImpl,
|
||||
requestTimeoutMs: options.requestTimeoutMs,
|
||||
});
|
||||
const { response } = request;
|
||||
|
||||
try {
|
||||
if (response.status === 404) {
|
||||
return false;
|
||||
}
|
||||
if (response.ok) {
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Failed to query ClawHub for ${packageName}@${version}: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
} finally {
|
||||
await cancelClawHubResponseBody(response);
|
||||
request.clearTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
async function doesClawHubPackageExist(
|
||||
packageName: string,
|
||||
options: {
|
||||
fetchImpl?: typeof fetch;
|
||||
registryBaseUrl?: string;
|
||||
requestTimeoutMs?: number;
|
||||
} = {},
|
||||
): Promise<boolean> {
|
||||
const url = new URL(
|
||||
`/api/v1/packages/${encodeURIComponent(packageName)}`,
|
||||
getRegistryBaseUrl(options.registryBaseUrl),
|
||||
);
|
||||
const request = await fetchClawHubRequest(url, {
|
||||
fetchImpl: options.fetchImpl,
|
||||
requestTimeoutMs: options.requestTimeoutMs,
|
||||
});
|
||||
const { response } = request;
|
||||
|
||||
try {
|
||||
if (response.status === 404) {
|
||||
return false;
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to query ClawHub package ${packageName}: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
} finally {
|
||||
await cancelClawHubResponseBody(response);
|
||||
request.clearTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
async function hasClawHubTrustedPublisher(
|
||||
packageName: string,
|
||||
options: {
|
||||
fetchImpl?: typeof fetch;
|
||||
registryBaseUrl?: string;
|
||||
requestTimeoutMs?: number;
|
||||
} = {},
|
||||
): Promise<boolean> {
|
||||
const url = new URL(
|
||||
`/api/v1/packages/${encodeURIComponent(packageName)}/trusted-publisher`,
|
||||
getRegistryBaseUrl(options.registryBaseUrl),
|
||||
);
|
||||
for (let attempt = 0; ; attempt += 1) {
|
||||
const request = await fetchClawHubRequest(url, {
|
||||
fetchImpl: options.fetchImpl,
|
||||
requestTimeoutMs: options.requestTimeoutMs,
|
||||
});
|
||||
const { response } = request;
|
||||
|
||||
const retryRateLimit =
|
||||
response.status === 429 && attempt < CLAWHUB_RATE_LIMIT_RETRY_DELAYS_MS.length;
|
||||
try {
|
||||
if (!retryRateLimit) {
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to query ClawHub trusted publisher for ${packageName}: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
let trustedPublisherDetail: ClawHubTrustedPublisherDetail;
|
||||
const text = await readBoundedResponseText(
|
||||
response,
|
||||
`ClawHub trusted publisher ${packageName}`,
|
||||
CLAWHUB_RESPONSE_BODY_MAX_BYTES,
|
||||
{
|
||||
signal: request.signal,
|
||||
timeoutPromise: request.timeoutPromise,
|
||||
},
|
||||
);
|
||||
try {
|
||||
trustedPublisherDetail = JSON.parse(text) as ClawHubTrustedPublisherDetail;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse ClawHub trusted publisher ${packageName} response.`, {
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
|
||||
return isOpenClawPluginTrustedPublisher(trustedPublisherDetail.trustedPublisher);
|
||||
}
|
||||
} finally {
|
||||
request.clearTimeout();
|
||||
}
|
||||
|
||||
await response.body?.cancel().catch(() => undefined);
|
||||
await delay(clawHubRetryDelayMs(response, attempt));
|
||||
}
|
||||
}
|
||||
|
||||
function clawHubRetryDelayMs(response: Response, attempt: number): number {
|
||||
const retryAfter = response.headers.get("retry-after")?.trim();
|
||||
if (retryAfter) {
|
||||
const retryAfterSeconds = Number(retryAfter);
|
||||
if (Number.isFinite(retryAfterSeconds) && retryAfterSeconds >= 0) {
|
||||
const retryAfterMs = Math.round(retryAfterSeconds * 1_000);
|
||||
if (retryAfterMs <= CLAWHUB_MAX_RETRY_AFTER_MS) {
|
||||
return retryAfterMs;
|
||||
}
|
||||
}
|
||||
const retryAfterAt = Date.parse(retryAfter);
|
||||
if (Number.isFinite(retryAfterAt)) {
|
||||
const retryAfterMs = Math.max(0, retryAfterAt - Date.now());
|
||||
if (retryAfterMs <= CLAWHUB_MAX_RETRY_AFTER_MS) {
|
||||
return retryAfterMs;
|
||||
}
|
||||
}
|
||||
}
|
||||
return CLAWHUB_RATE_LIMIT_RETRY_DELAYS_MS[attempt] ?? 0;
|
||||
}
|
||||
|
||||
async function delay(ms: number): Promise<void> {
|
||||
await new Promise<void>((resolveDelay) => {
|
||||
setTimeout(resolveDelay, ms);
|
||||
});
|
||||
}
|
||||
|
||||
function isOpenClawPluginTrustedPublisher(value: unknown): boolean {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return false;
|
||||
}
|
||||
const trustedPublisher = value as ClawHubTrustedPublisherConfig;
|
||||
return (
|
||||
trustedPublisher.repository === OPENCLAW_PLUGIN_CLAWHUB_REPOSITORY &&
|
||||
trustedPublisher.workflowFilename === OPENCLAW_PLUGIN_CLAWHUB_WORKFLOW_FILENAME &&
|
||||
trustedPublisher.environment == null
|
||||
);
|
||||
}
|
||||
|
||||
function stripPackageReleaseState(
|
||||
item: PluginReleasePlanItemWithPackageState,
|
||||
): PluginReleasePlanItem {
|
||||
const {
|
||||
packageExists: _packageExists,
|
||||
hasTrustedPublisher: _hasTrustedPublisher,
|
||||
...planItem
|
||||
} = item;
|
||||
return planItem;
|
||||
}
|
||||
|
||||
export async function collectPluginClawHubReleasePlan(params?: {
|
||||
rootDir?: string;
|
||||
selection?: string[];
|
||||
selectionMode?: PluginReleaseSelectionMode;
|
||||
gitRange?: GitRangeSelection;
|
||||
registryBaseUrl?: string;
|
||||
fetchImpl?: typeof fetch;
|
||||
requestTimeoutMs?: number;
|
||||
resolveLatestVersion?: NpmLatestVersionResolver;
|
||||
}): Promise<PluginReleasePlan> {
|
||||
const rootDir = params?.rootDir;
|
||||
const selection = params?.selection ?? [];
|
||||
const changedPaths = params?.gitRange
|
||||
? collectPluginClawHubRelevantPathsFromGitRange({
|
||||
rootDir,
|
||||
gitRange: params.gitRange,
|
||||
})
|
||||
: [];
|
||||
const sharedInputChanged = hasSharedClawHubReleaseInputChanges(changedPaths);
|
||||
const extensionIds =
|
||||
params?.selectionMode === "all-publishable" || !params?.gitRange || sharedInputChanged
|
||||
? undefined
|
||||
: collectChangedExtensionIdsFromPaths(changedPaths);
|
||||
const allPublishable = collectClawHubPublishablePluginPackages(rootDir, {
|
||||
extensionIds,
|
||||
packageNames: selection.length > 0 ? selection : undefined,
|
||||
});
|
||||
const selectedPublishable = resolveSelectedClawHubPublishablePluginPackages({
|
||||
plugins: allPublishable,
|
||||
selection,
|
||||
selectionMode: params?.selectionMode,
|
||||
gitRange: params?.gitRange,
|
||||
rootDir,
|
||||
});
|
||||
|
||||
const explicitPublishSelection = params?.selectionMode !== undefined || selection.length > 0;
|
||||
if (explicitPublishSelection) {
|
||||
assertPluginReleaseVersionFloors(selectedPublishable, "Plugin ClawHub release plan");
|
||||
}
|
||||
assertPluginReleaseDependencyFreshness(
|
||||
selectedPublishable,
|
||||
"Plugin ClawHub release plan",
|
||||
params?.resolveLatestVersion,
|
||||
);
|
||||
|
||||
const planned: PluginReleasePlanItemWithPackageState[] = [];
|
||||
for (const plugin of selectedPublishable) {
|
||||
const packageExists = await doesClawHubPackageExist(plugin.packageName, {
|
||||
registryBaseUrl: params?.registryBaseUrl,
|
||||
fetchImpl: params?.fetchImpl,
|
||||
requestTimeoutMs: params?.requestTimeoutMs,
|
||||
});
|
||||
const hasTrustedPublisher = packageExists
|
||||
? await hasClawHubTrustedPublisher(plugin.packageName, {
|
||||
registryBaseUrl: params?.registryBaseUrl,
|
||||
fetchImpl: params?.fetchImpl,
|
||||
requestTimeoutMs: params?.requestTimeoutMs,
|
||||
})
|
||||
: false;
|
||||
const alreadyPublished = packageExists
|
||||
? await isPluginVersionPublishedOnClawHub(plugin.packageName, plugin.version, {
|
||||
registryBaseUrl: params?.registryBaseUrl,
|
||||
fetchImpl: params?.fetchImpl,
|
||||
requestTimeoutMs: params?.requestTimeoutMs,
|
||||
})
|
||||
: false;
|
||||
|
||||
planned.push({
|
||||
extensionId: plugin.extensionId,
|
||||
packageDir: plugin.packageDir,
|
||||
packageName: plugin.packageName,
|
||||
version: plugin.version,
|
||||
channel: plugin.channel,
|
||||
publishTag: plugin.publishTag,
|
||||
packageExists,
|
||||
hasTrustedPublisher,
|
||||
alreadyPublished,
|
||||
artifactName: formatClawHubPackageArtifactName(plugin),
|
||||
});
|
||||
}
|
||||
const all = planned.map(stripPackageReleaseState);
|
||||
|
||||
return {
|
||||
all,
|
||||
candidates: planned
|
||||
.filter(
|
||||
(plugin) => plugin.packageExists && plugin.hasTrustedPublisher && !plugin.alreadyPublished,
|
||||
)
|
||||
.map(stripPackageReleaseState),
|
||||
bootstrapCandidates: planned
|
||||
.filter((plugin) => !plugin.packageExists)
|
||||
.map(stripPackageReleaseState),
|
||||
missingTrustedPublisher: planned
|
||||
.filter((plugin) => plugin.packageExists && !plugin.hasTrustedPublisher)
|
||||
.map(stripPackageReleaseState),
|
||||
skippedPublished: planned
|
||||
.filter((plugin) => plugin.alreadyPublished)
|
||||
.map(stripPackageReleaseState),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user