From 22af797ca3faa65e4713d57d5b074d3efefa4cb7 Mon Sep 17 00:00:00 2001 From: dotta Date: Sat, 28 Mar 2026 11:40:48 -0500 Subject: [PATCH] Provision local node_modules in issue worktrees Co-Authored-By: Paperclip --- scripts/provision-worktree.sh | 38 +++++++ .../src/__tests__/workspace-runtime.test.ts | 105 ++++++++++++++++++ 2 files changed, 143 insertions(+) diff --git a/scripts/provision-worktree.sh b/scripts/provision-worktree.sh index 09cfc36b..14acc040 100644 --- a/scripts/provision-worktree.sh +++ b/scripts/provision-worktree.sh @@ -335,6 +335,44 @@ disable_seeded_routines() { disable_seeded_routines +if [[ -f "$worktree_cwd/package.json" && -f "$worktree_cwd/pnpm-lock.yaml" ]]; then + needs_install=0 + + while IFS= read -r relative_path; do + [[ -n "$relative_path" ]] || continue + target_path="$worktree_cwd/$relative_path" + + if [[ -L "$target_path" ]]; then + rm "$target_path" + needs_install=1 + continue + fi + + if [[ ! -e "$target_path" ]]; then + needs_install=1 + fi + done < <( + cd "$base_cwd" && + find . \ + -mindepth 1 \ + -maxdepth 3 \ + -type d \ + -name node_modules \ + ! -path './.git/*' \ + ! -path './.paperclip/*' \ + | sed 's#^\./##' + ) + + if [[ "$needs_install" -eq 1 ]]; then + ( + cd "$worktree_cwd" + pnpm install --frozen-lockfile + ) + fi + + exit 0 +fi + while IFS= read -r relative_path; do [[ -n "$relative_path" ]] || continue source_path="$base_cwd/$relative_path" diff --git a/server/src/__tests__/workspace-runtime.test.ts b/server/src/__tests__/workspace-runtime.test.ts index 472f58b0..a04e5302 100644 --- a/server/src/__tests__/workspace-runtime.test.ts +++ b/server/src/__tests__/workspace-runtime.test.ts @@ -50,11 +50,16 @@ if (!embeddedPostgresSupport.supported) { `Skipping embedded Postgres workspace-runtime tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`, ); } +const provisionWorktreeScriptPath = new URL("../../../scripts/provision-worktree.sh", import.meta.url); async function runGit(cwd: string, args: string[]) { await execFileAsync("git", args, { cwd }); } +async function runPnpm(cwd: string, args: string[]) { + await execFileAsync("pnpm", args, { cwd }); +} + async function createTempRepo(defaultBranch = "main") { const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-worktree-repo-")); await runGit(repoRoot, ["init"]); @@ -557,6 +562,106 @@ describe("realizeExecutionWorkspace", () => { } }, 15_000); + it("provisions worktree-local pnpm node_modules instead of reusing base-repo links", async () => { + const repoRoot = await createTempRepo(); + await fs.mkdir(path.join(repoRoot, "scripts"), { recursive: true }); + await fs.mkdir(path.join(repoRoot, "packages", "shared"), { recursive: true }); + await fs.mkdir(path.join(repoRoot, "server"), { recursive: true }); + await fs.writeFile( + path.join(repoRoot, "package.json"), + JSON.stringify( + { + name: "workspace-root", + private: true, + packageManager: "pnpm@9.15.4", + }, + null, + 2, + ), + "utf8", + ); + await fs.writeFile( + path.join(repoRoot, "pnpm-workspace.yaml"), + ["packages:", " - packages/*", " - server", ""].join("\n"), + "utf8", + ); + await fs.writeFile( + path.join(repoRoot, "packages", "shared", "package.json"), + JSON.stringify( + { + name: "@repo/shared", + version: "1.0.0", + private: true, + type: "module", + exports: "./index.js", + }, + null, + 2, + ), + "utf8", + ); + await fs.writeFile(path.join(repoRoot, "packages", "shared", "index.js"), "export const value = 'shared';\n", "utf8"); + await fs.writeFile( + path.join(repoRoot, "server", "package.json"), + JSON.stringify( + { + name: "server", + private: true, + type: "module", + dependencies: { + "@repo/shared": "workspace:*", + }, + }, + null, + 2, + ), + "utf8", + ); + await fs.writeFile(path.join(repoRoot, "server", "index.js"), "export {};\n", "utf8"); + await fs.copyFile(provisionWorktreeScriptPath, path.join(repoRoot, "scripts", "provision-worktree.sh")); + await fs.chmod(path.join(repoRoot, "scripts", "provision-worktree.sh"), 0o755); + await runPnpm(repoRoot, ["install"]); + await runGit(repoRoot, ["add", "."]); + await runGit(repoRoot, ["commit", "-m", "Add pnpm workspace fixture"]); + + const workspace = await realizeExecutionWorkspace({ + base: { + baseCwd: repoRoot, + source: "project_primary", + projectId: "project-1", + workspaceId: "workspace-1", + repoUrl: null, + repoRef: "HEAD", + }, + config: { + workspaceStrategy: { + type: "git_worktree", + branchTemplate: "{{issue.identifier}}-{{slug}}", + provisionCommand: "bash ./scripts/provision-worktree.sh", + }, + }, + issue: { + id: "issue-1", + identifier: "PAP-551", + title: "Provision local workspace dependencies", + }, + agent: { + id: "agent-1", + name: "Codex Coder", + companyId: "company-1", + }, + }); + + expect((await fs.lstat(path.join(workspace.cwd, "node_modules"))).isSymbolicLink()).toBe(false); + expect((await fs.lstat(path.join(workspace.cwd, "server", "node_modules"))).isSymbolicLink()).toBe(false); + await expect(fs.realpath(path.join(workspace.cwd, "server", "node_modules", "@repo", "shared"))).resolves.toBe( + await fs.realpath(path.join(workspace.cwd, "packages", "shared")), + ); + await expect(fs.realpath(path.join(repoRoot, "server", "node_modules", "@repo", "shared"))).resolves.toBe( + await fs.realpath(path.join(repoRoot, "packages", "shared")), + ); + }); + it("records worktree setup and provision operations when a recorder is provided", async () => { const repoRoot = await createTempRepo(); const { recorder, operations } = createWorkspaceOperationRecorderDouble();