fix(ci): rewrite workspace deps for OCM installs

This commit is contained in:
Vincent Koc 2026-07-08 14:33:28 -07:00
parent b63317d07e
commit e8c1780999
No known key found for this signature in database
2 changed files with 168 additions and 1 deletions

View file

@ -64,12 +64,28 @@ function runNpm(npm, args, options = {}) {
return result;
}
function runTar(args) {
const result = spawnSync("tar", args, {
env: process.env,
stdio: "inherit",
});
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
throw new Error(`tar failed with status ${result.status ?? 1}`);
}
}
function packWorkspaceDependencies(npm, workspaceDirs, outputDir) {
return workspaceDirs.map((packageDir) => {
const packageJson = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8"));
if (typeof packageJson.name !== "string" || packageJson.name.trim() === "") {
throw new Error(`workspace dependency has no package name: ${packageDir}`);
}
if (typeof packageJson.version !== "string" || packageJson.version.trim() === "") {
throw new Error(`workspace dependency has no package version: ${packageDir}`);
}
const before = new Set(readdirSync(outputDir));
const result = runNpm(npm, ["pack", packageDir, "--pack-destination", outputDir, "--silent"], {
encoding: "utf8",
@ -88,11 +104,58 @@ function packWorkspaceDependencies(npm, workspaceDirs, outputDir) {
}
return {
name: packageJson.name,
version: packageJson.version,
tarball: join(outputDir, tarballs[0]),
};
});
}
export function rewriteWorkspaceDependencyVersions(packageJson, workspacePackages) {
const workspaceVersions = new Map(workspacePackages.map(({ name, version }) => [name, version]));
let rewritten = 0;
for (const section of [
"dependencies",
"optionalDependencies",
"peerDependencies",
"devDependencies",
]) {
const dependencies = packageJson[section];
if (!dependencies || typeof dependencies !== "object" || Array.isArray(dependencies)) {
continue;
}
for (const [name, spec] of Object.entries(dependencies)) {
if (typeof spec !== "string" || !spec.startsWith("workspace:")) {
continue;
}
const version = workspaceVersions.get(name);
if (!version) {
throw new Error(`root archive references unconfigured workspace dependency: ${name}`);
}
dependencies[name] = version;
rewritten += 1;
}
}
return rewritten;
}
function patchRootArchiveWorkspaceDependencies(rootArchive, workspacePackages, outputDir) {
const unpackDir = join(outputDir, "root-archive");
mkdirSync(unpackDir);
runTar(["-xzf", rootArchive, "-C", unpackDir]);
const packageJsonPath = join(unpackDir, "package", "package.json");
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
const rewritten = rewriteWorkspaceDependencyVersions(packageJson, workspacePackages);
if (rewritten === 0) {
return rootArchive;
}
writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
const patchedArchive = join(outputDir, "openclaw-root-patched.tgz");
runTar(["-czf", patchedArchive, "-C", unpackDir, "package"]);
return patchedArchive;
}
function main() {
const args = process.argv.slice(2);
const npm = process.env[REAL_NPM_ENV]?.trim() || "npm";
@ -106,10 +169,15 @@ function main() {
const packDir = mkdtempSync(join(tmpdir(), "openclaw-ocm-workspace-deps-"));
try {
const workspacePackages = packWorkspaceDependencies(npm, workspaceDirs, packDir);
const rootArchive = patchRootArchiveWorkspaceDependencies(
plan.rootArchive,
workspacePackages,
packDir,
);
mkdirSync(plan.prefixDir, { recursive: true });
writeFileSync(
join(plan.prefixDir, "package.json"),
`${JSON.stringify(buildInstallManifest(plan.rootArchive, workspacePackages), null, 2)}\n`,
`${JSON.stringify(buildInstallManifest(rootArchive, workspacePackages), null, 2)}\n`,
);
const result = runNpm(npm, plan.installArgs, { stdio: "inherit" });
return result.status ?? 1;

View file

@ -1,11 +1,20 @@
import { execFileSync } from "node:child_process";
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { delimiter, join } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import {
buildInstallManifest,
parseWorkspaceDependencyDirs,
resolveWorkspaceInstallPlan,
rewriteWorkspaceDependencyVersions,
} from "../../scripts/ocm-npm-workspace-deps.mjs";
const adapterPath = fileURLToPath(
new URL("../../scripts/ocm-npm-workspace-deps.mjs", import.meta.url),
);
describe("OCM npm workspace dependency adapter", () => {
it("resolves workspace package directories", () => {
expect(
@ -60,4 +69,94 @@ describe("OCM npm workspace dependency adapter", () => {
},
});
});
it("rewrites packed workspace protocols to the local package version", () => {
const packageJson = {
dependencies: {
"@openclaw/ai": "workspace:*",
chalk: "5.6.2",
},
};
expect(
rewriteWorkspaceDependencyVersions(packageJson, [
{
name: "@openclaw/ai",
version: "2026.7.1-beta.3",
tarball: "/tmp/openclaw-ai.tgz",
},
]),
).toBe(1);
expect(packageJson.dependencies).toEqual({
"@openclaw/ai": "2026.7.1-beta.3",
chalk: "5.6.2",
});
});
it("installs a packed root with a local workspace dependency", () => {
const root = mkdtempSync(join(tmpdir(), "openclaw-ocm-adapter-test-"));
try {
const archiveRoot = join(root, "archive");
const packagedRoot = join(archiveRoot, "package");
const workspaceDir = join(root, "ai");
const installDir = join(root, "install");
const rootArchive = join(root, "openclaw.tgz");
mkdirSync(packagedRoot, { recursive: true });
mkdirSync(workspaceDir, { recursive: true });
writeFileSync(
join(packagedRoot, "package.json"),
`${JSON.stringify({
name: "openclaw",
version: "1.0.0",
dependencies: { "@openclaw/ai": "workspace:*" },
})}\n`,
);
writeFileSync(
join(workspaceDir, "package.json"),
`${JSON.stringify({
name: "@openclaw/ai",
version: "1.0.0",
main: "index.js",
})}\n`,
);
writeFileSync(join(workspaceDir, "index.js"), "export const ready = true;\n");
execFileSync("tar", ["-czf", rootArchive, "-C", archiveRoot, "package"]);
execFileSync(
process.execPath,
[
adapterPath,
"install",
"--prefix",
installDir,
"--omit=dev",
"--no-save",
"--package-lock=false",
rootArchive,
],
{
env: {
...process.env,
OPENCLAW_OCM_REAL_NPM_BIN: process.platform === "win32" ? "npm.cmd" : "npm",
OPENCLAW_OCM_WORKSPACE_DEPENDENCY_DIRS: workspaceDir,
npm_config_audit: "false",
npm_config_cache: join(root, "npm-cache"),
npm_config_fund: "false",
},
stdio: "pipe",
},
);
expect(
JSON.parse(readFileSync(join(installDir, "node_modules/openclaw/package.json"), "utf8"))
.version,
).toBe("1.0.0");
expect(
JSON.parse(readFileSync(join(installDir, "node_modules/@openclaw/ai/package.json"), "utf8"))
.version,
).toBe("1.0.0");
} finally {
rmSync(root, { force: true, recursive: true });
}
});
});