build(pnpm): upgrade workspace to pnpm 11

This commit is contained in:
Altay 2026-05-08 16:18:00 +03:00 committed by Peter Steinberger
parent 3bf0d10de3
commit 3855e7b0ac
21 changed files with 320 additions and 138 deletions

6
.npmrc
View file

@ -1,4 +1,2 @@
# pnpm build-script allowlist lives in package.json -> pnpm.onlyBuiltDependencies.
# TS 7 native-preview fails to resolve packages reliably from pnpm's isolated linker.
# Keep the workspace on a hoisted layout so pnpm check/build stay stable.
node-linker=hoisted
# pnpm v11 reads project settings from pnpm-workspace.yaml.
# Keep this file for registry/auth-only npmrc entries so Docker COPY steps stay stable.

View file

@ -277,7 +277,7 @@ Package Acceptance has bounded legacy-compatibility windows for already-publishe
- known private QA entries in `dist/postinstall-inventory.json` may point at tarball-omitted files;
- `doctor-switch` may skip the `gateway install --wrapper` persistence subcase when the package does not expose that flag;
- `update-channel-switch` may prune missing `pnpm.patchedDependencies` from the tarball-derived fake git fixture and may log missing persisted `update.channel`;
- `update-channel-switch` may prune missing pnpm `patchedDependencies` from the tarball-derived fake git fixture and may log missing persisted `update.channel`;
- plugin smokes may read legacy install-record locations or accept missing marketplace install-record persistence;
- `plugin-update` may allow config metadata migration while still requiring the install record and no-reinstall behavior to stay unchanged.

View file

@ -158,7 +158,7 @@ manually.
Rebases onto the selected commit (dev only).
</Step>
<Step title="Install dependencies">
Uses the repo package manager. For pnpm checkouts, the updater bootstraps `pnpm` on demand (via `corepack` first, then a temporary `npm install pnpm@10` fallback) instead of running `npm run build` inside a pnpm workspace.
Uses the repo package manager. For pnpm checkouts, the updater bootstraps `pnpm` on demand (via `corepack` first, then a temporary `npm install pnpm@11` fallback) instead of running `npm run build` inside a pnpm workspace.
</Step>
<Step title="Build Control UI">
Builds the gateway and the Control UI.

View file

@ -18,14 +18,14 @@ Use this flow when OpenClaw needs unreleased ACPX changes before the ACPX versio
1. Make the ACPX code change in the `openclaw/acpx` repo first.
2. In OpenClaw, temporarily point `extensions/acpx/package.json` at the ACPX GitHub commit you need.
3. If pnpm blocks ACPX lifecycle/build scripts for that temporary GitHub-sourced package, temporarily add `acpx` to `onlyBuiltDependencies` in both `package.json` and `pnpm-workspace.yaml`.
3. If pnpm blocks ACPX lifecycle/build scripts for that temporary GitHub-sourced package, temporarily add `acpx: true` to `allowBuilds` in `pnpm-workspace.yaml`.
4. Refresh the root workspace lock:
- `pnpm install --lockfile-only --filter ./extensions/acpx`
5. Refresh the extension-local npm lock for install metadata:
- `cd extensions/acpx && npm install --package-lock-only --ignore-scripts`
6. Rebuild OpenClaw and restart the gateway before doing live ACP validation.
7. Once ACPX is released, switch `extensions/acpx/package.json` back to the published npm version and refresh the same lockfiles again.
8. Remove any temporary `acpx` build-script allowlist entries that were only needed for the GitHub-sourced development pin.
8. Remove any temporary `acpx` build-script allowlist entry that was only needed for the GitHub-sourced development pin.
## Lockfile Notes

View file

