Skip to content

Commit b63317d

Browse files
committed
fix(ci): install workspace packages for Kova
1 parent 686b6b5 commit b63317d

5 files changed

Lines changed: 219 additions & 0 deletions

File tree

.github/workflows/openclaw-performance.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,23 @@ jobs:
246246
chmod 0755 "$HOME/.local/bin/kova"
247247
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
248248
249+
- name: Configure OCM local workspace dependencies
250+
if: steps.lane.outputs.run == 'true'
251+
shell: bash
252+
run: |
253+
set -euo pipefail
254+
npm_wrapper="$PERFORMANCE_HELPER_DIR/scripts/ocm-npm-workspace-deps.mjs"
255+
workspace_dependency_dirs=""
256+
if [[ -f "${GITHUB_WORKSPACE}/packages/ai/package.json" ]]; then
257+
workspace_dependency_dirs="${GITHUB_WORKSPACE}/packages/ai"
258+
fi
259+
chmod 0755 "$npm_wrapper"
260+
{
261+
echo "OCM_INTERNAL_NPM_BIN=$npm_wrapper"
262+
echo "OPENCLAW_OCM_REAL_NPM_BIN=$(command -v npm)"
263+
echo "OPENCLAW_OCM_WORKSPACE_DEPENDENCY_DIRS=$workspace_dependency_dirs"
264+
} >> "$GITHUB_ENV"
265+
249266
- name: Pin Kova OpenAI model to GPT 5.5
250267
if: steps.lane.outputs.run == 'true'
251268
shell: bash

scripts/ocm-npm-workspace-deps.mjs

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
#!/usr/bin/env node
2+
3+
import { spawnSync } from "node:child_process";
4+
import { mkdtempSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
5+
import { tmpdir } from "node:os";
6+
import { delimiter, join, resolve } from "node:path";
7+
import { pathToFileURL } from "node:url";
8+
9+
const WORKSPACE_DIRS_ENV = "OPENCLAW_OCM_WORKSPACE_DEPENDENCY_DIRS";
10+
const REAL_NPM_ENV = "OPENCLAW_OCM_REAL_NPM_BIN";
11+
12+
export function parseWorkspaceDependencyDirs(
13+
raw = process.env[WORKSPACE_DIRS_ENV],
14+
cwd = process.cwd(),
15+
) {
16+
return (raw ?? "")
17+
.split(delimiter)
18+
.map((entry) => entry.trim())
19+
.filter(Boolean)
20+
.map((entry) => resolve(cwd, entry));
21+
}
22+
23+
function optionValue(args, name) {
24+
const index = args.indexOf(name);
25+
return index >= 0 ? args[index + 1] : undefined;
26+
}
27+
28+
export function resolveWorkspaceInstallPlan(args, workspaceDirs, cwd = process.cwd()) {
29+
if (args[0] !== "install" || workspaceDirs.length === 0) {
30+
return null;
31+
}
32+
const prefixDir = optionValue(args, "--prefix");
33+
const rootArchive = args.at(-1);
34+
if (!prefixDir || !rootArchive?.endsWith(".tgz")) {
35+
throw new Error("OCM workspace dependency install requires --prefix and a root .tgz archive");
36+
}
37+
return {
38+
installArgs: args.slice(0, -1),
39+
prefixDir: resolve(cwd, prefixDir),
40+
rootArchive: resolve(cwd, rootArchive),
41+
};
42+
}
43+
44+
export function buildInstallManifest(rootArchive, workspacePackages) {
45+
return {
46+
private: true,
47+
dependencies: {
48+
openclaw: pathToFileURL(rootArchive).href,
49+
...Object.fromEntries(
50+
workspacePackages.map(({ name, tarball }) => [name, pathToFileURL(tarball).href]),
51+
),
52+
},
53+
};
54+
}
55+
56+
function runNpm(npm, args, options = {}) {
57+
const result = spawnSync(npm, args, {
58+
...options,
59+
env: process.env,
60+
});
61+
if (result.error) {
62+
throw result.error;
63+
}
64+
return result;
65+
}
66+
67+
function packWorkspaceDependencies(npm, workspaceDirs, outputDir) {
68+
return workspaceDirs.map((packageDir) => {
69+
const packageJson = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8"));
70+
if (typeof packageJson.name !== "string" || packageJson.name.trim() === "") {
71+
throw new Error(`workspace dependency has no package name: ${packageDir}`);
72+
}
73+
const before = new Set(readdirSync(outputDir));
74+
const result = runNpm(npm, ["pack", packageDir, "--pack-destination", outputDir, "--silent"], {
75+
encoding: "utf8",
76+
stdio: ["ignore", "pipe", "inherit"],
77+
});
78+
if (result.status !== 0) {
79+
throw new Error(`npm pack failed for ${packageJson.name} with status ${result.status ?? 1}`);
80+
}
81+
const tarballs = readdirSync(outputDir).filter(
82+
(entry) => entry.endsWith(".tgz") && !before.has(entry),
83+
);
84+
if (tarballs.length !== 1) {
85+
throw new Error(
86+
`expected npm pack to create one archive for ${packageJson.name}, found ${tarballs.length}`,
87+
);
88+
}
89+
return {
90+
name: packageJson.name,
91+
tarball: join(outputDir, tarballs[0]),
92+
};
93+
});
94+
}
95+
96+
function main() {
97+
const args = process.argv.slice(2);
98+
const npm = process.env[REAL_NPM_ENV]?.trim() || "npm";
99+
const workspaceDirs = parseWorkspaceDependencyDirs();
100+
const plan = resolveWorkspaceInstallPlan(args, workspaceDirs);
101+
if (!plan) {
102+
const result = runNpm(npm, args, { stdio: "inherit" });
103+
return result.status ?? 1;
104+
}
105+
106+
const packDir = mkdtempSync(join(tmpdir(), "openclaw-ocm-workspace-deps-"));
107+
try {
108+
const workspacePackages = packWorkspaceDependencies(npm, workspaceDirs, packDir);
109+
mkdirSync(plan.prefixDir, { recursive: true });
110+
writeFileSync(
111+
join(plan.prefixDir, "package.json"),
112+
`${JSON.stringify(buildInstallManifest(plan.rootArchive, workspacePackages), null, 2)}\n`,
113+
);
114+
const result = runNpm(npm, plan.installArgs, { stdio: "inherit" });
115+
return result.status ?? 1;
116+
} finally {
117+
rmSync(packDir, { force: true, recursive: true });
118+
}
119+
}
120+
121+
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
122+
process.exitCode = main();
123+
}

