Skip to content

Commit 333f65f

Browse files
committed
fix: tighten release tooling checks
1 parent 0b7ff66 commit 333f65f

11 files changed

Lines changed: 232 additions & 27 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai
1212

1313
### Fixes
1414

15+
- Release tooling: align the published launcher Node floor, `npm start`, package script checks, sharded lint locking, Vitest root project coverage, and plugin-SDK declaration build cache metadata so release/package validation does not silently skip or ship stale surfaces.
1516
- Codex app-server/MCP: scope user MCP servers to specific OpenClaw agent ids through an optional `mcp.servers.<name>.codex.agents` list and accept `codex.defaultToolsApprovalMode` (`auto`/`prompt`/`approve`) for native Codex approval defaults; OpenClaw strips the `codex` block before handing `mcp_servers` config to Codex. (#82180) Thanks @sercada.
1617
- Agents/OpenAI Responses: clamp `input_tokens - cached_tokens` at zero and reconstruct `totalTokens` from input + output + cached components so Responses-API streams report consistent usage when providers under-report `input_tokens` relative to `cached_tokens`.
1718
- Plugins: reject malformed `package.json` `openclaw.extensions` metadata during install, discovery, and post-update payload smoke instead of silently dropping invalid entries.

openclaw.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import path from "node:path";
99
import { fileURLToPath } from "node:url";
1010

1111
const MIN_NODE_MAJOR = 22;
12-
const MIN_NODE_MINOR = 12;
12+
const MIN_NODE_MINOR = 16;
1313
const MIN_NODE_VERSION = `${MIN_NODE_MAJOR}.${MIN_NODE_MINOR}`;
1414

1515
const parseNodeVersion = (rawVersion) => {

package.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1360,9 +1360,6 @@
13601360
"build:plugin-sdk:dts": "node scripts/run-tsgo.mjs -p tsconfig.plugin-sdk.dts.json --declaration true",
13611361
"build:plugin-sdk:strict-smoke": "pnpm build:plugin-sdk:dts && node --experimental-strip-types scripts/write-plugin-sdk-entry-dts.ts",
13621362
"build:strict-smoke": "pnpm plugins:assets:build && node scripts/tsdown-build.mjs && node scripts/check-cli-bootstrap-imports.mjs && node scripts/runtime-postbuild.mjs && node scripts/build-stamp.mjs && node scripts/runtime-postbuild-stamp.mjs && pnpm build:plugin-sdk:dts && node --experimental-strip-types scripts/write-plugin-sdk-entry-dts.ts && node scripts/check-plugin-sdk-exports.mjs",
1363-
"canon:check": "node scripts/canon.mjs check",
1364-
"canon:check:json": "node scripts/canon.mjs check --json",
1365-
"canon:enforce": "node scripts/canon.mjs enforce --json",
13661363
"canvas:a2ui:bundle": "node scripts/bundle-a2ui.mjs",
13671364
"changed:lanes": "node scripts/changed-lanes.mjs",
13681365
"check": "node scripts/check.mjs",
@@ -1572,7 +1569,7 @@
15721569
"rtt": "node --import tsx scripts/rtt.ts",
15731570
"runtime-sidecars:check": "node --import tsx scripts/generate-runtime-sidecar-paths-baseline.ts --check",
15741571
"runtime-sidecars:gen": "node --import tsx scripts/generate-runtime-sidecar-paths-baseline.ts --write",
1575-
"start": "node scripts/run-node.mjs",
1572+
"start": "node openclaw.mjs",
15761573
"test": "node scripts/test-projects.mjs",
15771574
"test:all": "pnpm lint && pnpm build && pnpm test && pnpm test:e2e && pnpm test:live && pnpm test:docker:all",
15781575
"test:auth:compat": "node scripts/run-vitest.mjs run --config test/vitest/vitest.gateway.config.ts src/gateway/server.auth.compat-baseline.test.ts src/gateway/client.test.ts src/gateway/reconnect-gating.test.ts src/gateway/protocol/connect-error-details.test.ts",

scripts/build-all.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,12 @@ export const BUILD_ALL_STEPS = [
3535
"tsconfig.json",
3636
"tsconfig.plugin-sdk.dts.json",
3737
"src/plugin-sdk",
38+
"packages/memory-host-sdk/src",
3839
"src/types",
3940
"src/video-generation/dashscope-compatible.ts",
4041
"src/video-generation/types.ts",
4142
],
42-
outputs: ["dist/plugin-sdk/.tsbuildinfo", "dist/plugin-sdk/src"],
43+
outputs: ["dist/plugin-sdk/.tsbuildinfo", "dist/plugin-sdk/packages", "dist/plugin-sdk/src"],
4344
},
4445
},
4546
{

scripts/run-oxlint-shards.mjs

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,33 @@
11
import { spawn, spawnSync } from "node:child_process";
22
import path from "node:path";
3+
import {
4+
acquireLocalHeavyCheckLockSync,
5+
resolveLocalHeavyCheckEnv,
6+
shouldAcquireLocalHeavyCheckLockForOxlint,
7+
} from "./lib/local-heavy-check-runtime.mjs";
38

49
const extraArgs = process.argv.slice(2);
510
const runner = path.resolve("scripts", "run-oxlint.mjs");
6-
7-
const prepareResult = spawnSync(
8-
process.execPath,
9-
[path.resolve("scripts", "prepare-extension-package-boundary-artifacts.mjs")],
10-
{
11-
stdio: "inherit",
12-
env: process.env,
13-
},
11+
const env = resolveLocalHeavyCheckEnv(process.env);
12+
const hasMetadataOnlyFlag = extraArgs.some((arg) =>
13+
["--help", "-h", "--version", "-V", "--rules", "--print-config", "--init"].includes(arg),
1414
);
15-
16-
if (prepareResult.error) {
17-
throw prepareResult.error;
18-
}
19-
if ((prepareResult.status ?? 1) !== 0) {
20-
process.exit(prepareResult.status ?? 1);
21-
}
15+
const shouldAcquireParentLock =
16+
!hasMetadataOnlyFlag ||
17+
shouldAcquireLocalHeavyCheckLockForOxlint(extraArgs, {
18+
cwd: process.cwd(),
19+
env,
20+
});
21+
const releaseLock =
22+
env.OPENCLAW_OXLINT_SKIP_LOCK === "1"
23+
? () => {}
24+
: shouldAcquireParentLock
25+
? acquireLocalHeavyCheckLockSync({
26+
cwd: process.cwd(),
27+
env,
28+
toolName: "oxlint shards",
29+
})
30+
: () => {};
2231

2332
const shards = [
2433
{
@@ -35,11 +44,31 @@ const shards = [
3544
},
3645
];
3746

38-
const runSerial = process.env.OPENCLAW_OXLINT_SHARDS_SERIAL === "1";
39-
const results = runSerial
40-
? await runShardsSerial(shards)
41-
: await Promise.all(shards.map((shard) => runShard(shard)));
42-
process.exitCode = results.find((status) => status !== 0) ?? 0;
47+
try {
48+
const prepareResult = spawnSync(
49+
process.execPath,
50+
[path.resolve("scripts", "prepare-extension-package-boundary-artifacts.mjs")],
51+
{
52+
stdio: "inherit",
53+
env,
54+
},
55+
);
56+
57+
if (prepareResult.error) {
58+
throw prepareResult.error;
59+
}
60+
if ((prepareResult.status ?? 1) !== 0) {
61+
process.exitCode = prepareResult.status ?? 1;
62+
} else {
63+
const runSerial = env.OPENCLAW_OXLINT_SHARDS_SERIAL === "1";
64+
const results = runSerial
65+
? await runShardsSerial(shards)
66+
: await Promise.all(shards.map((shard) => runShard(shard)));
67+
process.exitCode = results.find((status) => status !== 0) ?? 0;
68+
}
69+
} finally {
70+
releaseLock();
71+
}
4372

4473
async function runShardsSerial(entries) {
4574
const results = [];
@@ -54,7 +83,7 @@ async function runShard(shard) {
5483
const child = spawn(process.execPath, [runner, ...shard.args, ...extraArgs], {
5584
stdio: "inherit",
5685
env: {
57-
...process.env,
86+
...env,
5887
OPENCLAW_OXLINT_SKIP_LOCK: "1",
5988
OPENCLAW_OXLINT_SKIP_PREPARE: "1",
6089
},

test/openclaw-launcher.e2e.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,30 @@ describe("openclaw launcher", () => {
139139
cleanupTempDirs(fixtureRoots);
140140
});
141141

142+
it("keeps the bootstrap Node floor aligned with package and runtime guards", async () => {
143+
const [launcher, runtimeGuard, packageJsonRaw] = await Promise.all([
144+
fs.readFile(path.resolve(process.cwd(), "openclaw.mjs"), "utf8"),
145+
fs.readFile(path.resolve(process.cwd(), "src/infra/runtime-guard.ts"), "utf8"),
146+
fs.readFile(path.resolve(process.cwd(), "package.json"), "utf8"),
147+
]);
148+
const packageJson = JSON.parse(packageJsonRaw) as { engines?: { node?: string } };
149+
const launcherMatch = launcher.match(
150+
/const MIN_NODE_MAJOR = (\d+);\s+const MIN_NODE_MINOR = (\d+);/u,
151+
);
152+
const runtimeMatch = runtimeGuard.match(
153+
/const MIN_NODE: Semver = \{ major: (\d+), minor: (\d+), patch: (\d+) \};/u,
154+
);
155+
const engineMatch = packageJson.engines?.node?.match(/^>=(\d+)\.(\d+)\.(\d+)$/u);
156+
157+
expect(launcherMatch).not.toBeNull();
158+
expect(runtimeMatch).not.toBeNull();
159+
expect(engineMatch).not.toBeNull();
160+
expect(`${launcherMatch?.[1]}.${launcherMatch?.[2]}.0`).toBe(
161+
`${engineMatch?.[1]}.${engineMatch?.[2]}.${engineMatch?.[3]}`,
162+
);
163+
expect(runtimeMatch?.slice(1, 4)).toEqual(engineMatch?.slice(1, 4));
164+
});
165+
142166
it("surfaces transitive entry import failures instead of masking them as missing dist", async () => {
143167
const fixtureRoot = await makeLauncherFixture(fixtureRoots);
144168
await fs.writeFile(

test/package-scripts.test.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import fs from "node:fs";
2+
import { describe, expect, it } from "vitest";
3+
4+
type RootPackageJson = {
5+
scripts: Record<string, string>;
6+
};
7+
8+
const ENV_ASSIGNMENT_RE = /^[A-Za-z_][A-Za-z0-9_]*=/u;
9+
const NODE_OPTIONS_WITH_VALUE = new Set([
10+
"--conditions",
11+
"--env-file",
12+
"--env-file-if-exists",
13+
"--import",
14+
"--loader",
15+
"--max-old-space-size",
16+
"--require",
17+
"--test-name-pattern",
18+
"--test-reporter",
19+
"-C",
20+
"-r",
21+
]);
22+
23+
function readPackageJson(): RootPackageJson {
24+
return JSON.parse(fs.readFileSync("package.json", "utf8")) as RootPackageJson;
25+
}
26+
27+
function tokenizeCommand(command: string): string[] {
28+
return (
29+
command
30+
.match(/"[^"]*"|'[^']*'|[^\s]+/gu)
31+
?.map((token) => token.replace(/^(['"])(.*)\1$/u, "$2")) ?? []
32+
);
33+
}
34+
35+
function extractNodeScriptTargets(script: string): string[] {
36+
return script.split(/\s*(?:&&|\|\||;)\s*/u).flatMap((command) => {
37+
const tokens = tokenizeCommand(command);
38+
let index = tokens[0] === "env" ? 1 : 0;
39+
40+
while (ENV_ASSIGNMENT_RE.test(tokens[index] ?? "")) {
41+
index += 1;
42+
}
43+
44+
if (tokens[index] !== "node") {
45+
return [];
46+
}
47+
48+
for (let tokenIndex = index + 1; tokenIndex < tokens.length; tokenIndex += 1) {
49+
const token = tokens[tokenIndex];
50+
if (!token) {
51+
continue;
52+
}
53+
if (token.startsWith("scripts/")) {
54+
return [token];
55+
}
56+
if (token === "--") {
57+
continue;
58+
}
59+
if (token.startsWith("--") && token.includes("=")) {
60+
continue;
61+
}
62+
if (NODE_OPTIONS_WITH_VALUE.has(token)) {
63+
tokenIndex += 1;
64+
continue;
65+
}
66+
if (token.startsWith("-")) {
67+
continue;
68+
}
69+
70+
return [];
71+
}
72+
73+
return [];
74+
});
75+
}
76+
77+
describe("package scripts", () => {
78+
it("finds node script targets after env assignments and valued node options", () => {
79+
expect(
80+
extractNodeScriptTargets(
81+
"FOO=1 node --import tsx scripts/release-check.ts && node --max-old-space-size=8192 scripts/plugin-sdk-surface-report.mjs && env BAR=1 node -r tsx scripts/check.ts",
82+
),
83+
).toEqual([
84+
"scripts/release-check.ts",
85+
"scripts/plugin-sdk-surface-report.mjs",
86+
"scripts/check.ts",
87+
]);
88+
});
89+
90+
it("keeps direct node script targets present in the source checkout", () => {
91+
const packageJson = readPackageJson();
92+
const missingTargets = Object.entries(packageJson.scripts).flatMap(([name, script]) =>
93+
extractNodeScriptTargets(script)
94+
.filter((target) => !fs.existsSync(target))
95+
.map((target) => `${name}: ${target}`),
96+
);
97+
98+
expect(missingTargets).toEqual([]);
99+
});
100+
101+
it("uses the shipped package launcher for npm start", () => {
102+
expect(readPackageJson().scripts.start).toBe("node openclaw.mjs");
103+
});
104+
});

test/scripts/build-all.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,13 @@ describe("resolveBuildAllStep", () => {
124124
},
125125
});
126126
});
127+
128+
it("keeps plugin-sdk dts cache metadata aligned with declaration inputs", () => {
129+
const step = getBuildAllStep("build:plugin-sdk:dts");
130+
131+
expect(step.cache?.inputs).toEqual(expect.arrayContaining(["packages/memory-host-sdk/src"]));
132+
expect(step.cache?.outputs).toEqual(expect.arrayContaining(["dist/plugin-sdk/packages"]));
133+
});
127134
});
128135

129136
describe("resolveBuildAllSteps", () => {

test/scripts/run-oxlint.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,20 @@ describe("run-oxlint", () => {
3434
expect(shardedLintRunner).toContain('OPENCLAW_OXLINT_SKIP_PREPARE: "1"');
3535
});
3636

37+
it("holds one parent heavy-check lock for sharded lint runs", () => {
38+
const shardedLintRunner = readFileSync("scripts/run-oxlint-shards.mjs", "utf8");
39+
const skipLockIndex = shardedLintRunner.indexOf('env.OPENCLAW_OXLINT_SKIP_LOCK === "1"');
40+
const lockIndex = shardedLintRunner.indexOf("acquireLocalHeavyCheckLockSync({");
41+
const childSkipIndex = shardedLintRunner.indexOf('OPENCLAW_OXLINT_SKIP_LOCK: "1"');
42+
43+
expect(shardedLintRunner).toContain("resolveLocalHeavyCheckEnv");
44+
expect(shardedLintRunner).toContain("shouldAcquireLocalHeavyCheckLockForOxlint");
45+
expect(skipLockIndex).toBeGreaterThan(-1);
46+
expect(lockIndex).toBeGreaterThan(-1);
47+
expect(lockIndex).toBeGreaterThan(skipLockIndex);
48+
expect(childSkipIndex).toBeGreaterThan(lockIndex);
49+
});
50+
3751
it("lets dev update preflight run oxlint shards serially", () => {
3852
const shardedLintRunner = readFileSync("scripts/run-oxlint-shards.mjs", "utf8");
3953

test/vitest-projects-config.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
resolveSharedVitestWorkerConfig,
2626
sharedVitestConfig,
2727
} from "./vitest/vitest.shared.config.ts";
28+
import { fullSuiteVitestShards } from "./vitest/vitest.test-shards.mjs";
2829
import { createUiVitestConfig, unitUiIncludePatterns } from "./vitest/vitest.ui.config.ts";
2930
import { createUnitFastVitestConfig } from "./vitest/vitest.unit-fast.config.ts";
3031
import unitUiConfig from "./vitest/vitest.unit-ui.config.ts";
@@ -58,6 +59,29 @@ describe("projects vitest config", () => {
5859
expect(requireTestConfig(baseConfig).projects).toEqual([...rootVitestProjects]);
5960
});
6061

62+
it("keeps root watch projects aligned with dedicated extension shard lanes", () => {
63+
const extensionShard = fullSuiteVitestShards.find(
64+
(shard) => shard.config === "test/vitest/vitest.full-extensions.config.ts",
65+
);
66+
67+
expect(extensionShard?.projects).toEqual(
68+
expect.arrayContaining([
69+
"test/vitest/vitest.extension-browser.config.ts",
70+
"test/vitest/vitest.extension-qa.config.ts",
71+
"test/vitest/vitest.extension-media.config.ts",
72+
"test/vitest/vitest.extension-misc.config.ts",
73+
]),
74+
);
75+
expect(rootVitestProjects).toEqual(
76+
expect.arrayContaining([
77+
"test/vitest/vitest.extension-browser.config.ts",
78+
"test/vitest/vitest.extension-qa.config.ts",
79+
"test/vitest/vitest.extension-media.config.ts",
80+
"test/vitest/vitest.extension-misc.config.ts",
81+
]),
82+
);
83+
});
84+
6185
it("disables vite env-file loading for vitest lanes", () => {
6286
expect(baseConfig.envFile).toBe(false);
6387
expect(sharedVitestConfig.envFile).toBe(false);

0 commit comments

Comments
 (0)