@ -489,7 +489,7 @@ qa_status=0
{
set -e
echo "remote pwd: $(pwd)"
sudo corepack enable || sudo npm install -g pnpm@10.33.2
sudo corepack enable || sudo npm install -g pnpm@11
if [ "$hydrate_mode" = "source" ]; then
if ! command -v make >/dev/null 2>&1 || ! command -v python3 >/dev/null 2>&1; then
sudo apt-get update -y >>"$out/apt.log" 2>&1 || true

View file

@ -24,6 +24,7 @@
"CHANGELOG.md",
"LICENSE",
"openclaw.mjs",
"pnpm-workspace.yaml",
"README.md",
"dist/",
"!dist/.buildstamp",
@ -1814,70 +1815,5 @@
"engines": {
"node": ">=22.16.0"
},
"packageManager": "pnpm@10.33.2+sha512.a90faf6feeab71ad6c6e57f94e0fe1a12f5dcc22cd754db40ae9593eb6a3e0b6b12e3540218bb37ae083404b1f2ce6db2a4121e979829b4aff94b99f49da1cf8",
"pnpm": {
"overrides": {
"@anthropic-ai/sdk": "0.95.1",
"hono": "4.12.18",
"@hono/node-server": "1.19.14",
"@aws-sdk/client-bedrock-runtime": "3.1045.0",
"axios": "1.16.0",
"fast-uri": "3.1.2",
"follow-redirects": "1.16.0",
"defu": "6.1.5",
"fast-xml-parser": "5.7.0",
"request": "npm:@cypress/request@3.0.10",
"request-promise": "npm:@cypress/request-promise@5.0.0",
"basic-ftp": "6.0.1",
"file-type": "22.0.1",
"form-data": "2.5.4",
"ip-address": "10.2.0",
"minimatch": "10.2.5",
"path-to-regexp": "8.4.0",
"qs": "6.14.2",
"node-domexception": "npm:@nolyfill/domexception@1.0.28",
"typebox": "1.1.38",
"tar": "7.5.15",
"tough-cookie": "4.1.3",
"yauzl": "3.2.1",
"protobufjs": "7.5.5",
"uuid": "14.0.0"
},
"onlyBuiltDependencies": [
"@openclaw/fs-safe",
"@google/genai",
"@lydell/node-pty",
"@matrix-org/matrix-sdk-crypto-nodejs",
"@tloncorp/api",
"@tloncorp/tlon-skill",
"baileys",
"@whiskeysockets/libsignal-node",
"authenticate-pam",
"esbuild",
"node-llama-cpp",
"protobufjs",
"sharp"
],
"ignoredBuiltDependencies": [
"@discordjs/opus",
"koffi",
"tree-sitter-bash"
],
"packageExtensions": {
"@mariozechner/pi-coding-agent": {
"dependencies": {
"strip-ansi": "^7.2.0"
}
}
},
"peerDependencyRules": {
"allowedVersions": {
"prism-media>opusscript": "^0.0.8 || ^0.1.1"
}
},
"patchedDependencies": {
"baileys@7.0.0-rc10": "patches/baileys@7.0.0-rc10.patch",
"@agentclientprotocol/claude-agent-acp@0.33.1": "patches/@agentclientprotocol__claude-agent-acp@0.33.1.patch"
}
}
"packageManager": "pnpm@11.0.8+sha512.4c4097e1dd2d42372c4e7fa5a791ff28fc75a484c7ac192e64b1df0fdef17594ba982f9b4fed9adfb3c757846f565b799b2763fb3733d1de1bcb82cf46684912"
}

4
pnpm-lock.yaml generated
View file