scripts/test-projects.test-support.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1418,6 +1418,7 @@ const TOOLING_SOURCE_TEST_TARGETS = new Map([
14181418
["scripts/create-dmg.sh", ["test/scripts/create-dmg.test.ts"]],
14191419
["scripts/kova-ci-summary.mjs", ["test/scripts/kova-ci-summary.test.ts"]],
14201420
["scripts/make_appcast.sh", ["test/scripts/make-appcast.test.ts"]],
1421+
["scripts/ocm-npm-workspace-deps.mjs", ["test/scripts/ocm-npm-workspace-deps.test.ts"]],
14211422
["scripts/openclaw-npm-prepublish-verify.ts", ["test/openclaw-npm-prepublish-verify.test.ts"]],
14221423
["scripts/openclaw-npm-postpublish-verify.ts", ["test/openclaw-npm-postpublish-verify.test.ts"]],
14231424
["scripts/openclaw-npm-release-check.ts", ["test/openclaw-npm-release-check.test.ts"]],
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { delimiter, join } from "node:path";
2+
import { describe, expect, it } from "vitest";
3+
import {
4+
buildInstallManifest,
5+
parseWorkspaceDependencyDirs,
6+
resolveWorkspaceInstallPlan,
7+
} from "../../scripts/ocm-npm-workspace-deps.mjs";
8+
9+
describe("OCM npm workspace dependency adapter", () => {
10+
it("resolves workspace package directories", () => {
11+
expect(
12+
parseWorkspaceDependencyDirs(["packages/ai", "extensions/example"].join(delimiter), "/repo"),
13+
).toEqual(["/repo/packages/ai", "/repo/extensions/example"]);
14+
});
15+
16+
it("replaces the root archive argument with a prepared install manifest", () => {
17+
expect(
18+
resolveWorkspaceInstallPlan(
19+
[
20+
"install",
21+
"--prefix",
22+
"runtime",
23+
"--omit=dev",
24+
"--no-save",
25+
"--package-lock=false",
26+
"openclaw.tgz",
27+
],
28+
["/repo/packages/ai"],
29+
"/repo",
30+
),
31+
).toEqual({
32+
installArgs: [
33+
"install",
34+
"--prefix",
35+
"runtime",
36+
"--omit=dev",
37+
"--no-save",
38+
"--package-lock=false",
39+
],
40+
prefixDir: "/repo/runtime",
41+
rootArchive: "/repo/openclaw.tgz",
42+
});
43+
});
44+
45+
it("keeps normal npm commands unchanged", () => {
46+
expect(resolveWorkspaceInstallPlan(["pack", "--silent"], ["/repo/packages/ai"])).toBeNull();
47+
expect(resolveWorkspaceInstallPlan(["install", "openclaw.tgz"], [])).toBeNull();
48+
});
49+
50+
it("builds a manifest with the root and local workspace tarballs", () => {
51+
expect(
52+
buildInstallManifest("/tmp/openclaw.tgz", [
53+
{ name: "@openclaw/ai", tarball: "/tmp/openclaw-ai.tgz" },
54+
]),
55+
).toEqual({
56+
private: true,
57+
dependencies: {
58+
"@openclaw/ai": "file:///tmp/openclaw-ai.tgz",
59+
openclaw: "file:///tmp/openclaw.tgz",
60+
},
61+
});
62+
});
63+
});

test/scripts/openclaw-performance-workflow.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,21 @@ describe("OpenClaw performance workflow", () => {
104104
expect(runKova.run).not.toContain("report.summary?.statuses ?? {}");
105105
});
106106

107+
it("installs local workspace packages beside the OCM root tarball", () => {
108+
const configure = findStep("Configure OCM local workspace dependencies");
109+
110+
expect(configure.run).toContain(
111+
'npm_wrapper="$PERFORMANCE_HELPER_DIR/scripts/ocm-npm-workspace-deps.mjs"',
112+
);
113+
expect(configure.run).toContain("OCM_INTERNAL_NPM_BIN=$npm_wrapper");
114+
expect(configure.run).toContain(
115+
'if [[ -f "${GITHUB_WORKSPACE}/packages/ai/package.json" ]]; then',
116+
);
117+
expect(configure.run).toContain(
118+
"OPENCLAW_OCM_WORKSPACE_DEPENDENCY_DIRS=$workspace_dependency_dirs",
119+
);
120+
});
121+
107122
it("fails selected live Kova lanes when live auth is missing", () => {
108123
const configureAuth = findStep("Configure live OpenAI auth");
109124
const runKova = findStep("Run Kova");

0 commit comments

Comments
 (0)