From 8c4ecf42dfcf8f0265081e2801d221a70dc96886 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 19 Apr 2026 09:04:58 +0100 Subject: [PATCH] fix: stabilize release smoke reruns --- scripts/e2e/parallels-windows-smoke.sh | 1 + scripts/stage-bundled-plugin-runtime-deps.mjs | 28 +++++++++++++- .../stage-bundled-plugin-runtime-deps.test.ts | 37 +++++++++++++++++++ ui/src/ui/controllers/cron.ts | 2 +- 4 files changed, 65 insertions(+), 3 deletions(-) diff --git a/scripts/e2e/parallels-windows-smoke.sh b/scripts/e2e/parallels-windows-smoke.sh index f032803dca7..f4e859492e5 100644 --- a/scripts/e2e/parallels-windows-smoke.sh +++ b/scripts/e2e/parallels-windows-smoke.sh @@ -2349,6 +2349,7 @@ run_fresh_main_lane() { FRESH_MAIN_VERSION="$(extract_last_version "$(phase_log_path "$install_log_phase")")" phase_run "fresh.verify-main-version" "$TIMEOUT_VERIFY_S" verify_target_version || return $? phase_run "fresh.onboard-ref" "$TIMEOUT_ONBOARD_PHASE_S" run_ref_onboard || return $? + phase_run "fresh.gateway-restart" "$TIMEOUT_GATEWAY_S" restart_gateway || return $? phase_run "fresh.gateway-status" "$TIMEOUT_GATEWAY_S" verify_gateway_reachable || return $? FRESH_GATEWAY_STATUS="pass" phase_run "fresh.first-agent-turn" "$TIMEOUT_AGENT_S" verify_turn || return $? diff --git a/scripts/stage-bundled-plugin-runtime-deps.mjs b/scripts/stage-bundled-plugin-runtime-deps.mjs index cef89d6e272..e672861fe71 100644 --- a/scripts/stage-bundled-plugin-runtime-deps.mjs +++ b/scripts/stage-bundled-plugin-runtime-deps.mjs @@ -241,12 +241,14 @@ function collectInstalledRuntimeDependencyRoots( rootNodeModulesDir, dependencySpecs, directDependencyPackageRoot = null, + optionalDependencyNames = new Set(), ) { const packageCache = new Map(); const directRoots = []; const allRoots = []; const queue = Object.entries(dependencySpecs).map(([depName, spec]) => ({ depName, + optional: optionalDependencyNames.has(depName), spec, parentPackageRoot: directDependencyPackageRoot, direct: true, @@ -263,6 +265,9 @@ function collectInstalledRuntimeDependencyRoots( rootNodeModulesDir, }); if (depRoot === null) { + if (current.optional) { + continue; + } return null; } const canonicalDepRoot = fs.realpathSync(depRoot); @@ -285,6 +290,7 @@ function collectInstalledRuntimeDependencyRoots( for (const [childName, childSpec] of Object.entries(packageJson.dependencies ?? {})) { queue.push({ depName: childName, + optional: false, spec: childSpec, parentPackageRoot: depRoot, direct: false, @@ -293,6 +299,7 @@ function collectInstalledRuntimeDependencyRoots( for (const [childName, childSpec] of Object.entries(packageJson.optionalDependencies ?? {})) { queue.push({ depName: childName, + optional: true, spec: childSpec, parentPackageRoot: depRoot, direct: false, @@ -391,6 +398,7 @@ function resolveInstalledDirectDependencyNames( rootNodeModulesDir, dependencySpecs, directDependencyPackageRoot = null, + optionalDependencyNames = new Set(), ) { const directDependencyNames = []; for (const [depName, spec] of Object.entries(dependencySpecs)) { @@ -401,6 +409,9 @@ function resolveInstalledDirectDependencyNames( rootNodeModulesDir, }); if (depRoot === null) { + if (optionalDependencyNames.has(depName)) { + continue; + } return null; } const installedVersion = readInstalledDependencyVersionFromRoot(depRoot); @@ -463,6 +474,7 @@ function resolveInstalledRuntimeClosureFingerprint(params) { params.rootNodeModulesDir, dependencySpecs, params.directDependencyPackageRoot, + new Set(Object.keys(params.packageJson.optionalDependencies ?? {})), ); if (resolution === null) { return null; @@ -890,6 +902,7 @@ function stageInstalledRootRuntimeDeps(params) { ...packageJson.dependencies, ...packageJson.optionalDependencies, }; + const optionalDependencyNames = new Set(Object.keys(packageJson.optionalDependencies ?? {})); const rootNodeModulesDir = path.join(repoRoot, "node_modules"); if (Object.keys(dependencySpecs).length === 0 || !fs.existsSync(rootNodeModulesDir)) { return false; @@ -899,6 +912,7 @@ function stageInstalledRootRuntimeDeps(params) { rootNodeModulesDir, dependencySpecs, directDependencyPackageRoot, + optionalDependencyNames, ); if (directDependencyNames === null) { return false; @@ -907,15 +921,25 @@ function stageInstalledRootRuntimeDeps(params) { rootNodeModulesDir, dependencySpecs, directDependencyPackageRoot, + optionalDependencyNames, ); if (resolution === null) { return false; } const rootsToCopy = selectRuntimeDependencyRootsToCopy(resolution); - const allowedRealRoots = rootsToCopy.map((record) => record.realRoot); - const nodeModulesDir = path.join(pluginDir, "node_modules"); const stampPath = resolveRuntimeDepsStampPath(pluginDir); + if (rootsToCopy.length === 0) { + assertPathIsNotSymlink(nodeModulesDir, "remove runtime deps"); + removePathIfExists(nodeModulesDir); + writeJsonAtomically(stampPath, { + fingerprint, + generatedAt: new Date().toISOString(), + }); + return true; + } + const allowedRealRoots = rootsToCopy.map((record) => record.realRoot); + const stagedNodeModulesDir = path.join( makePluginOwnedTempDir(pluginDir, "stage"), "node_modules", diff --git a/test/scripts/stage-bundled-plugin-runtime-deps.test.ts b/test/scripts/stage-bundled-plugin-runtime-deps.test.ts index ed6190d517b..d439ab5a75a 100644 --- a/test/scripts/stage-bundled-plugin-runtime-deps.test.ts +++ b/test/scripts/stage-bundled-plugin-runtime-deps.test.ts @@ -362,6 +362,43 @@ describe("stageBundledPluginRuntimeDeps", () => { expect(fs.existsSync(path.join(pluginDir, ".openclaw-runtime-deps-stamp.json"))).toBe(true); }); + it("skips missing optional runtime deps when copying the installed closure", () => { + const { pluginDir, repoRoot } = createBundledPluginFixture({ + packageJson: { + name: "@openclaw/fixture-plugin", + version: "1.0.0", + dependencies: { direct: "1.0.0" }, + optionalDependencies: { missingOptional: "1.0.0" }, + openclaw: { bundle: { stageRuntimeDependencies: true } }, + }, + }); + const directDir = path.join(repoRoot, "node_modules", "direct"); + fs.mkdirSync(directDir, { recursive: true }); + fs.writeFileSync( + path.join(directDir, "package.json"), + '{ "name": "direct", "version": "1.0.0", "optionalDependencies": { "native-extra": "1.0.0" } }\n', + "utf8", + ); + fs.writeFileSync(path.join(directDir, "index.js"), "module.exports = 1;\n", "utf8"); + + let installCount = 0; + stageBundledPluginRuntimeDeps({ + cwd: repoRoot, + installPluginRuntimeDepsImpl: () => { + installCount += 1; + }, + }); + + expect(installCount).toBe(0); + expect( + fs.readFileSync(path.join(pluginDir, "node_modules", "direct", "index.js"), "utf8"), + ).toBe("module.exports = 1;\n"); + expect(fs.existsSync(path.join(pluginDir, "node_modules", "missingOptional"))).toBe(false); + expect( + fs.existsSync(path.join(pluginDir, "node_modules", "direct", "node_modules", "native-extra")), + ).toBe(false); + }); + it("prunes staged test cargo from copied runtime dependencies", () => { const { pluginDir, repoRoot } = createBundledPluginFixture({ packageJson: { diff --git a/ui/src/ui/controllers/cron.ts b/ui/src/ui/controllers/cron.ts index 044516c3de2..409b1b979b2 100644 --- a/ui/src/ui/controllers/cron.ts +++ b/ui/src/ui/controllers/cron.ts @@ -611,7 +611,7 @@ function normalizePersistedDeliveryChannel( return channel; } -function buildFailureAlert(form: CronFormState, existingChannel?: string | undefined) { +function buildFailureAlert(form: CronFormState, existingChannel?: string) { if (form.failureAlertMode === "disabled") { return false as const; }