@ -3360,7 +3360,7 @@ packages:
os: [win32]
'@openclaw/fs-safe@https://codeload.github.com/openclaw/fs-safe/tar.gz/c7ccb99d3058f2acf2ad2758ad2470c7e113a53c':
resolution: {tarball: https://codeload.github.com/openclaw/fs-safe/tar.gz/c7ccb99d3058f2acf2ad2758ad2470c7e113a53c}
resolution: {gitHosted: true, tarball: https://codeload.github.com/openclaw/fs-safe/tar.gz/c7ccb99d3058f2acf2ad2758ad2470c7e113a53c}
version: 0.2.0
engines: {node: '>=20.11'}
@ -4848,7 +4848,7 @@ packages:
resolution: {integrity: sha512-58rWEqDGg+CKCyEeKm2KoxxSwTWtHh/NLTW9ObR4K8CGF6VwuuGudEI1CtniS/oSRmL1nJq/eh8MKARiluw4DQ==}
'@whiskeysockets/libsignal-node@https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67':
resolution: {tarball: https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67}
resolution: {gitHosted: true, tarball: https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67}
version: 2.0.1
'@zed-industries/codex-acp-darwin-arm64@0.14.0':

View file

@ -32,22 +32,63 @@ minimumReleaseAgeExclude:
- "sqlite-vec"
- "sqlite-vec-*"
onlyBuiltDependencies:
- "@openclaw/fs-safe"
- "@google/genai"
- "@lydell/node-pty"
- "@matrix-org/matrix-sdk-crypto-nodejs"
- "@napi-rs/canvas"
- "@tloncorp/api"
- "baileys"
- "@whiskeysockets/libsignal-node"
- authenticate-pam
- esbuild
- node-llama-cpp
- protobufjs
- sharp
nodeLinker: hoisted
ignoredBuiltDependencies:
- "@discordjs/opus"
- koffi
- tree-sitter-bash
overrides:
"@anthropic-ai/sdk": 0.95.1
hono: 4.12.18
"@hono/node-server": 1.19.14
"@aws-sdk/client-bedrock-runtime": 3.1045.0
axios: 1.16.0
fast-uri: 3.1.2
follow-redirects: 1.16.0
defu: 6.1.5
fast-xml-parser: 5.7.0
request: "npm:@cypress/request@3.0.10"
request-promise: "npm:@cypress/request-promise@5.0.0"
basic-ftp: 6.0.1
file-type: 22.0.1
form-data: 2.5.4
ip-address: 10.2.0
minimatch: 10.2.5
path-to-regexp: 8.4.0
qs: 6.14.2
node-domexception: "npm:@nolyfill/domexception@1.0.28"
typebox: 1.1.38
tar: 7.5.15
tough-cookie: 4.1.3
yauzl: 3.2.1
protobufjs: 7.5.5
uuid: 14.0.0
allowBuilds:
"@openclaw/fs-safe": true
"@google/genai": true
"@lydell/node-pty": true
"@matrix-org/matrix-sdk-crypto-nodejs": true
"@napi-rs/canvas": true
"@tloncorp/api": true
"@tloncorp/tlon-skill": true
baileys: true
"@whiskeysockets/libsignal-node": true
authenticate-pam: true
"@discordjs/opus": false
esbuild: true
koffi: false
node-llama-cpp: true
protobufjs: true
sharp: true
tree-sitter-bash: false
packageExtensions:
"@mariozechner/pi-coding-agent":
dependencies:
strip-ansi: ^7.2.0
peerDependencyRules:
allowedVersions:
"prism-media>opusscript": "^0.0.8 || ^0.1.1"
patchedDependencies:
"baileys@7.0.0-rc10": "patches/baileys@7.0.0-rc10.patch"
"@agentclientprotocol/claude-agent-acp@0.33.1": "patches/@agentclientprotocol__claude-agent-acp@0.33.1.patch"

View file

@ -16,6 +16,80 @@ function readJson(file) {
return JSON.parse(fs.readFileSync(file, "utf8"));
}
// Runs inside the bare Docker E2E image, before package dependencies are installed.
// Keep this to the small pnpm-workspace.yaml surface the fixture mutates.
function findTopLevelBlock(lines, key) {
const start = lines.findIndex((line) => new RegExp(`^${key}:\\s*(?:#.*)?$`).test(line));
if (start === -1) {
return null;
}
let end = start + 1;
while (end < lines.length && !/^[A-Za-z0-9_-]+:\s*/.test(lines[end])) {
end += 1;
}
return { start, end };
}
function parseYamlScalar(raw) {
const trimmed = raw.trim();
const withoutComment = trimmed.replace(/\s+#.*$/, "");
if (withoutComment.startsWith('"') && withoutComment.endsWith('"')) {
return withoutComment.slice(1, -1);
}
if (withoutComment.startsWith("'") && withoutComment.endsWith("'")) {
return withoutComment.slice(1, -1);
}
return withoutComment;
}
function readWorkspacePatchedDependencies(file) {
const lines = fs.readFileSync(file, "utf8").split("\n");
const block = findTopLevelBlock(lines, "patchedDependencies");
if (!block) {
return { patches: undefined };
}
const patches = {};
for (const line of lines.slice(block.start + 1, block.end)) {
const match = line.match(/^\s+(.+?):\s+(.+?)\s*$/);
if (!match) {
continue;
}
patches[parseYamlScalar(match[1])] = parseYamlScalar(match[2]);
}
return { patches };
}
function writeWorkspacePnpmConfig(file, keptPatches) {
const original = fs.readFileSync(file, "utf8");
const hadTrailingNewline = original.endsWith("\n");
const lines = original.replace(/\n$/, "").split("\n");
const patchBlock = findTopLevelBlock(lines, "patchedDependencies");
if (patchBlock) {
const nextLines = [];
nextLines.push(...lines.slice(0, patchBlock.start));
if (Object.keys(keptPatches).length > 0) {
nextLines.push("patchedDependencies:");
for (const [dependency, patchFile] of Object.entries(keptPatches)) {
nextLines.push(` ${JSON.stringify(dependency)}: ${JSON.stringify(patchFile)}`);
}
}
nextLines.push(...lines.slice(patchBlock.end));
lines.length = 0;
lines.push(...nextLines);
}
const allowUnusedIndex = lines.findIndex((line) => /^allowUnusedPatches:\s*/.test(line));
if (allowUnusedIndex === -1) {
lines.push("allowUnusedPatches: true");
} else {
lines[allowUnusedIndex] = "allowUnusedPatches: true";
}
fs.writeFileSync(file, `${lines.join("\n")}${hadTrailingNewline ? "\n" : ""}`);
}
function writeControlUi(root) {
const file = path.join(root, "dist", "control-ui", "index.html");
fs.mkdirSync(path.dirname(file), { recursive: true });
@ -25,31 +99,41 @@ function writeControlUi(root) {
function prepareGitFixture(root) {
const packageJsonPath = path.join(root, "package.json");
const packageJson = readJson(packageJsonPath);
packageJson.pnpm = { ...packageJson.pnpm, allowUnusedPatches: true };
const patches = packageJson.pnpm.patchedDependencies;
const pnpmWorkspacePath = path.join(root, "pnpm-workspace.yaml");
const workspaceConfig = fs.existsSync(pnpmWorkspacePath)
? readWorkspacePatchedDependencies(pnpmWorkspacePath)
: undefined;
const pnpmConfig = workspaceConfig ? {} : { ...packageJson.pnpm };
const patches = workspaceConfig?.patches ?? pnpmConfig.patchedDependencies;
const keptPatches = {};
if (patches && typeof patches === "object" && !Array.isArray(patches)) {
const kept = {};
const missing = [];
for (const [dependency, patchFile] of Object.entries(patches)) {
const exists =
typeof patchFile === "string" &&
fs.existsSync(path.resolve(path.dirname(packageJsonPath), patchFile));
if (exists) {
kept[dependency] = patchFile;
keptPatches[dependency] = patchFile;
} else {
missing.push(`${dependency} -> ${String(patchFile)}`);
}
}
if (missing.length > 0 && !legacyPackageAcceptanceCompat(packageJson.version)) {
throw new Error(
`package ${packageJson.version} has missing pnpm.patchedDependencies in package fixture: ${missing.join(", ")}`,
`package ${packageJson.version} has missing pnpm patchedDependencies in package fixture: ${missing.join(", ")}`,
);
}
if (Object.keys(kept).length > 0) {
packageJson.pnpm.patchedDependencies = kept;
}
if (workspaceConfig) {
writeWorkspacePnpmConfig(pnpmWorkspacePath, keptPatches);
} else {
pnpmConfig.allowUnusedPatches = true;
if (Object.keys(keptPatches).length > 0) {
pnpmConfig.patchedDependencies = keptPatches;
} else {
delete packageJson.pnpm.patchedDependencies;
delete pnpmConfig.patchedDependencies;
}
packageJson.pnpm = pnpmConfig;
}
const fixtureUiBuildSource = `const fs=require("node:fs");fs.mkdirSync("dist/control-ui",{recursive:true});fs.writeFileSync("dist/control-ui/index.html",${JSON.stringify(controlUiHtml)})`;
packageJson.scripts = {

View file

@ -851,7 +851,7 @@ fi
echo "bootstrap-pnpm: install"
rm -rf "$bootstrap_root"
mkdir -p "$bootstrap_root"
/opt/homebrew/bin/node /opt/homebrew/bin/npm install --prefix "$bootstrap_root" --no-save pnpm@10
/opt/homebrew/bin/node /opt/homebrew/bin/npm install --prefix "$bootstrap_root" --no-save pnpm@11
"$bootstrap_bin/pnpm" --version`);
}

View file

@ -1839,7 +1839,7 @@ ensure_pnpm() {
if command -v corepack &> /dev/null; then
ui_info "Configuring pnpm via Corepack"
corepack enable >/dev/null 2>&1 || true
if ! run_quiet_step "Activating pnpm" corepack prepare pnpm@10 --activate; then
if ! run_quiet_step "Activating pnpm" corepack prepare pnpm@11 --activate; then
ui_warn "Corepack pnpm activation failed; falling back"
fi
refresh_shell_command_cache
@ -1854,7 +1854,7 @@ ensure_pnpm() {
ui_info "Installing pnpm via npm"
fix_npm_permissions
run_quiet_step "Installing pnpm" npm install -g pnpm@10
run_quiet_step "Installing pnpm" npm install -g pnpm@11
refresh_shell_command_cache
if detect_pnpm_cmd && pnpm_cmd_is_ready; then
ui_success "pnpm ready ($(pnpm_cmd_pretty))"
@ -1873,7 +1873,7 @@ ensure_pnpm_binary_for_scripts() {
if command -v corepack >/dev/null 2>&1; then
ui_info "Ensuring pnpm command is available"
corepack enable >/dev/null 2>&1 || true
corepack prepare pnpm@10 --activate >/dev/null 2>&1 || true
corepack prepare pnpm@11 --activate >/dev/null 2>&1 || true
refresh_shell_command_cache
if command -v pnpm >/dev/null 2>&1; then
ui_success "pnpm command enabled via Corepack"
@ -1899,7 +1899,7 @@ EOF
fi
ui_error "pnpm command not available on PATH"
ui_info "Install pnpm globally (npm install -g pnpm@10) and retry"
ui_info "Install pnpm globally (npm install -g pnpm@11) and retry"
return 1
}

View file

@ -0,0 +1,48 @@
import fs from "node:fs/promises";
import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { note } from "../terminal/note.js";
import { withTempDir } from "../test-helpers/temp-dir.js";
import { noteSourceInstallIssues } from "./doctor-install.js";
vi.mock("../terminal/note.js", () => ({
note: vi.fn(),
}));
async function writeFile(root: string, relativePath: string, content = "") {
const file = path.join(root, relativePath);
await fs.mkdir(path.dirname(file), { recursive: true });
await fs.writeFile(file, content, "utf8");
}
describe("noteSourceInstallIssues", () => {
beforeEach(() => {
vi.mocked(note).mockReset();
});
it("does not treat a packaged workspace config as a source checkout", async () => {
await withTempDir({ prefix: "openclaw-doctor-install-" }, async (root) => {
await fs.mkdir(path.join(root, "node_modules"), { recursive: true });
await writeFile(root, "pnpm-workspace.yaml", "packages:\n - .\n");
noteSourceInstallIssues(root);
expect(note).not.toHaveBeenCalled();
});
});
it("warns source checkouts when node_modules was not installed by pnpm", async () => {
await withTempDir({ prefix: "openclaw-doctor-install-" }, async (root) => {
await fs.mkdir(path.join(root, "node_modules"), { recursive: true });
await writeFile(root, "pnpm-workspace.yaml", "packages:\n - .\n");
await writeFile(root, "src/entry.ts", "export {};\n");
noteSourceInstallIssues(root);
expect(note).toHaveBeenCalledWith(
expect.stringContaining("node_modules was not installed by pnpm"),
"Install",
);
});
});
});

View file

@ -7,8 +7,9 @@ export function noteSourceInstallIssues(root: string | null) {
return;
}
const srcEntry = path.join(root, "src", "entry.ts");
const workspaceMarker = path.join(root, "pnpm-workspace.yaml");
if (!fs.existsSync(workspaceMarker)) {
if (!fs.existsSync(workspaceMarker) || !fs.existsSync(srcEntry)) {
return;
}
@ -16,7 +17,6 @@ export function noteSourceInstallIssues(root: string | null) {
const nodeModules = path.join(root, "node_modules");
const pnpmStore = path.join(nodeModules, ".pnpm");
const tsxBin = path.join(nodeModules, ".bin", "tsx");
const srcEntry = path.join(root, "src", "entry.ts");
if (fs.existsSync(nodeModules) && !fs.existsSync(pnpmStore)) {
warnings.push(

View file

@ -3,10 +3,11 @@ import { join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { BUNDLED_PLUGIN_ROOT_DIR } from "openclaw/plugin-sdk/test-fixtures";
import { describe, expect, it } from "vitest";
import YAML from "yaml";
const repoRoot = resolve(fileURLToPath(new URL(".", import.meta.url)), "..");
const dockerfilePath = join(repoRoot, "Dockerfile");
const packageJsonPath = join(repoRoot, "package.json");
const pnpmWorkspacePath = join(repoRoot, "pnpm-workspace.yaml");
function collapseDockerContinuations(dockerfile: string): string {
return dockerfile.replace(/\\\r?\n[ \t]*/g, " ");
@ -140,11 +141,11 @@ describe("Dockerfile", () => {
it("keeps package manager patch files in runtime images", async () => {
const dockerfile = await readFile(dockerfilePath, "utf8");
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8")) as {
pnpm?: { patchedDependencies?: Record<string, string> };
const pnpmWorkspace = YAML.parse(await readFile(pnpmWorkspacePath, "utf8")) as {
patchedDependencies?: Record<string, string>;
};
expect(Object.keys(packageJson.pnpm?.patchedDependencies ?? {})).not.toHaveLength(0);
expect(Object.keys(pnpmWorkspace.patchedDependencies ?? {})).not.toHaveLength(0);
expect(dockerfile).toContain(
"COPY --from=runtime-assets --chown=node:node /app/patches ./patches",
);

View file

@ -13,7 +13,7 @@ describe("resolveUpdateBuildManager", () => {
const envPath = options.env?.PATH ?? options.env?.Path ?? "";
if (envPath.includes("openclaw-update-pnpm-")) {
paths.push(envPath);
return { stdout: "10.0.0", stderr: "", code: 0 };
return { stdout: "11.0.0", stderr: "", code: 0 };
}
throw new Error("spawn pnpm ENOENT");
}
@ -23,7 +23,7 @@ describe("resolveUpdateBuildManager", () => {
if (key === "npm --version") {
return { stdout: "10.0.0", stderr: "", code: 0 };
}
if (key.startsWith("npm install --prefix ") && key.endsWith(" pnpm@10")) {
if (key.startsWith("npm install --prefix ") && key.endsWith(" pnpm@11")) {
return { stdout: "added 1 package", stderr: "", code: 0 };
}
return { stdout: "", stderr: "", code: 0 };
@ -53,7 +53,7 @@ describe("resolveUpdateBuildManager", () => {
if (key === "npm --version") {
return { stdout: "10.0.0", stderr: "", code: 0 };
}
if (key.startsWith("npm install --prefix ") && key.endsWith(" pnpm@10")) {
if (key.startsWith("npm install --prefix ") && key.endsWith(" pnpm@11")) {
return { stdout: "", stderr: "network exploded", code: 1 };
}
return { stdout: "", stderr: "", code: 0 };

View file

@ -34,6 +34,8 @@ type ResolvedBuildManager =
reason: UpdatePackageManagerFailureReason;
};
const PNPM_NPM_FALLBACK_SPEC = "pnpm@11";
async function detectBuildManager(root: string): Promise<BuildManager> {
return (await detectPackageManagerImpl(root)) ?? "npm";
}
@ -124,7 +126,7 @@ async function bootstrapPnpmViaNpm(params: {
};
try {
const installResult = await params.runCommand(
["npm", "install", "--prefix", tempRoot, "pnpm@10"],
["npm", "install", "--prefix", tempRoot, PNPM_NPM_FALLBACK_SPEC],
{
timeoutMs: params.timeoutMs,
env: params.baseEnv,

View file

@ -438,7 +438,7 @@ describe("runGatewayUpdate", () => {
if (key === "pnpm --version") {
const envPath = options?.env?.PATH ?? options?.env?.Path ?? "";
if (envPath.includes("openclaw-update-pnpm-")) {
return { stdout: "10.0.0" };
return { stdout: "11.0.0" };
}
throw new Error("spawn pnpm ENOENT");
}
@ -448,7 +448,7 @@ describe("runGatewayUpdate", () => {
if (key === "npm --version") {
return { stdout: "10.0.0" };
}
if (key.startsWith("npm install --prefix ") && key.endsWith(" pnpm@10")) {
if (key.startsWith("npm install --prefix ") && key.endsWith(" pnpm@11")) {
return { stdout: "added 1 package" };
}
return undefined;
@ -550,7 +550,7 @@ describe("runGatewayUpdate", () => {
const envPath = options?.env?.PATH ?? options?.env?.Path ?? "";
if (envPath.includes("openclaw-update-pnpm-")) {
pnpmEnvPaths.push(envPath);
return { stdout: "10.0.0", stderr: "", code: 0 };
return { stdout: "11.0.0", stderr: "", code: 0 };
}
throw new Error("spawn pnpm ENOENT");
}
@ -560,7 +560,7 @@ describe("runGatewayUpdate", () => {
if (key === "npm --version") {
return { stdout: "10.0.0", stderr: "", code: 0 };
}
if (key.startsWith("npm install --prefix ") && key.endsWith(" pnpm@10")) {
if (key.startsWith("npm install --prefix ") && key.endsWith(" pnpm@11")) {
return { stdout: "added 1 package", stderr: "", code: 0 };
}
if (
@ -1412,7 +1412,7 @@ describe("runGatewayUpdate", () => {
if (key === "npm --version") {
return { stdout: "10.0.0", stderr: "", code: 0 };
}
if (key.startsWith("npm install --prefix ") && key.endsWith(" pnpm@10")) {
if (key.startsWith("npm install --prefix ") && key.endsWith(" pnpm@11")) {
return { stdout: "", stderr: "network exploded", code: 1 };
}
return { stdout: "", stderr: "", code: 0 };

View file

@ -1,6 +1,7 @@
import fs from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
import YAML from "yaml";
import {
blockedInstallDependencyPackageNames,
findBlockedPackageDirectoryInPath,
@ -15,9 +16,10 @@ type RootPackageManifest = {
optionalDependencies?: Record<string, string>;
overrides?: Record<string, string | Record<string, string>>;
peerDependencies?: Record<string, string>;
pnpm?: {
overrides?: Record<string, string>;
};
};
type PnpmWorkspaceConfig = {
overrides?: Record<string, string>;
};
function readRootManifest(): RootPackageManifest {
@ -26,6 +28,12 @@ function readRootManifest(): RootPackageManifest {
) as RootPackageManifest;
}
function readPnpmWorkspaceConfig(): PnpmWorkspaceConfig {
return YAML.parse(
fs.readFileSync(path.resolve(process.cwd(), "pnpm-workspace.yaml"), "utf8"),
) as PnpmWorkspaceConfig;
}
function readRootLockfile(): string {
return fs.readFileSync(path.resolve(process.cwd(), "pnpm-lock.yaml"), "utf8");
}
@ -84,8 +92,9 @@ describe("dependency denylist guardrails", () => {
it("pins the axios override to an exact version", () => {
const manifest = readRootManifest();
const pnpmWorkspace = readPnpmWorkspaceConfig();
expect(manifest.overrides?.axios).toMatch(/^\d+\.\d+\.\d+$/);
expect(manifest.pnpm?.overrides?.axios).toMatch(/^\d+\.\d+\.\d+$/);
expect(pnpmWorkspace.overrides?.axios).toMatch(/^\d+\.\d+\.\d+$/);
});
it("finds blocked package directories under node_modules regardless of node_modules casing", () => {

View file

@ -1,12 +1,14 @@
import fs from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
import YAML from "yaml";
type RootPackageManifest = {
dependencies?: Record<string, string>;
pnpm?: {
overrides?: Record<string, string>;
};
};
type PnpmWorkspaceConfig = {
overrides?: Record<string, string>;
};
const PI_PACKAGE_NAMES = [
@ -21,6 +23,11 @@ function readRootManifest(): RootPackageManifest {
return JSON.parse(fs.readFileSync(manifestPath, "utf8")) as RootPackageManifest;
}
function readPnpmWorkspaceConfig(): PnpmWorkspaceConfig {
const workspacePath = path.resolve(process.cwd(), "pnpm-workspace.yaml");
return YAML.parse(fs.readFileSync(workspacePath, "utf8")) as PnpmWorkspaceConfig;
}
function isExactPinnedVersion(spec: string): boolean {
return !spec.startsWith("^") && !spec.startsWith("~");
}
@ -76,8 +83,8 @@ describe("pi package graph guardrails", () => {
});
it("forbids pnpm overrides that target Pi packages", () => {
const manifest = readRootManifest();
const overrides = manifest.pnpm?.overrides ?? {};
const pnpmWorkspace = readPnpmWorkspaceConfig();
const overrides = pnpmWorkspace.overrides ?? {};
const piOverrides = Object.keys(overrides).filter(isPiOverrideKey);
expectNoGraphViolations(

View file

@ -1,4 +1,7 @@
import { readFileSync } from "node:fs";
import { execFileSync } from "node:child_process";
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
const HELPER_PATH = "scripts/lib/docker-build.sh";
@ -225,6 +228,50 @@ describe("docker build helper", () => {
expect(pluginsAssertions).toContain("expected modern installRecords in installed plugin index");
});
it("prepares pnpm workspace package fixtures without package dependencies", () => {
const root = mkdtempSync(join(tmpdir(), "openclaw-update-channel-fixture-"));
try {
mkdirSync(join(root, "patches"));
writeFileSync(
join(root, "package.json"),
`${JSON.stringify({ name: "openclaw", version: "2026.5.6", scripts: {} }, null, 2)}\n`,
"utf8",
);
writeFileSync(
join(root, "pnpm-workspace.yaml"),
[
"packages:",
" - .",
"",
"patchedDependencies:",
' "kept@1.0.0": "patches/kept.patch"',
"allowBuilds:",
" esbuild: true",
"",
].join("\n"),
"utf8",
);
writeFileSync(join(root, "patches", "kept.patch"), "", "utf8");
execFileSync(process.execPath, [
UPDATE_CHANNEL_SWITCH_ASSERTIONS_PATH,
"prepare-git-fixture",
root,
]);
const workspace = readFileSync(join(root, "pnpm-workspace.yaml"), "utf8");
const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as {
pnpm?: unknown;
};
expect(workspace).toContain(' "kept@1.0.0": "patches/kept.patch"');
expect(workspace).toContain("allowUnusedPatches: true");
expect(workspace).toContain("allowBuilds:");
expect(manifest.pnpm).toBeUndefined();
} finally {
rmSync(root, { recursive: true, force: true });
}
});
it("keeps bundled plugin install/uninstall sweep chunkable", () => {
const runner = readFileSync(BUNDLED_PLUGIN_INSTALL_UNINSTALL_E2E_PATH, "utf8");
const sweep = readFileSync(BUNDLED_PLUGIN_INSTALL_UNINSTALL_SWEEP_PATH, "utf8");

View file

@ -1,13 +1,15 @@
import fs from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
import YAML from "yaml";
type RootPackageManifest = {
dependencies?: Record<string, string>;
overrides?: Record<string, string>;
pnpm?: {
overrides?: Record<string, string>;
};
};
type PnpmWorkspaceConfig = {
overrides?: Record<string, string>;
};
function readRootManifest(): RootPackageManifest {
@ -15,13 +17,19 @@ function readRootManifest(): RootPackageManifest {
return JSON.parse(fs.readFileSync(manifestPath, "utf8")) as RootPackageManifest;
}
function readPnpmWorkspaceConfig(): PnpmWorkspaceConfig {
const workspacePath = path.resolve(process.cwd(), "pnpm-workspace.yaml");
return YAML.parse(fs.readFileSync(workspacePath, "utf8")) as PnpmWorkspaceConfig;
}
describe("root package override guardrails", () => {
it("pins the Bedrock runtime below the Windows ARM Node 24 npm resolver failure", () => {
const manifest = readRootManifest();
const pnpmWorkspace = readPnpmWorkspaceConfig();
const packageName = "@aws-sdk/client-bedrock-runtime";
const dependencyVersion = manifest.dependencies?.[packageName];
const npmOverride = manifest.overrides?.[packageName];
const pnpmOverride = manifest.pnpm?.overrides?.["@aws-sdk/client-bedrock-runtime"];
const pnpmOverride = pnpmWorkspace.overrides?.["@aws-sdk/client-bedrock-runtime"];
expect(manifest.dependencies).toHaveProperty(packageName);
expect(pnpmOverride).toBe(dependencyVersion);
@ -30,7 +38,8 @@ describe("root package override guardrails", () => {
it("pins the node-domexception alias exactly in npm and pnpm overrides", () => {
const manifest = readRootManifest();
const pnpmOverride = manifest.pnpm?.overrides?.["node-domexception"];
const pnpmWorkspace = readPnpmWorkspaceConfig();
const pnpmOverride = pnpmWorkspace.overrides?.["node-domexception"];
const npmOverride = manifest.overrides?.["node-domexception"];
expect(pnpmOverride).toBe("npm:@nolyfill/domexception@1.0.28");