From 57ec5c0a86064a82499b9f5abf0c7eec55dfd95e Mon Sep 17 00:00:00 2001 From: Dotta Date: Fri, 31 Jul 2026 18:21:11 +0000 Subject: [PATCH] fix(install): provision corepack pnpm shim for git checkout builds Workspace build scripts (packages/db, packages/plugins/sdk) invoke bare `pnpm`. On a clean machine pnpm exists only through corepack, so the hermetic git-ref build died with 'pnpm: not found' inside `pnpm -r --filter @paperclipai/server... run build`. Enable a corepack pnpm shim into the staging dir and prepend it to PATH for every checkout build command. Found by the PAP-14994 clean-machine Docker e2e. Co-Authored-By: Claude Fable 5 --- cli/src/__tests__/install-command.test.ts | 2 +- cli/src/commands/install.ts | 21 ++++++++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/cli/src/__tests__/install-command.test.ts b/cli/src/__tests__/install-command.test.ts index 4c194cdf340..620504e348a 100644 --- a/cli/src/__tests__/install-command.test.ts +++ b/cli/src/__tests__/install-command.test.ts @@ -178,7 +178,7 @@ describe("managed install commands", () => { file === "corepack" || (file === "npm" && args[0] === "pack") || (file === process.execPath && args[0]?.endsWith("prepare-bundled-package.mjs"))); - expect(buildCalls).toHaveLength(8); + expect(buildCalls).toHaveLength(9); for (const call of buildCalls) { const env = call[2]?.env; expect(env, `${call[0]} ${call[1].join(" ")} must run with an explicit env`).toBeDefined(); diff --git a/cli/src/commands/install.ts b/cli/src/commands/install.ts index 8be743ca1bc..846e10225ab 100644 --- a/cli/src/commands/install.ts +++ b/cli/src/commands/install.ts @@ -244,12 +244,19 @@ export async function installGitPayload(repo: string, sha: string, runCommand: C const stagedPayload = path.join(stagingRoot, "payload"); fs.rmSync(stagingRoot, { recursive: true, force: true }); fs.mkdirSync(checkoutPath, { recursive: true, mode: 0o700 }); + // Workspace build scripts invoke bare `pnpm`; on a machine where pnpm exists only + // through corepack, nothing puts it on PATH, so provision a shim into the staging dir. + const pnpmShimDir = path.join(stagingRoot, "pnpm-bin"); + fs.mkdirSync(pnpmShimDir, { recursive: true, mode: 0o700 }); + const buildEnv = (extra: NodeJS.ProcessEnv = {}) => + gitBuildEnv({ PATH: [pnpmShimDir, process.env.PATH].filter(Boolean).join(path.delimiter), ...extra }); try { await runCommand("curl", ["--fail", "--silent", "--show-error", "--location", "--output", archivePath, `https://codeload.github.com/${repo}/tar.gz/${sha}`], { maxBuffer: 4 * 1024 * 1024 }); await runCommand("tar", ["-xzf", archivePath, "--strip-components=1", "-C", checkoutPath], { maxBuffer: 4 * 1024 * 1024 }); - await runCommand("corepack", ["pnpm", "install", "--frozen-lockfile"], { cwd: checkoutPath, env: gitBuildEnv(), maxBuffer: 32 * 1024 * 1024 }); - await runCommand("bash", ["scripts/build-npm.sh", "--skip-checks", "--skip-typecheck"], { cwd: checkoutPath, env: gitBuildEnv(), maxBuffer: 32 * 1024 * 1024 }); - await runCommand("corepack", ["pnpm", "-r", "--filter", "@paperclipai/server...", "--if-present", "run", "build"], { cwd: checkoutPath, env: gitBuildEnv(), maxBuffer: 32 * 1024 * 1024 }); + await runCommand("corepack", ["enable", "pnpm", "--install-directory", pnpmShimDir], { cwd: checkoutPath, env: buildEnv(), maxBuffer: 4 * 1024 * 1024 }); + await runCommand("corepack", ["pnpm", "install", "--frozen-lockfile"], { cwd: checkoutPath, env: buildEnv(), maxBuffer: 32 * 1024 * 1024 }); + await runCommand("bash", ["scripts/build-npm.sh", "--skip-checks", "--skip-typecheck"], { cwd: checkoutPath, env: buildEnv(), maxBuffer: 32 * 1024 * 1024 }); + await runCommand("corepack", ["pnpm", "-r", "--filter", "@paperclipai/server...", "--if-present", "run", "build"], { cwd: checkoutPath, env: buildEnv(), maxBuffer: 32 * 1024 * 1024 }); const metadata = JSON.parse(fs.readFileSync(path.join(checkoutPath, "cli", "package.json"), "utf8")) as { version: string }; const workspacePackages = resolveGitInstallWorkspacePackages(checkoutPath); for (const [index, workspacePackage] of workspacePackages.entries()) { @@ -258,13 +265,13 @@ export async function installGitPayload(repo: string, sha: string, runCommand: C const bundledDependencies = packageJson.bundleDependencies ?? packageJson.bundledDependencies ?? []; if (bundledDependencies.length > 0) { const stagedPackage = path.join(stagingRoot, `workspace-package-${index}`); - await runCommand(process.execPath, [path.join(checkoutPath, "scripts", "prepare-bundled-package.mjs"), packageDir, stagedPackage], { cwd: checkoutPath, env: gitBuildEnv(), maxBuffer: 32 * 1024 * 1024 }); - await runCommand("npm", ["pack", stagedPackage, "--pack-destination", stagingRoot], { cwd: checkoutPath, env: gitBuildEnv(), maxBuffer: 16 * 1024 * 1024 }); + await runCommand(process.execPath, [path.join(checkoutPath, "scripts", "prepare-bundled-package.mjs"), packageDir, stagedPackage], { cwd: checkoutPath, env: buildEnv(), maxBuffer: 32 * 1024 * 1024 }); + await runCommand("npm", ["pack", stagedPackage, "--pack-destination", stagingRoot], { cwd: checkoutPath, env: buildEnv(), maxBuffer: 16 * 1024 * 1024 }); } else { - await runCommand("corepack", ["pnpm", "--dir", workspacePackage.dir, "pack", "--pack-destination", stagingRoot], { cwd: checkoutPath, env: gitBuildEnv({ PAPERCLIP_RELEASE_REUSE_UI_DIST: "1" }), maxBuffer: 32 * 1024 * 1024 }); + await runCommand("corepack", ["pnpm", "--dir", workspacePackage.dir, "pack", "--pack-destination", stagingRoot], { cwd: checkoutPath, env: buildEnv({ PAPERCLIP_RELEASE_REUSE_UI_DIST: "1" }), maxBuffer: 32 * 1024 * 1024 }); } } - await runCommand("npm", ["pack", "--pack-destination", stagingRoot], { cwd: path.join(checkoutPath, "cli"), env: gitBuildEnv(), maxBuffer: 16 * 1024 * 1024 }); + await runCommand("npm", ["pack", "--pack-destination", stagingRoot], { cwd: path.join(checkoutPath, "cli"), env: buildEnv(), maxBuffer: 16 * 1024 * 1024 }); const tarballs = fs.readdirSync(stagingRoot).filter((entry) => entry.endsWith(".tgz")); const cliTarball = tarballs.find((entry) => entry === `paperclipai-${metadata.version}.tgz`); const workspaceTarballs = tarballs.filter((entry) => entry !== cliTarball);