Compare commits

...

14 Commits

Author SHA1 Message Date
Peter Steinberger
a185ca283a test: align release timeout budget expectations 2026-05-20 18:06:09 +01:00
Peter Steinberger
ca3c3fca43 ci: extend stable release validation monitors 2026-05-20 17:48:04 +01:00
Peter Steinberger
a6172a7d0e ci: preserve node path across setup action steps 2026-05-20 16:27:29 +01:00
Peter Steinberger
2823725134 fix: preserve update compatibility host during release upgrades 2026-05-20 15:07:51 +01:00
Peter Steinberger
eab57ad8ad fix(update): prefer npm during post-core repair 2026-05-20 14:00:35 +01:00
Peter Steinberger
93c2d1ea99 fix(update): defer legacy parent plugin repair 2026-05-20 13:43:17 +01:00
Peter Steinberger
29faac2f9c fix(update): adopt post-core plugin payloads 2026-05-20 13:20:59 +01:00
Peter Steinberger
e8d8c5dd6f fix(update): preserve post-core host version 2026-05-20 12:56:18 +01:00
Peter Steinberger
3743d6bdeb fix(update): prefer existing npm plugins during repair 2026-05-20 12:25:01 +01:00
Peter Steinberger
b14ae2fbea chore(release): prepare 2026.5.19 stable 2026-05-20 11:48:35 +01:00
Peter Steinberger
d0eec21887 docs: keep developer tooling out of release tweets 2026-05-20 11:40:00 +01:00
Peter Steinberger
aa5706f5e8 docs: keep qa proof out of release tweets 2026-05-20 11:37:36 +01:00
Peter Steinberger
947a07016e chore(release): prepare 2026.5.19 beta 2 2026-05-20 05:34:30 +01:00
Peter Steinberger
430f868cb6 chore(release): prepare 2026.5.19 beta 1 2026-05-20 05:34:30 +01:00
18 changed files with 303 additions and 22 deletions

View File

@@ -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.

View File

@@ -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'

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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();

View File

@@ -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",
},
});

View File

@@ -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",
};

View File

@@ -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],

View File

@@ -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: {

View File

@@ -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[];

View File

@@ -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,
});
});

View File

@@ -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",

View File

@@ -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);

View File

@@ -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 () => {

View File

@@ -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)

View File

@@ -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,

View File

@@ -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);

View File

@@ -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", () => {