mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 14:01:24 +08:00
Compare commits
14 Commits
fix/plugin
...
v2026.5.19
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a185ca283a | ||
|
|
ca3c3fca43 | ||
|
|
a6172a7d0e | ||
|
|
2823725134 | ||
|
|
eab57ad8ad | ||
|
|
93c2d1ea99 | ||
|
|
29faac2f9c | ||
|
|
e8d8c5dd6f | ||
|
|
3743d6bdeb | ||
|
|
b14ae2fbea | ||
|
|
d0eec21887 | ||
|
|
aa5706f5e8 | ||
|
|
947a07016e | ||
|
|
430f868cb6 |
@@ -170,6 +170,13 @@ live`; keep it clearly beta and avoid implying stable promotion.
|
||||
CI, validation, or internal release mechanics unless the release is explicitly
|
||||
about those. Peter prefers concrete user wins: features, integrations,
|
||||
workflow improvements, and practical reliability fixes.
|
||||
- Do not feature QA parity, test coverage, release gates, or validation lanes in
|
||||
user-facing launch tweets. Keep them for release notes or maintainer proof
|
||||
unless the operator explicitly asks for validation-focused copy.
|
||||
- Do not feature plugin-author or developer tooling such as SDK helpers,
|
||||
tool-plugin scaffolding, build/validate/init commands, or internal CLI
|
||||
plumbing in general user-facing launch tweets unless the operator explicitly
|
||||
asks for developer-focused copy.
|
||||
- Tone: high-signal, slightly cheeky, confident, not corporate. One joke is
|
||||
enough. Avoid punching down, insulting users, or promising what was not
|
||||
verified.
|
||||
|
||||
3
.github/actions/setup-node-env/action.yml
vendored
3
.github/actions/setup-node-env/action.yml
vendored
@@ -59,14 +59,15 @@ runs:
|
||||
if command -v bun &>/dev/null; then bun -v; fi
|
||||
|
||||
- name: Capture node path
|
||||
if: inputs.install-deps == 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
node_bin="$(dirname "$(node -p 'process.execPath')")"
|
||||
if command -v cygpath >/dev/null 2>&1; then
|
||||
node_bin="$(cygpath -u "$node_bin")"
|
||||
fi
|
||||
# zizmor: ignore[github-env] node_bin comes from trusted actions/setup-node output in this composite action.
|
||||
echo "NODE_BIN=$node_bin" >> "$GITHUB_ENV"
|
||||
echo "$node_bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Install dependencies
|
||||
if: inputs.install-deps == 'true'
|
||||
|
||||
@@ -215,7 +215,7 @@ jobs:
|
||||
needs: [resolve_target]
|
||||
if: contains(fromJSON('["all","ci"]'), inputs.rerun_group)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: ${{ inputs.release_profile == 'full' && 240 || 60 }}
|
||||
timeout-minutes: ${{ inputs.release_profile != 'minimum' && 240 || 60 }}
|
||||
outputs:
|
||||
run_id: ${{ steps.dispatch.outputs.run_id }}
|
||||
url: ${{ steps.dispatch.outputs.url }}
|
||||
@@ -315,7 +315,7 @@ jobs:
|
||||
needs: [resolve_target]
|
||||
if: contains(fromJSON('["all","plugin-prerelease"]'), inputs.rerun_group)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: ${{ inputs.release_profile == 'full' && 300 || 60 }}
|
||||
timeout-minutes: ${{ inputs.release_profile == 'full' && 300 || inputs.release_profile == 'stable' && 240 || 60 }}
|
||||
outputs:
|
||||
run_id: ${{ steps.dispatch.outputs.run_id }}
|
||||
url: ${{ steps.dispatch.outputs.url }}
|
||||
@@ -415,7 +415,7 @@ jobs:
|
||||
needs: [resolve_target]
|
||||
if: contains(fromJSON('["all","release-checks","install-smoke","cross-os","live-e2e","package","qa","qa-parity","qa-live"]'), inputs.rerun_group)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: ${{ inputs.release_profile == 'full' && 240 || 60 }}
|
||||
timeout-minutes: ${{ inputs.release_profile != 'minimum' && 240 || 60 }}
|
||||
outputs:
|
||||
run_id: ${{ steps.dispatch.outputs.run_id }}
|
||||
url: ${{ steps.dispatch.outputs.url }}
|
||||
|
||||
@@ -1042,11 +1042,28 @@ resolve_candidate_version() {
|
||||
export OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT
|
||||
}
|
||||
|
||||
candidate_update_spec() {
|
||||
if [ "$CANDIDATE_KIND" != "tarball" ]; then
|
||||
printf '%s\n' "$CANDIDATE_SPEC"
|
||||
return 0
|
||||
fi
|
||||
case "$CANDIDATE_SPEC" in
|
||||
file:*)
|
||||
printf '%s\n' "$CANDIDATE_SPEC"
|
||||
;;
|
||||
*)
|
||||
printf 'file:%s\n' "$CANDIDATE_SPEC"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
update_candidate() {
|
||||
echo "Updating baseline $baseline_spec to candidate $CANDIDATE_KIND:$CANDIDATE_SPEC ($candidate_version)"
|
||||
local update_spec
|
||||
update_spec="$(candidate_update_spec)"
|
||||
echo "Updating baseline $baseline_spec to candidate $CANDIDATE_KIND:$update_spec ($candidate_version)"
|
||||
local update_start=""
|
||||
local update_end=""
|
||||
local update_args=(update --tag "$CANDIDATE_SPEC" --yes --json)
|
||||
local update_args=(update --tag "$update_spec" --yes --json)
|
||||
local update_env=(
|
||||
env
|
||||
-u OPENCLAW_GATEWAY_TOKEN
|
||||
|
||||
@@ -824,6 +824,7 @@ describe("update-cli", () => {
|
||||
expect(call?.[2]?.env?.NODE_DISABLE_COMPILE_CACHE).toBe("1");
|
||||
expect(call?.[2]?.env?.OPENCLAW_UPDATE_POST_CORE).toBe("1");
|
||||
expect(call?.[2]?.env?.OPENCLAW_UPDATE_POST_CORE_CHANNEL).toBe("dev");
|
||||
expect(call?.[2]?.env?.OPENCLAW_COMPATIBILITY_HOST_VERSION).toBe("1.0.0");
|
||||
expect(updateNpmInstalledPlugins).not.toHaveBeenCalled();
|
||||
expect(runDaemonInstall).not.toHaveBeenCalled();
|
||||
expect(runDaemonRestart).not.toHaveBeenCalled();
|
||||
|
||||
@@ -13,6 +13,7 @@ vi.mock("./plugin-payload-validation.js", () => ({
|
||||
}));
|
||||
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { VERSION } from "../../version.js";
|
||||
import {
|
||||
convergenceWarningsToOutcomes,
|
||||
filterRecordsToActive,
|
||||
@@ -41,6 +42,22 @@ describe("runPostCorePluginConvergence", () => {
|
||||
cfg,
|
||||
env: {
|
||||
OPENCLAW_UPDATE_IN_PROGRESS: "1",
|
||||
OPENCLAW_COMPATIBILITY_HOST_VERSION: VERSION,
|
||||
OPENCLAW_UPDATE_POST_CORE_CONVERGENCE: "1",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the candidate runtime version over a stale inherited host version", async () => {
|
||||
const cfg = { plugins: { entries: {} } } as unknown as OpenClawConfig;
|
||||
await runPostCorePluginConvergence({
|
||||
cfg,
|
||||
env: { OPENCLAW_COMPATIBILITY_HOST_VERSION: "2026.5.12" },
|
||||
});
|
||||
expect(mocks.repairMissingConfiguredPluginInstalls).toHaveBeenCalledWith({
|
||||
cfg,
|
||||
env: {
|
||||
OPENCLAW_COMPATIBILITY_HOST_VERSION: VERSION,
|
||||
OPENCLAW_UPDATE_POST_CORE_CONVERGENCE: "1",
|
||||
},
|
||||
});
|
||||
@@ -97,6 +114,7 @@ describe("runPostCorePluginConvergence", () => {
|
||||
expect(mocks.repairMissingConfiguredPluginInstalls).toHaveBeenCalledWith({
|
||||
cfg,
|
||||
env: {
|
||||
OPENCLAW_COMPATIBILITY_HOST_VERSION: VERSION,
|
||||
OPENCLAW_UPDATE_POST_CORE_CONVERGENCE: "1",
|
||||
},
|
||||
baselineRecords: baseline,
|
||||
@@ -222,6 +240,7 @@ describe("runPostCorePluginConvergence", () => {
|
||||
expect(mocks.runPluginPayloadSmokeCheck).toHaveBeenCalledWith({
|
||||
records,
|
||||
env: {
|
||||
OPENCLAW_COMPATIBILITY_HOST_VERSION: VERSION,
|
||||
OPENCLAW_UPDATE_POST_CORE_CONVERGENCE: "1",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
resolveTrustedSourceLinkedOfficialClawHubSpec,
|
||||
resolveTrustedSourceLinkedOfficialNpmSpec,
|
||||
} from "../../plugins/update.js";
|
||||
import { VERSION } from "../../version.js";
|
||||
import {
|
||||
runPluginPayloadSmokeCheck,
|
||||
type PluginPayloadSmokeFailure,
|
||||
@@ -62,6 +63,7 @@ export async function runPostCorePluginConvergence(params: {
|
||||
}): Promise<PostCoreConvergenceResult> {
|
||||
const env: NodeJS.ProcessEnv = {
|
||||
...params.env,
|
||||
OPENCLAW_COMPATIBILITY_HOST_VERSION: VERSION,
|
||||
[UPDATE_POST_CORE_CONVERGENCE_ENV]: "1",
|
||||
};
|
||||
|
||||
|
||||
@@ -95,6 +95,7 @@ import { normalizeOptionalString } from "../../shared/string-coerce.js";
|
||||
import { stylePromptMessage } from "../../terminal/prompt-style.js";
|
||||
import { theme } from "../../terminal/theme.js";
|
||||
import { resolveUserPath } from "../../utils.js";
|
||||
import { VERSION } from "../../version.js";
|
||||
import { replaceCliName, resolveCliName } from "../cli-name.js";
|
||||
import { formatCliCommand } from "../command-format.js";
|
||||
import { installCompletion } from "../completion-runtime.js";
|
||||
@@ -2678,6 +2679,7 @@ async function continuePostCoreUpdateInFreshProcess(params: {
|
||||
const resultPath = path.join(resultDir, "plugins.json");
|
||||
const installRecordsPath = path.join(resultDir, "plugin-install-records.json");
|
||||
const sourceConfigPath = path.join(resultDir, "source-config.json");
|
||||
const postCoreHostVersion = await readPackageVersion(params.root);
|
||||
|
||||
try {
|
||||
await writePostCorePluginInstallRecordsFile(installRecordsPath, params.pluginInstallRecords);
|
||||
@@ -2695,6 +2697,9 @@ async function continuePostCoreUpdateInFreshProcess(params: {
|
||||
[POST_CORE_UPDATE_RESULT_PATH_ENV]: resultPath,
|
||||
[POST_CORE_UPDATE_INSTALL_RECORDS_PATH_ENV]: installRecordsPath,
|
||||
[POST_CORE_UPDATE_STARTED_AT_ENV]: String(params.updateStartedAtMs),
|
||||
...(postCoreHostVersion === null
|
||||
? {}
|
||||
: { OPENCLAW_COMPATIBILITY_HOST_VERSION: postCoreHostVersion }),
|
||||
...(params.preUpdateConfig
|
||||
? { [POST_CORE_UPDATE_SOURCE_CONFIG_PATH_ENV]: sourceConfigPath }
|
||||
: {}),
|
||||
@@ -2882,6 +2887,8 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
process.env.OPENCLAW_COMPATIBILITY_HOST_VERSION = (await readPackageVersion(root)) ?? VERSION;
|
||||
|
||||
let postCoreConfigSnapshot = await readConfigFileSnapshot({ skipPluginValidation: true });
|
||||
const preUpdateSourceConfig = await readPostCorePreUpdateSourceConfig({
|
||||
sourceConfigPath: process.env[POST_CORE_UPDATE_SOURCE_CONFIG_PATH_ENV],
|
||||
|
||||
@@ -1139,6 +1139,148 @@ describe("repairMissingConfiguredPluginInstalls", () => {
|
||||
expect(result.records.discord?.installPath).toBe(packageDir);
|
||||
});
|
||||
|
||||
it("prefers an existing npm payload over ClawHub during post-core repair", async () => {
|
||||
const npmRoot = makeTempDir();
|
||||
const packageDir = path.join(npmRoot, "node_modules", "@openclaw", "matrix");
|
||||
fs.mkdirSync(packageDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(packageDir, "package.json"),
|
||||
JSON.stringify({ name: "@openclaw/matrix", version: "1.2.3" }),
|
||||
);
|
||||
mocks.resolveDefaultPluginNpmDir.mockReturnValue(npmRoot);
|
||||
mocks.listChannelPluginCatalogEntries.mockReturnValue([
|
||||
{
|
||||
id: "matrix",
|
||||
pluginId: "matrix",
|
||||
meta: { label: "Matrix" },
|
||||
install: {
|
||||
clawhubSpec: "clawhub:@openclaw/matrix",
|
||||
npmSpec: "@openclaw/matrix",
|
||||
},
|
||||
},
|
||||
]);
|
||||
mocks.installPluginFromClawHub.mockResolvedValue({
|
||||
ok: false,
|
||||
error: 'Plugin "@openclaw/matrix" requires plugin API >=2026.5.18.',
|
||||
});
|
||||
mocks.installPluginFromNpmSpec.mockResolvedValue({
|
||||
ok: true,
|
||||
pluginId: "matrix",
|
||||
targetDir: packageDir,
|
||||
version: "1.2.3",
|
||||
npmResolution: {
|
||||
name: "@openclaw/matrix",
|
||||
version: "1.2.3",
|
||||
resolvedSpec: "@openclaw/matrix@1.2.3",
|
||||
integrity: "sha512-matrix",
|
||||
resolvedAt: "2026-05-01T00:00:00.000Z",
|
||||
},
|
||||
});
|
||||
|
||||
const { repairMissingConfiguredPluginInstalls } =
|
||||
await import("./missing-configured-plugin-install.js");
|
||||
const result = await repairMissingConfiguredPluginInstalls({
|
||||
cfg: {
|
||||
plugins: {
|
||||
entries: {
|
||||
matrix: { enabled: true },
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
matrix: { enabled: true },
|
||||
},
|
||||
},
|
||||
env: {
|
||||
OPENCLAW_UPDATE_POST_CORE_CONVERGENCE: "1",
|
||||
},
|
||||
});
|
||||
|
||||
expect(mocks.installPluginFromClawHub).not.toHaveBeenCalled();
|
||||
expect(mocks.installPluginFromNpmSpec).not.toHaveBeenCalled();
|
||||
expect(result.warnings).toEqual([]);
|
||||
expectRecordFields(result.records.matrix, {
|
||||
source: "npm",
|
||||
spec: "@openclaw/matrix",
|
||||
installPath: packageDir,
|
||||
version: "1.2.3",
|
||||
resolvedName: "@openclaw/matrix",
|
||||
resolvedVersion: "1.2.3",
|
||||
resolvedSpec: "@openclaw/matrix@1.2.3",
|
||||
});
|
||||
});
|
||||
|
||||
it("passes the post-core compatibility host version to ClawHub repair", async () => {
|
||||
const npmRoot = makeTempDir();
|
||||
mocks.resolveDefaultPluginNpmDir.mockReturnValue(npmRoot);
|
||||
mocks.listChannelPluginCatalogEntries.mockReturnValue([
|
||||
{
|
||||
id: "whatsapp",
|
||||
pluginId: "whatsapp",
|
||||
meta: { label: "WhatsApp" },
|
||||
install: {
|
||||
clawhubSpec: "clawhub:@openclaw/whatsapp",
|
||||
npmSpec: "@openclaw/whatsapp",
|
||||
},
|
||||
},
|
||||
]);
|
||||
mocks.installPluginFromClawHub.mockResolvedValue({
|
||||
ok: true,
|
||||
pluginId: "whatsapp",
|
||||
targetDir: "/tmp/openclaw-plugins/whatsapp",
|
||||
version: "1.2.3",
|
||||
clawhub: {
|
||||
source: "clawhub",
|
||||
clawhubUrl: "https://clawhub.ai",
|
||||
clawhubPackage: "@openclaw/whatsapp",
|
||||
clawhubFamily: "code-plugin",
|
||||
clawhubChannel: "official",
|
||||
version: "1.2.3",
|
||||
integrity: "sha256-whatsapp",
|
||||
resolvedAt: "2026-05-01T00:00:00.000Z",
|
||||
clawpackSha256: "2".repeat(64),
|
||||
clawpackSpecVersion: 1,
|
||||
clawpackManifestSha256: "3".repeat(64),
|
||||
clawpackSize: 1234,
|
||||
},
|
||||
});
|
||||
|
||||
const { repairMissingConfiguredPluginInstalls } =
|
||||
await import("./missing-configured-plugin-install.js");
|
||||
const result = await repairMissingConfiguredPluginInstalls({
|
||||
cfg: {
|
||||
plugins: {
|
||||
entries: {
|
||||
whatsapp: { enabled: true },
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
whatsapp: { enabled: true },
|
||||
},
|
||||
},
|
||||
env: {
|
||||
OPENCLAW_COMPATIBILITY_HOST_VERSION: "2026.5.19",
|
||||
OPENCLAW_UPDATE_POST_CORE_CONVERGENCE: "1",
|
||||
},
|
||||
});
|
||||
|
||||
expectRecordFields(mockCallArg(mocks.installPluginFromClawHub), {
|
||||
spec: "clawhub:@openclaw/whatsapp",
|
||||
env: {
|
||||
OPENCLAW_COMPATIBILITY_HOST_VERSION: "2026.5.19",
|
||||
OPENCLAW_UPDATE_POST_CORE_CONVERGENCE: "1",
|
||||
},
|
||||
mode: "install",
|
||||
});
|
||||
expect(mocks.installPluginFromNpmSpec).not.toHaveBeenCalled();
|
||||
expect(result.warnings).toEqual([]);
|
||||
expectRecordFields(result.records.whatsapp, {
|
||||
source: "clawhub",
|
||||
spec: "clawhub:@openclaw/whatsapp",
|
||||
installPath: "/tmp/openclaw-plugins/whatsapp",
|
||||
clawhubPackage: "@openclaw/whatsapp",
|
||||
});
|
||||
});
|
||||
|
||||
it("repairs missing external payload during post-core convergence even with OPENCLAW_UPDATE_IN_PROGRESS=1", async () => {
|
||||
const records = {
|
||||
discord: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { rm } from "node:fs/promises";
|
||||
import { readFile, rm } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import {
|
||||
listExplicitlyDisabledChannelIdsForConfig,
|
||||
@@ -54,6 +54,7 @@ import {
|
||||
} from "./configured-runtime-plugin-installs.js";
|
||||
import { asObjectRecord } from "./object.js";
|
||||
import {
|
||||
isPostCoreConvergencePass,
|
||||
isLegacyPackageUpdateDoctorPass,
|
||||
shouldDeferConfiguredPluginInstallRepair,
|
||||
} from "./update-phase.js";
|
||||
@@ -786,14 +787,35 @@ async function installCandidate(params: {
|
||||
const existingNpmPackagePath = npmInstallSpec
|
||||
? resolveExistingCandidateNpmPackagePath({ candidate, npmDir })
|
||||
: null;
|
||||
const existingNpmPackageVersion = existingNpmPackagePath
|
||||
? await readNpmPackageVersion(existingNpmPackagePath)
|
||||
: undefined;
|
||||
if (
|
||||
existingNpmPackagePath &&
|
||||
existingNpmPackageVersion &&
|
||||
npmInstallSpec &&
|
||||
params.mode !== "update" &&
|
||||
isPostCoreConvergencePass(params.env)
|
||||
) {
|
||||
return await adoptExistingNpmPackage({
|
||||
candidate,
|
||||
records: params.records,
|
||||
npmInstallSpec,
|
||||
npmRecordSpec: npmSpecs?.recordSpec ?? npmInstallSpec,
|
||||
packagePath: existingNpmPackagePath,
|
||||
version: existingNpmPackageVersion,
|
||||
});
|
||||
}
|
||||
const shouldTryClawHub =
|
||||
clawhubInstallSpec &&
|
||||
!existingNpmPackagePath &&
|
||||
!(params.preferNpm && npmInstallSpec) &&
|
||||
candidate.defaultChoice !== "npm";
|
||||
if (shouldTryClawHub) {
|
||||
const clawhubResult = await installPluginFromClawHub({
|
||||
spec: clawhubInstallSpec,
|
||||
extensionsDir,
|
||||
env: params.env,
|
||||
expectedPluginId: candidate.pluginId,
|
||||
mode: params.mode === "update" || existingClawHubPackagePath ? "update" : "install",
|
||||
});
|
||||
@@ -929,6 +951,53 @@ function resolveExistingCandidateClawHubPackagePath(params: {
|
||||
}
|
||||
}
|
||||
|
||||
async function readNpmPackageVersion(packagePath: string): Promise<string | undefined> {
|
||||
try {
|
||||
const parsed = JSON.parse(await readFile(path.join(packagePath, "package.json"), "utf-8")) as {
|
||||
version?: unknown;
|
||||
};
|
||||
return typeof parsed.version === "string" && parsed.version.trim()
|
||||
? parsed.version.trim()
|
||||
: undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function adoptExistingNpmPackage(params: {
|
||||
candidate: DownloadableInstallCandidate;
|
||||
records: Record<string, PluginInstallRecord>;
|
||||
npmInstallSpec: string;
|
||||
npmRecordSpec: string;
|
||||
packagePath: string;
|
||||
version: string;
|
||||
}): Promise<{
|
||||
records: Record<string, PluginInstallRecord>;
|
||||
changes: string[];
|
||||
warnings: string[];
|
||||
}> {
|
||||
const npmName = parseRegistryNpmSpec(params.npmInstallSpec)?.name;
|
||||
return {
|
||||
records: {
|
||||
...params.records,
|
||||
[params.candidate.pluginId]: {
|
||||
source: "npm",
|
||||
spec: params.npmRecordSpec,
|
||||
installPath: params.packagePath,
|
||||
installedAt: new Date().toISOString(),
|
||||
version: params.version,
|
||||
resolvedVersion: params.version,
|
||||
...(npmName ? { resolvedName: npmName } : {}),
|
||||
...(npmName ? { resolvedSpec: `${npmName}@${params.version}` } : {}),
|
||||
},
|
||||
},
|
||||
changes: [
|
||||
`Repaired missing configured plugin "${params.candidate.pluginId}" from existing npm payload ${params.npmInstallSpec}.`,
|
||||
],
|
||||
warnings: [],
|
||||
};
|
||||
}
|
||||
|
||||
export type RepairMissingPluginInstallsResult = {
|
||||
changes: string[];
|
||||
warnings: string[];
|
||||
|
||||
@@ -428,7 +428,7 @@ describe("configured plugin install release step", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("repairs package-manager plugins for writable legacy parents without explicit deferral", async () => {
|
||||
it("defers package-manager plugin release completion for writable legacy parents", async () => {
|
||||
mocks.repairMissingPluginInstallsForIds.mockResolvedValue({
|
||||
changes: ['Installed missing configured plugin "discord".'],
|
||||
warnings: [],
|
||||
@@ -459,8 +459,8 @@ describe("configured plugin install release step", () => {
|
||||
expect(result).toEqual({
|
||||
changes: ['Installed missing configured plugin "discord".'],
|
||||
warnings: [],
|
||||
completed: true,
|
||||
touchedConfig: true,
|
||||
completed: false,
|
||||
touchedConfig: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ describe("update-phase env helpers", () => {
|
||||
expect(isPostCoreConvergencePass(env)).toBe(true);
|
||||
});
|
||||
|
||||
it("defers configured plugin repair only for explicit update handoffs", () => {
|
||||
it("defers configured plugin repair for post-core handoffs", () => {
|
||||
expect(
|
||||
shouldDeferConfiguredPluginInstallRepair({
|
||||
[UPDATE_IN_PROGRESS_ENV]: "1",
|
||||
@@ -53,7 +53,7 @@ describe("update-phase env helpers", () => {
|
||||
[UPDATE_IN_PROGRESS_ENV]: "1",
|
||||
[UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV]: "1",
|
||||
}),
|
||||
).toBe(false);
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldDeferConfiguredPluginInstallRepair({
|
||||
[UPDATE_IN_PROGRESS_ENV]: "1",
|
||||
@@ -80,7 +80,7 @@ describe("update-phase env helpers", () => {
|
||||
[UPDATE_IN_PROGRESS_ENV]: "1",
|
||||
[UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV]: "1",
|
||||
}),
|
||||
).toBe(true);
|
||||
).toBe(false);
|
||||
expect(
|
||||
isLegacyPackageUpdateDoctorPass({
|
||||
[UPDATE_IN_PROGRESS_ENV]: "1",
|
||||
|
||||
@@ -40,14 +40,15 @@ export function isUpdatePackageSwapInProgress(env: NodeJS.ProcessEnv): boolean {
|
||||
/**
|
||||
* True iff configured plugin install repair should be deferred because the
|
||||
* updater guarantees a later post-core convergence pass. Older shipped
|
||||
* parents may set only the writable-config marker; they still resume after
|
||||
* the candidate doctor exits, so the candidate must repair payloads before
|
||||
* control returns to that stale process.
|
||||
* parents may set only the writable-config marker. Those parents still have a
|
||||
* post-core handoff, but their in-memory install records are stale after the
|
||||
* candidate doctor exits, so defer payload repair to the updated child process.
|
||||
*/
|
||||
export function shouldDeferConfiguredPluginInstallRepair(env: NodeJS.ProcessEnv): boolean {
|
||||
return (
|
||||
isUpdatePackageSwapInProgress(env) &&
|
||||
isTruthyEnvValue(env[UPDATE_DEFER_CONFIGURED_PLUGIN_INSTALL_REPAIR_ENV])
|
||||
(isTruthyEnvValue(env[UPDATE_DEFER_CONFIGURED_PLUGIN_INSTALL_REPAIR_ENV]) ||
|
||||
isTruthyEnvValue(env[UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV]))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -67,9 +68,9 @@ export function isLegacyParentWritableUpdateDoctorPass(env: NodeJS.ProcessEnv):
|
||||
}
|
||||
|
||||
/**
|
||||
* True iff this newer doctor is running under an older updater. Legacy
|
||||
* updaters set only `OPENCLAW_UPDATE_IN_PROGRESS`; they do not opt into the
|
||||
* post-core convergence pass, so configured plugin repair must happen now.
|
||||
* True iff this newer doctor is running under an older updater that does not
|
||||
* advertise any post-core handoff marker. Those parents set only
|
||||
* `OPENCLAW_UPDATE_IN_PROGRESS`, so configured plugin repair must happen now.
|
||||
*/
|
||||
export function isLegacyPackageUpdateDoctorPass(env: NodeJS.ProcessEnv): boolean {
|
||||
return isUpdatePackageSwapInProgress(env) && !shouldDeferConfiguredPluginInstallRepair(env);
|
||||
|
||||
@@ -238,12 +238,15 @@ describe("update global helpers", () => {
|
||||
expect(isExplicitPackageInstallSpec("github:openclaw/openclaw#main")).toBe(true);
|
||||
expect(isExplicitPackageInstallSpec("https://example.com/openclaw-main.tgz")).toBe(true);
|
||||
expect(isExplicitPackageInstallSpec("file:/tmp/openclaw-main.tgz")).toBe(true);
|
||||
expect(isExplicitPackageInstallSpec("/tmp/openclaw-main.tgz")).toBe(true);
|
||||
expect(isExplicitPackageInstallSpec("openclaw-main.tgz")).toBe(true);
|
||||
expect(isExplicitPackageInstallSpec("beta")).toBe(false);
|
||||
|
||||
expect(canResolveRegistryVersionForPackageTarget("latest")).toBe(true);
|
||||
expect(canResolveRegistryVersionForPackageTarget("2026.3.22")).toBe(true);
|
||||
expect(canResolveRegistryVersionForPackageTarget("main")).toBe(false);
|
||||
expect(canResolveRegistryVersionForPackageTarget("github:openclaw/openclaw#main")).toBe(false);
|
||||
expect(canResolveRegistryVersionForPackageTarget("/tmp/openclaw-main.tgz")).toBe(false);
|
||||
});
|
||||
|
||||
it("detects install managers from resolved roots and on-disk presence", async () => {
|
||||
|
||||
@@ -79,6 +79,7 @@ export function isExplicitPackageInstallSpec(value: string): boolean {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
/\.(?:tgz|tar\.gz)$/iu.test(trimmed) ||
|
||||
trimmed.includes("://") ||
|
||||
trimmed.includes("#") ||
|
||||
/^(?:file|github|git\+ssh|git\+https|git\+http|git\+file|npm):/i.test(trimmed)
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import { normalizeOptionalString } from "../shared/string-coerce.js";
|
||||
import { resolveCompatibilityHostVersion } from "../version.js";
|
||||
import type { RuntimeVersionEnv } from "../version.js";
|
||||
import type { ClawHubPluginInstallRecordFields } from "./clawhub-install-records.js";
|
||||
import type { InstallSafetyOverrides } from "./install-security-scan.js";
|
||||
import { installPluginFromArchive, type InstallPluginResult } from "./install.js";
|
||||
@@ -1057,6 +1058,7 @@ export async function installPluginFromClawHub(
|
||||
timeoutMs?: number;
|
||||
dryRun?: boolean;
|
||||
expectedPluginId?: string;
|
||||
env?: RuntimeVersionEnv;
|
||||
},
|
||||
): Promise<
|
||||
| ({
|
||||
@@ -1101,7 +1103,7 @@ export async function installPluginFromClawHub(
|
||||
if (!versionState.ok) {
|
||||
return versionState;
|
||||
}
|
||||
const runtimeVersion = resolveCompatibilityHostVersion();
|
||||
const runtimeVersion = resolveCompatibilityHostVersion(params.env);
|
||||
const validationFailure = validateClawHubPluginPackage({
|
||||
detail,
|
||||
compatibility: versionState.compatibility,
|
||||
|
||||
@@ -978,7 +978,7 @@ describe("package artifact reuse", () => {
|
||||
}
|
||||
|
||||
expect(fullRelease.jobs?.release_checks?.["timeout-minutes"]).toBe(
|
||||
"${{ inputs.release_profile == 'full' && 240 || 60 }}",
|
||||
"${{ inputs.release_profile != 'minimum' && 240 || 60 }}",
|
||||
);
|
||||
expect(fullRelease.jobs?.prepare_release_package?.["timeout-minutes"]).toBe(15);
|
||||
expect(releaseChecks.jobs?.prepare_release_package?.["timeout-minutes"]).toBe(15);
|
||||
|
||||
@@ -490,6 +490,15 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => {
|
||||
]) {
|
||||
expect(fullReleaseWorkflow.jobs[jobName]["runs-on"]).toBe("ubuntu-24.04");
|
||||
}
|
||||
expect(fullReleaseWorkflow.jobs.normal_ci["timeout-minutes"]).toBe(
|
||||
"${{ inputs.release_profile != 'minimum' && 240 || 60 }}",
|
||||
);
|
||||
expect(fullReleaseWorkflow.jobs.plugin_prerelease["timeout-minutes"]).toBe(
|
||||
"${{ inputs.release_profile == 'full' && 300 || inputs.release_profile == 'stable' && 240 || 60 }}",
|
||||
);
|
||||
expect(fullReleaseWorkflow.jobs.release_checks["timeout-minutes"]).toBe(
|
||||
"${{ inputs.release_profile != 'minimum' && 240 || 60 }}",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps runtime tool coverage blocking in release checks", () => {
|
||||
|
||||
Reference in New Issue
Block a user