mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-17 03:10:38 +09:00
Use latest repo-managed worktree scripts on reuse
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
7e34d6c66b
commit
55d756f9a3
2 changed files with 134 additions and 6 deletions
|
|
@ -483,6 +483,96 @@ describe("realizeExecutionWorkspace", () => {
|
||||||
await expect(fs.readFile(path.join(reused.cwd, ".paperclip-provision-created"), "utf8")).resolves.toBe("false\n");
|
await expect(fs.readFile(path.join(reused.cwd, ".paperclip-provision-created"), "utf8")).resolves.toBe("false\n");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses the latest repo-managed provision script when reusing an existing worktree", async () => {
|
||||||
|
const repoRoot = await createTempRepo();
|
||||||
|
await fs.mkdir(path.join(repoRoot, "scripts"), { recursive: true });
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(repoRoot, "scripts", "provision.sh"),
|
||||||
|
[
|
||||||
|
"#!/usr/bin/env bash",
|
||||||
|
"set -euo pipefail",
|
||||||
|
"printf 'v1\\n' > .paperclip-provision-version",
|
||||||
|
].join("\n"),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
await runGit(repoRoot, ["add", "scripts/provision.sh"]);
|
||||||
|
await runGit(repoRoot, ["commit", "-m", "Add initial provision script"]);
|
||||||
|
|
||||||
|
const initial = 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.sh",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
issue: {
|
||||||
|
id: "issue-1",
|
||||||
|
identifier: "PAP-449",
|
||||||
|
title: "Reuse latest provision script",
|
||||||
|
},
|
||||||
|
agent: {
|
||||||
|
id: "agent-1",
|
||||||
|
name: "Codex Coder",
|
||||||
|
companyId: "company-1",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(fs.readFile(path.join(initial.cwd, ".paperclip-provision-version"), "utf8")).resolves.toBe("v1\n");
|
||||||
|
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(repoRoot, "scripts", "provision.sh"),
|
||||||
|
[
|
||||||
|
"#!/usr/bin/env bash",
|
||||||
|
"set -euo pipefail",
|
||||||
|
"printf 'v2\\n' > .paperclip-provision-version",
|
||||||
|
].join("\n"),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
await runGit(repoRoot, ["add", "scripts/provision.sh"]);
|
||||||
|
await runGit(repoRoot, ["commit", "-m", "Update provision script"]);
|
||||||
|
|
||||||
|
await expect(fs.readFile(path.join(initial.cwd, "scripts", "provision.sh"), "utf8")).resolves.toContain("v1");
|
||||||
|
|
||||||
|
const reused = 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.sh",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
issue: {
|
||||||
|
id: "issue-1",
|
||||||
|
identifier: "PAP-449",
|
||||||
|
title: "Reuse latest provision script",
|
||||||
|
},
|
||||||
|
agent: {
|
||||||
|
id: "agent-1",
|
||||||
|
name: "Codex Coder",
|
||||||
|
companyId: "company-1",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(fs.readFile(path.join(reused.cwd, ".paperclip-provision-version"), "utf8")).resolves.toBe("v2\n");
|
||||||
|
});
|
||||||
|
|
||||||
it("writes an isolated repo-local Paperclip config and worktree branding when provisioning", async () => {
|
it("writes an isolated repo-local Paperclip config and worktree branding when provisioning", async () => {
|
||||||
const repoRoot = await createTempRepo();
|
const repoRoot = await createTempRepo();
|
||||||
const previousCwd = process.cwd();
|
const previousCwd = process.cwd();
|
||||||
|
|
|
||||||
|
|
@ -500,8 +500,35 @@ function buildWorkspaceCommandEnv(input: {
|
||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function quoteShellArg(value: string) {
|
||||||
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRepoManagedWorkspaceCommand(command: string, repoRoot: string) {
|
||||||
|
const patterns = [
|
||||||
|
/^(?<prefix>(?:bash|sh|zsh)\s+)(?<quote>["']?)(?<relative>\.\/[^"'\s]+)\k<quote>(?<suffix>(?:\s.*)?)$/s,
|
||||||
|
/^(?<quote>["']?)(?<relative>\.\/[^"'\s]+)\k<quote>(?<suffix>(?:\s.*)?)$/s,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
const match = command.match(pattern);
|
||||||
|
if (!match?.groups) continue;
|
||||||
|
|
||||||
|
const relativePath = match.groups.relative;
|
||||||
|
const repoManagedPath = path.join(repoRoot, relativePath.slice(2));
|
||||||
|
if (!existsSync(repoManagedPath)) continue;
|
||||||
|
|
||||||
|
const prefix = match.groups.prefix ?? "";
|
||||||
|
const suffix = match.groups.suffix ?? "";
|
||||||
|
return `${prefix}${quoteShellArg(repoManagedPath)}${suffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
|
||||||
async function runWorkspaceCommand(input: {
|
async function runWorkspaceCommand(input: {
|
||||||
command: string;
|
command: string;
|
||||||
|
resolvedCommand?: string;
|
||||||
cwd: string;
|
cwd: string;
|
||||||
env: NodeJS.ProcessEnv;
|
env: NodeJS.ProcessEnv;
|
||||||
label: string;
|
label: string;
|
||||||
|
|
@ -509,7 +536,7 @@ async function runWorkspaceCommand(input: {
|
||||||
const shell = resolveShell();
|
const shell = resolveShell();
|
||||||
const proc = await executeProcess({
|
const proc = await executeProcess({
|
||||||
command: shell,
|
command: shell,
|
||||||
args: ["-c", input.command],
|
args: ["-c", input.resolvedCommand ?? input.command],
|
||||||
cwd: input.cwd,
|
cwd: input.cwd,
|
||||||
env: input.env,
|
env: input.env,
|
||||||
});
|
});
|
||||||
|
|
@ -581,6 +608,7 @@ async function recordWorkspaceCommandOperation(
|
||||||
input: {
|
input: {
|
||||||
phase: "workspace_provision" | "workspace_teardown";
|
phase: "workspace_provision" | "workspace_teardown";
|
||||||
command: string;
|
command: string;
|
||||||
|
resolvedCommand?: string;
|
||||||
cwd: string;
|
cwd: string;
|
||||||
env: NodeJS.ProcessEnv;
|
env: NodeJS.ProcessEnv;
|
||||||
label: string;
|
label: string;
|
||||||
|
|
@ -605,7 +633,7 @@ async function recordWorkspaceCommandOperation(
|
||||||
const shell = resolveShell();
|
const shell = resolveShell();
|
||||||
const result = await executeProcess({
|
const result = await executeProcess({
|
||||||
command: shell,
|
command: shell,
|
||||||
args: ["-c", input.command],
|
args: ["-c", input.resolvedCommand ?? input.command],
|
||||||
cwd: input.cwd,
|
cwd: input.cwd,
|
||||||
env: input.env,
|
env: input.env,
|
||||||
});
|
});
|
||||||
|
|
@ -645,10 +673,12 @@ async function provisionExecutionWorktree(input: {
|
||||||
}) {
|
}) {
|
||||||
const provisionCommand = asString(input.strategy.provisionCommand, "").trim();
|
const provisionCommand = asString(input.strategy.provisionCommand, "").trim();
|
||||||
if (!provisionCommand) return;
|
if (!provisionCommand) return;
|
||||||
|
const resolvedProvisionCommand = resolveRepoManagedWorkspaceCommand(provisionCommand, input.repoRoot);
|
||||||
|
|
||||||
await recordWorkspaceCommandOperation(input.recorder, {
|
await recordWorkspaceCommandOperation(input.recorder, {
|
||||||
phase: "workspace_provision",
|
phase: "workspace_provision",
|
||||||
command: provisionCommand,
|
command: provisionCommand,
|
||||||
|
resolvedCommand: resolvedProvisionCommand,
|
||||||
cwd: input.worktreePath,
|
cwd: input.worktreePath,
|
||||||
env: buildWorkspaceCommandEnv({
|
env: buildWorkspaceCommandEnv({
|
||||||
base: input.base,
|
base: input.base,
|
||||||
|
|
@ -665,6 +695,7 @@ async function provisionExecutionWorktree(input: {
|
||||||
worktreePath: input.worktreePath,
|
worktreePath: input.worktreePath,
|
||||||
branchName: input.branchName,
|
branchName: input.branchName,
|
||||||
created: input.created,
|
created: input.created,
|
||||||
|
resolvedCommand: resolvedProvisionCommand === provisionCommand ? null : resolvedProvisionCommand,
|
||||||
},
|
},
|
||||||
successMessage: `Provisioned workspace at ${input.worktreePath}\n`,
|
successMessage: `Provisioned workspace at ${input.worktreePath}\n`,
|
||||||
});
|
});
|
||||||
|
|
@ -892,6 +923,12 @@ export async function cleanupExecutionWorkspaceArtifacts(input: {
|
||||||
}) {
|
}) {
|
||||||
const warnings: string[] = [];
|
const warnings: string[] = [];
|
||||||
const workspacePath = input.workspace.providerRef ?? input.workspace.cwd;
|
const workspacePath = input.workspace.providerRef ?? input.workspace.cwd;
|
||||||
|
const repoRoot = input.workspace.providerType === "git_worktree" && workspacePath
|
||||||
|
? await resolveGitRepoRootForWorkspaceCleanup(
|
||||||
|
workspacePath,
|
||||||
|
input.projectWorkspace?.cwd ?? null,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
const cleanupEnv = buildExecutionWorkspaceCleanupEnv({
|
const cleanupEnv = buildExecutionWorkspaceCleanupEnv({
|
||||||
workspace: input.workspace,
|
workspace: input.workspace,
|
||||||
projectWorkspaceCwd: input.projectWorkspace?.cwd ?? null,
|
projectWorkspaceCwd: input.projectWorkspace?.cwd ?? null,
|
||||||
|
|
@ -907,9 +944,13 @@ export async function cleanupExecutionWorkspaceArtifacts(input: {
|
||||||
|
|
||||||
for (const command of cleanupCommands) {
|
for (const command of cleanupCommands) {
|
||||||
try {
|
try {
|
||||||
|
const resolvedCommand = repoRoot
|
||||||
|
? resolveRepoManagedWorkspaceCommand(command, repoRoot)
|
||||||
|
: command;
|
||||||
await recordWorkspaceCommandOperation(input.recorder, {
|
await recordWorkspaceCommandOperation(input.recorder, {
|
||||||
phase: "workspace_teardown",
|
phase: "workspace_teardown",
|
||||||
command,
|
command,
|
||||||
|
resolvedCommand,
|
||||||
cwd: workspacePath ?? input.projectWorkspace?.cwd ?? process.cwd(),
|
cwd: workspacePath ?? input.projectWorkspace?.cwd ?? process.cwd(),
|
||||||
env: cleanupEnv,
|
env: cleanupEnv,
|
||||||
label: `Execution workspace cleanup command "${command}"`,
|
label: `Execution workspace cleanup command "${command}"`,
|
||||||
|
|
@ -918,6 +959,7 @@ export async function cleanupExecutionWorkspaceArtifacts(input: {
|
||||||
workspacePath,
|
workspacePath,
|
||||||
branchName: input.workspace.branchName,
|
branchName: input.workspace.branchName,
|
||||||
providerType: input.workspace.providerType,
|
providerType: input.workspace.providerType,
|
||||||
|
resolvedCommand: resolvedCommand === command ? null : resolvedCommand,
|
||||||
},
|
},
|
||||||
successMessage: `Completed cleanup command "${command}"\n`,
|
successMessage: `Completed cleanup command "${command}"\n`,
|
||||||
});
|
});
|
||||||
|
|
@ -927,10 +969,6 @@ export async function cleanupExecutionWorkspaceArtifacts(input: {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.workspace.providerType === "git_worktree" && workspacePath) {
|
if (input.workspace.providerType === "git_worktree" && workspacePath) {
|
||||||
const repoRoot = await resolveGitRepoRootForWorkspaceCleanup(
|
|
||||||
workspacePath,
|
|
||||||
input.projectWorkspace?.cwd ?? null,
|
|
||||||
);
|
|
||||||
const worktreeExists = await directoryExists(workspacePath);
|
const worktreeExists = await directoryExists(workspacePath);
|
||||||
if (worktreeExists) {
|
if (worktreeExists) {
|
||||||
if (!repoRoot) {
|
if (!repoRoot) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue