fix(ci): restore full release validation blockers

This commit is contained in:
Peter Steinberger
2026-04-28 19:20:14 +01:00
parent 2290adbf57
commit e2295b33c1
6 changed files with 166 additions and 28 deletions

View File

@@ -1,6 +1,7 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
const TRASH_DESTINATION_COLLISION_CODES = new Set(["EEXIST", "ENOTEMPTY", "ERR_FS_CP_EEXIST"]);
const TRASH_DESTINATION_RETRY_LIMIT = 4;
@@ -23,7 +24,7 @@ function isSameOrChildPath(candidate: string, parent: string): boolean {
}
function resolveAllowedTrashRoots(): string[] {
const roots = [os.homedir(), os.tmpdir()].map((root) => {
const roots = [os.homedir(), resolvePreferredOpenClawTmpDir()].map((root) => {
try {
return path.resolve(fs.realpathSync.native(root));
} catch {

View File

@@ -247,21 +247,32 @@ async function installSignalCliViaBrew(runtime: RuntimeEnv): Promise<SignalInsta
async function installSignalCliFromRelease(runtime: RuntimeEnv): Promise<SignalInstallResult> {
const apiUrl = "https://api.github.com/repos/AsamK/signal-cli/releases/latest";
const response = await fetch(apiUrl, {
headers: {
"User-Agent": "openclaw",
Accept: "application/vnd.github+json",
const { response, release } = await fetchWithSsrFGuard({
url: apiUrl,
maxRedirects: 5,
requireHttps: true,
capture: false,
auditContext: "signal-cli-release-info",
init: {
headers: {
"User-Agent": "openclaw",
Accept: "application/vnd.github+json",
},
},
});
if (!response.ok) {
return {
ok: false,
error: `Failed to fetch release info (${response.status})`,
};
let payload: ReleaseResponse;
try {
if (!response.ok) {
return {
ok: false,
error: `Failed to fetch release info (${response.status})`,
};
}
payload = (await response.json()) as ReleaseResponse;
} finally {
await release();
}
const payload = (await response.json()) as ReleaseResponse;
const version = payload.tag_name?.replace(/^v/, "") ?? "unknown";
const assets = payload.assets ?? [];
const asset = pickAsset(assets, process.platform, process.arch);

View File

@@ -135,9 +135,10 @@ const record = registry.plugins.find((entry) => entry.id === pluginId);
assert.ok(record, "smoke plugin missing from registry");
assert.equal(record.status, "loaded", record.error ?? "smoke plugin failed to load");
assert.deepEqual(getPluginCommandSpecs(), [
{ name: "pair", description: "Pair a device", acceptsArgs: true },
]);
assert.deepEqual(
getPluginCommandSpecs().filter((command) => command.name === "pair"),
[{ name: "pair", description: "Pair a device", acceptsArgs: true }],
);
const match = matchPluginCommand("/pair now");
assert.ok(match, "canonical built command registry did not receive the command");

View File

@@ -1,7 +1,9 @@
import fs from "node:fs";
import path from "node:path";
import { formatCliCommand } from "../cli/command-format.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { resolveOpenClawPackageRootSync } from "../infra/openclaw-root.js";
import { resolveBundledPluginsDir } from "../plugins/bundled-dir.js";
import {
createBundledRuntimeDepsWritableInstallSpecs,
repairBundledRuntimeDepsInstallRootAsync,
@@ -9,13 +11,98 @@ import {
scanBundledPluginRuntimeDeps,
type BundledRuntimeDepsInstallParams,
} from "../plugins/bundled-runtime-deps.js";
import { normalizePluginsConfig } from "../plugins/config-state.js";
import { resolveEffectivePluginIds } from "../plugins/effective-plugin-ids.js";
import { passesManifestOwnerBasePolicy } from "../plugins/manifest-owner-policy.js";
import type { RuntimeEnv } from "../runtime.js";
import { note } from "../terminal/note.js";
import type { DoctorPrompter } from "./doctor-prompter.js";
const RUNTIME_DEPS_INSTALL_HEARTBEAT_MS = 15_000;
function filterPluginIdsPresentInBundledTree(
bundledPluginsDir: string,
pluginIds: readonly string[],
): string[] | undefined {
const present = pluginIds.filter((pluginId) => {
if (path.basename(pluginId) !== pluginId) {
return false;
}
return fs.existsSync(path.join(bundledPluginsDir, pluginId));
});
return present.length > 0 ? present : undefined;
}
function collectPackagedRuntimeDepsRepairPluginIds(params: {
bundledPluginsDir: string;
config: OpenClawConfig;
includeConfiguredChannels?: boolean;
}): string[] {
if (!fs.existsSync(params.bundledPluginsDir)) {
return [];
}
const plugins = normalizePluginsConfig(params.config.plugins);
const ids = new Set<string>();
for (const entry of fs.readdirSync(params.bundledPluginsDir, { withFileTypes: true })) {
if (!entry.isDirectory()) {
continue;
}
const pluginDir = path.join(params.bundledPluginsDir, entry.name);
let manifest: Record<string, unknown>;
try {
manifest = JSON.parse(
fs.readFileSync(path.join(pluginDir, "openclaw.plugin.json"), "utf-8"),
) as Record<string, unknown>;
} catch {
continue;
}
const pluginId = typeof manifest.id === "string" && manifest.id ? manifest.id : entry.name;
if (
!passesManifestOwnerBasePolicy({
plugin: { id: pluginId },
normalizedConfig: plugins,
allowRestrictiveAllowlistBypass: true,
})
) {
continue;
}
if (plugins.allow.includes(pluginId) || plugins.entries[pluginId]?.enabled === true) {
ids.add(pluginId);
continue;
}
const channels = Array.isArray(manifest.channels)
? manifest.channels.filter((channel): channel is string => typeof channel === "string")
: [];
if (
channels.some((channelId) => {
const channelConfig = (params.config.channels as Record<string, unknown> | undefined)?.[
channelId
];
if (!channelConfig || typeof channelConfig !== "object" || Array.isArray(channelConfig)) {
return false;
}
if ((channelConfig as { enabled?: unknown }).enabled === false) {
return false;
}
return (
(channelConfig as { enabled?: unknown }).enabled === true ||
params.includeConfiguredChannels === true
);
})
) {
ids.add(pluginId);
continue;
}
const providers = Array.isArray(manifest.providers)
? manifest.providers.filter((provider): provider is string => typeof provider === "string")
: [];
if (manifest.enabledByDefault === true && providers.length === 0 && channels.length === 0) {
ids.add(pluginId);
}
}
return [...ids].toSorted((left, right) => left.localeCompare(right));
}
function formatElapsedMs(elapsedMs: number): string {
if (elapsedMs < 1000) {
return `${elapsedMs}ms`;
@@ -56,13 +143,23 @@ export async function maybeRepairBundledPluginRuntimeDeps(params: {
const env = params.env ?? process.env;
const bundledPluginsDir = path.join(packageRoot, "dist", "extensions");
const effectivePluginIds = params.config
? resolveEffectivePluginIds({
config: params.config,
env: {
...env,
OPENCLAW_BUNDLED_PLUGINS_DIR: bundledPluginsDir,
},
})
? resolveBundledPluginsDir({ ...env, OPENCLAW_BUNDLED_PLUGINS_DIR: bundledPluginsDir }) ===
bundledPluginsDir
? filterPluginIdsPresentInBundledTree(
bundledPluginsDir,
resolveEffectivePluginIds({
config: params.config,
env: {
...env,
OPENCLAW_BUNDLED_PLUGINS_DIR: bundledPluginsDir,
},
}),
)
: collectPackagedRuntimeDepsRepairPluginIds({
bundledPluginsDir,
config: params.config,
includeConfiguredChannels: params.includeConfiguredChannels,
})
: undefined;
const { deps, missing, conflicts } = scanBundledPluginRuntimeDeps({
packageRoot,

View File

@@ -90,8 +90,8 @@ describe("doctor plugin manifest legacy contract repair", () => {
const migrations = collectLegacyPluginManifestContractMigrations({
env: {
...process.env,
OPENCLAW_BUNDLED_PLUGINS_DIR: pluginsRoot,
},
manifestRoots: [pluginsRoot],
});
expect(migrations).toHaveLength(1);
@@ -119,8 +119,8 @@ describe("doctor plugin manifest legacy contract repair", () => {
await maybeRepairLegacyPluginManifestContracts({
env: {
...process.env,
OPENCLAW_BUNDLED_PLUGINS_DIR: pluginsRoot,
},
manifestRoots: [pluginsRoot],
runtime: createRuntime(),
prompter: createPrompter(),
note: vi.fn(),
@@ -156,8 +156,8 @@ describe("doctor plugin manifest legacy contract repair", () => {
const migrations = collectLegacyPluginManifestContractMigrations({
env: {
...process.env,
OPENCLAW_BUNDLED_PLUGINS_DIR: pluginsRoot,
},
manifestRoots: [pluginsRoot],
});
expect(migrations).toHaveLength(1);

View File

@@ -1,4 +1,5 @@
import fs from "node:fs";
import path from "node:path";
import { z } from "zod";
import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js";
import type { RuntimeEnv } from "../runtime.js";
@@ -81,10 +82,35 @@ function buildLegacyManifestContractMigration(params: {
export function collectLegacyPluginManifestContractMigrations(params?: {
env?: NodeJS.ProcessEnv;
manifestRoots?: string[];
}): LegacyManifestContractMigration[] {
const seen = new Set<string>();
const migrations: LegacyManifestContractMigration[] = [];
for (const root of params?.manifestRoots ?? []) {
if (!fs.existsSync(root)) {
continue;
}
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
if (!entry.isDirectory()) {
continue;
}
const manifestPath = path.join(root, entry.name, "openclaw.plugin.json");
if (seen.has(manifestPath)) {
continue;
}
seen.add(manifestPath);
const raw = readManifestJson(manifestPath);
if (!raw) {
continue;
}
const migration = buildLegacyManifestContractMigration({ manifestPath, raw });
if (migration) {
migrations.push(migration);
}
}
}
for (const plugin of loadPluginManifestRegistry({
cache: false,
...(params?.env ? { env: params.env } : {}),
@@ -111,13 +137,15 @@ export function collectLegacyPluginManifestContractMigrations(params?: {
export async function maybeRepairLegacyPluginManifestContracts(params: {
env?: NodeJS.ProcessEnv;
manifestRoots?: string[];
runtime: RuntimeEnv;
prompter: DoctorPrompter;
note?: typeof note;
}): Promise<void> {
const migrations = collectLegacyPluginManifestContractMigrations(
params.env ? { env: params.env } : undefined,
);
const migrations = collectLegacyPluginManifestContractMigrations({
...(params.env ? { env: params.env } : {}),
...(params.manifestRoots ? { manifestRoots: params.manifestRoots } : {}),
});
if (migrations.length === 0) {
return;
}