mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-26 10:26:26 +01:00
fix(install): build git checkouts hermetically against NODE_ENV=production
installGitPayload's in-checkout pnpm install inherited ambient NODE_ENV=production, so pnpm omitted devDependencies (esbuild) and build-npm.sh failed with ERR_MODULE_NOT_FOUND. Strip NODE_ENV from the env of every build/pack command run inside the checkout so source installs succeed on hosts that export NODE_ENV=production globally (PAP-15205). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
8c0ee1fc8c
commit
88408b2884
2 files changed
+42
-10
No files matched your search
@@ -3,6 +3,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
type CommandRunner,
|
||||
installCommand,
|
||||
installGitPayload,
|
||||
resolveGitHubRef,
|
||||
@@ -104,9 +105,8 @@ describe("managed install commands", () => {
|
||||
expect(runCommand.mock.calls[0]?.[0]).toBe(process.execPath);
|
||||
});
|
||||
|
||||
it("installs a GitHub branch through codeload and reuses the resolved SHA", async () => {
|
||||
const sha = "c".repeat(40);
|
||||
const runCommand = vi.fn(async (file: string, args: string[]) => {
|
||||
const createGitCheckoutRunCommand = (sha: string) =>
|
||||
vi.fn(async (file: string, args: string[], _options?: Parameters<CommandRunner>[2]) => {
|
||||
if (file === "curl" && !args.includes("--output")) return { stdout: JSON.stringify({ sha }), stderr: "" };
|
||||
if (file === "curl") { fs.writeFileSync(args[args.indexOf("--output") + 1], "archive"); return { stdout: "", stderr: "" }; }
|
||||
if (file === "tar") {
|
||||
@@ -150,6 +150,10 @@ describe("managed install commands", () => {
|
||||
if (file === process.execPath) return { stdout: "0.3.1\n", stderr: "" };
|
||||
throw new Error(`Unexpected command: ${file} ${args.join(" ")}`);
|
||||
});
|
||||
|
||||
it("installs a GitHub branch through codeload and reuses the resolved SHA", async () => {
|
||||
const sha = "c".repeat(40);
|
||||
const runCommand = createGitCheckoutRunCommand(sha);
|
||||
await installCommand({ ref: "master", repo: "HenkDz/paperclip", yes: true }, { runCommand });
|
||||
await installCommand({ ref: "master", repo: "HenkDz/paperclip", yes: true }, { runCommand });
|
||||
const manifest = readInstallManifest(resolveInstallStorePaths());
|
||||
@@ -164,6 +168,26 @@ describe("managed install commands", () => {
|
||||
expect(installCall?.[1].filter((arg) => arg.endsWith(".tgz"))).toHaveLength(4);
|
||||
});
|
||||
|
||||
it("builds git checkouts with NODE_ENV cleared so ambient production mode keeps devDependencies", async () => {
|
||||
process.env.NODE_ENV = "production";
|
||||
const sha = "d".repeat(40);
|
||||
const runCommand = createGitCheckoutRunCommand(sha);
|
||||
await expect(installGitPayload("paperclipai/paperclip", sha, runCommand, resolveInstallStorePaths())).resolves.toMatchObject({ version: "0.3.1", reused: false });
|
||||
const buildCalls = runCommand.mock.calls.filter(([file, args]) =>
|
||||
file === "bash" ||
|
||||
file === "corepack" ||
|
||||
(file === "npm" && args[0] === "pack") ||
|
||||
(file === process.execPath && args[0]?.endsWith("prepare-bundled-package.mjs")));
|
||||
expect(buildCalls).toHaveLength(8);
|
||||
for (const call of buildCalls) {
|
||||
const env = call[2]?.env;
|
||||
expect(env, `${call[0]} ${call[1].join(" ")} must run with an explicit env`).toBeDefined();
|
||||
expect(env, `${call[0]} ${call[1].join(" ")} must not inherit NODE_ENV`).not.toHaveProperty("NODE_ENV");
|
||||
}
|
||||
const uiPackCall = buildCalls.find(([file, , options]) => file === "corepack" && options?.env?.PAPERCLIP_RELEASE_REUSE_UI_DIST === "1");
|
||||
expect(uiPackCall).toBeDefined();
|
||||
});
|
||||
|
||||
it("resolves the complete server workspace dependency closure in dependency order", () => {
|
||||
const checkout = path.join(root, "checkout");
|
||||
const packages = [
|
||||
|
||||
@@ -213,6 +213,14 @@ export async function installNpmPayload(
|
||||
}
|
||||
}
|
||||
|
||||
function gitBuildEnv(extra: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv {
|
||||
const env = { ...process.env, ...extra };
|
||||
// Source builds need devDependencies (esbuild, typescript); ambient NODE_ENV=production
|
||||
// makes pnpm/npm omit them, so the checkout build must not inherit it.
|
||||
delete env.NODE_ENV;
|
||||
return env;
|
||||
}
|
||||
|
||||
export async function installGitPayload(repo: string, sha: string, runCommand: CommandRunner, paths = resolveInstallStorePaths()): Promise<{ payloadPath: string; reused: boolean; version: string }> {
|
||||
const identifier = sha.slice(0, 12);
|
||||
const payloadPath = payloadPathFor(paths, "git", identifier);
|
||||
@@ -239,9 +247,9 @@ export async function installGitPayload(repo: string, sha: string, runCommand: C
|
||||
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, maxBuffer: 32 * 1024 * 1024 });
|
||||
await runCommand("bash", ["scripts/build-npm.sh", "--skip-checks", "--skip-typecheck"], { cwd: checkoutPath, maxBuffer: 32 * 1024 * 1024 });
|
||||
await runCommand("corepack", ["pnpm", "-r", "--filter", "@paperclipai/server...", "--if-present", "run", "build"], { cwd: checkoutPath, maxBuffer: 32 * 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 });
|
||||
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()) {
|
||||
@@ -250,13 +258,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, maxBuffer: 32 * 1024 * 1024 });
|
||||
await runCommand("npm", ["pack", stagedPackage, "--pack-destination", stagingRoot], { cwd: checkoutPath, maxBuffer: 16 * 1024 * 1024 });
|
||||
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 });
|
||||
} else {
|
||||
await runCommand("corepack", ["pnpm", "--dir", workspacePackage.dir, "pack", "--pack-destination", stagingRoot], { cwd: checkoutPath, env: { ...process.env, PAPERCLIP_RELEASE_REUSE_UI_DIST: "1" }, maxBuffer: 32 * 1024 * 1024 });
|
||||
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("npm", ["pack", "--pack-destination", stagingRoot], { cwd: path.join(checkoutPath, "cli"), maxBuffer: 16 * 1024 * 1024 });
|
||||
await runCommand("npm", ["pack", "--pack-destination", stagingRoot], { cwd: path.join(checkoutPath, "cli"), env: gitBuildEnv(), 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);
|
||||
|
||||
Reference in new issue
Block a user