Seed Paperclip env in provisioned worktrees
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
c6364149b1
commit
fcf3ba6974
2 changed files with 168 additions and 0 deletions
|
|
@ -3,6 +3,12 @@ set -euo pipefail
|
||||||
|
|
||||||
base_cwd="${PAPERCLIP_WORKSPACE_BASE_CWD:?PAPERCLIP_WORKSPACE_BASE_CWD is required}"
|
base_cwd="${PAPERCLIP_WORKSPACE_BASE_CWD:?PAPERCLIP_WORKSPACE_BASE_CWD is required}"
|
||||||
worktree_cwd="${PAPERCLIP_WORKSPACE_CWD:?PAPERCLIP_WORKSPACE_CWD is required}"
|
worktree_cwd="${PAPERCLIP_WORKSPACE_CWD:?PAPERCLIP_WORKSPACE_CWD is required}"
|
||||||
|
paperclip_home="${PAPERCLIP_HOME:-$HOME/.paperclip}"
|
||||||
|
paperclip_instance_id="${PAPERCLIP_INSTANCE_ID:-default}"
|
||||||
|
paperclip_dir="$worktree_cwd/.paperclip"
|
||||||
|
worktree_config_path="$paperclip_dir/config.json"
|
||||||
|
worktree_env_path="$paperclip_dir/.env"
|
||||||
|
worktree_name="${PAPERCLIP_WORKSPACE_BRANCH:-$(basename "$worktree_cwd")}"
|
||||||
|
|
||||||
if [[ ! -d "$base_cwd" ]]; then
|
if [[ ! -d "$base_cwd" ]]; then
|
||||||
echo "Base workspace does not exist: $base_cwd" >&2
|
echo "Base workspace does not exist: $base_cwd" >&2
|
||||||
|
|
@ -14,6 +20,37 @@ if [[ ! -d "$worktree_cwd" ]]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
source_config_path="${PAPERCLIP_CONFIG:-}"
|
||||||
|
if [[ -z "$source_config_path" && ( -e "$base_cwd/.paperclip/config.json" || -L "$base_cwd/.paperclip/config.json" ) ]]; then
|
||||||
|
source_config_path="$base_cwd/.paperclip/config.json"
|
||||||
|
fi
|
||||||
|
if [[ -z "$source_config_path" ]]; then
|
||||||
|
source_config_path="$paperclip_home/instances/$paperclip_instance_id/config.json"
|
||||||
|
fi
|
||||||
|
source_env_path="$(dirname "$source_config_path")/.env"
|
||||||
|
|
||||||
|
mkdir -p "$paperclip_dir"
|
||||||
|
|
||||||
|
if [[ ! -e "$worktree_config_path" && ! -L "$worktree_config_path" && -e "$source_config_path" ]]; then
|
||||||
|
ln -s "$source_config_path" "$worktree_config_path"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -e "$worktree_env_path" && -e "$source_env_path" ]]; then
|
||||||
|
cp "$source_env_path" "$worktree_env_path"
|
||||||
|
chmod 600 "$worktree_env_path"
|
||||||
|
fi
|
||||||
|
|
||||||
|
tmp_env="$(mktemp "${TMPDIR:-/tmp}/paperclip-worktree-env.XXXXXX")"
|
||||||
|
if [[ -e "$worktree_env_path" ]]; then
|
||||||
|
grep -vE '^(PAPERCLIP_IN_WORKTREE|PAPERCLIP_WORKTREE_NAME)=' "$worktree_env_path" > "$tmp_env" || true
|
||||||
|
fi
|
||||||
|
{
|
||||||
|
printf 'PAPERCLIP_IN_WORKTREE=true\n'
|
||||||
|
printf 'PAPERCLIP_WORKTREE_NAME=%s\n' "$worktree_name"
|
||||||
|
} >> "$tmp_env"
|
||||||
|
mv "$tmp_env" "$worktree_env_path"
|
||||||
|
chmod 600 "$worktree_env_path"
|
||||||
|
|
||||||
while IFS= read -r relative_path; do
|
while IFS= read -r relative_path; do
|
||||||
[[ -n "$relative_path" ]] || continue
|
[[ -n "$relative_path" ]] || continue
|
||||||
source_path="$base_cwd/$relative_path"
|
source_path="$base_cwd/$relative_path"
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { execFile } from "node:child_process";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
import { promisify } from "node:util";
|
import { promisify } from "node:util";
|
||||||
import { afterEach, describe, expect, it } from "vitest";
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
|
|
@ -13,6 +14,7 @@ import {
|
||||||
stopRuntimeServicesForExecutionWorkspace,
|
stopRuntimeServicesForExecutionWorkspace,
|
||||||
type RealizedExecutionWorkspace,
|
type RealizedExecutionWorkspace,
|
||||||
} from "../services/workspace-runtime.ts";
|
} from "../services/workspace-runtime.ts";
|
||||||
|
import { resolvePaperclipConfigPath } from "../paths.ts";
|
||||||
import type { WorkspaceOperation } from "@paperclipai/shared";
|
import type { WorkspaceOperation } from "@paperclipai/shared";
|
||||||
import type { WorkspaceOperationRecorder } from "../services/workspace-operations.ts";
|
import type { WorkspaceOperationRecorder } from "../services/workspace-operations.ts";
|
||||||
|
|
||||||
|
|
@ -282,6 +284,135 @@ 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("seeds repo-local Paperclip config and worktree branding when provisioning", async () => {
|
||||||
|
const repoRoot = await createTempRepo();
|
||||||
|
const previousCwd = process.cwd();
|
||||||
|
const paperclipHome = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-worktree-home-"));
|
||||||
|
const instanceId = "worktree-base";
|
||||||
|
const sharedConfigDir = path.join(paperclipHome, "instances", instanceId);
|
||||||
|
const sharedConfigPath = path.join(sharedConfigDir, "config.json");
|
||||||
|
const sharedEnvPath = path.join(sharedConfigDir, ".env");
|
||||||
|
|
||||||
|
process.env.PAPERCLIP_HOME = paperclipHome;
|
||||||
|
process.env.PAPERCLIP_INSTANCE_ID = instanceId;
|
||||||
|
|
||||||
|
await fs.mkdir(sharedConfigDir, { recursive: true });
|
||||||
|
await fs.writeFile(
|
||||||
|
sharedConfigPath,
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
$meta: {
|
||||||
|
version: 1,
|
||||||
|
updatedAt: "2026-03-26T00:00:00.000Z",
|
||||||
|
source: "doctor",
|
||||||
|
},
|
||||||
|
database: {
|
||||||
|
mode: "embedded-postgres",
|
||||||
|
embeddedPostgresDataDir: path.join(sharedConfigDir, "db"),
|
||||||
|
embeddedPostgresPort: 54329,
|
||||||
|
backup: {
|
||||||
|
enabled: true,
|
||||||
|
intervalMinutes: 60,
|
||||||
|
retentionDays: 30,
|
||||||
|
dir: path.join(sharedConfigDir, "backups"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
logging: {
|
||||||
|
mode: "file",
|
||||||
|
logDir: path.join(sharedConfigDir, "logs"),
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
deploymentMode: "local_trusted",
|
||||||
|
exposure: "private",
|
||||||
|
host: "127.0.0.1",
|
||||||
|
port: 3100,
|
||||||
|
allowedHostnames: [],
|
||||||
|
serveUi: true,
|
||||||
|
},
|
||||||
|
auth: {
|
||||||
|
baseUrlMode: "auto",
|
||||||
|
disableSignUp: false,
|
||||||
|
},
|
||||||
|
storage: {
|
||||||
|
provider: "local_disk",
|
||||||
|
localDisk: {
|
||||||
|
baseDir: path.join(sharedConfigDir, "storage"),
|
||||||
|
},
|
||||||
|
s3: {
|
||||||
|
bucket: "paperclip",
|
||||||
|
region: "us-east-1",
|
||||||
|
prefix: "",
|
||||||
|
forcePathStyle: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
secrets: {
|
||||||
|
provider: "local_encrypted",
|
||||||
|
strictMode: false,
|
||||||
|
localEncrypted: {
|
||||||
|
keyFilePath: path.join(sharedConfigDir, "master.key"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
) + "\n",
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
await fs.writeFile(sharedEnvPath, 'DATABASE_URL="postgres://worktree:test@db.example.com:6543/paperclip"\n', "utf8");
|
||||||
|
|
||||||
|
await fs.mkdir(path.join(repoRoot, "scripts"), { recursive: true });
|
||||||
|
await fs.copyFile(
|
||||||
|
fileURLToPath(new URL("../../../scripts/provision-worktree.sh", import.meta.url)),
|
||||||
|
path.join(repoRoot, "scripts", "provision-worktree.sh"),
|
||||||
|
);
|
||||||
|
await runGit(repoRoot, ["add", "scripts/provision-worktree.sh"]);
|
||||||
|
await runGit(repoRoot, ["commit", "-m", "Add worktree provision script"]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
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-885",
|
||||||
|
title: "Show worktree banner",
|
||||||
|
},
|
||||||
|
agent: {
|
||||||
|
id: "agent-1",
|
||||||
|
name: "Codex Coder",
|
||||||
|
companyId: "company-1",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const configLinkPath = path.join(workspace.cwd, ".paperclip", "config.json");
|
||||||
|
const envPath = path.join(workspace.cwd, ".paperclip", ".env");
|
||||||
|
const envContents = await fs.readFile(envPath, "utf8");
|
||||||
|
|
||||||
|
expect(await fs.readlink(configLinkPath)).toBe(sharedConfigPath);
|
||||||
|
expect(envContents).toContain('DATABASE_URL="postgres://worktree:test@db.example.com:6543/paperclip"');
|
||||||
|
expect(envContents).toContain("PAPERCLIP_IN_WORKTREE=true");
|
||||||
|
expect(envContents).toContain("PAPERCLIP_WORKTREE_NAME=PAP-885-show-worktree-banner");
|
||||||
|
|
||||||
|
process.chdir(workspace.cwd);
|
||||||
|
expect(resolvePaperclipConfigPath()).toBe(configLinkPath);
|
||||||
|
} finally {
|
||||||
|
process.chdir(previousCwd);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("records worktree setup and provision operations when a recorder is provided", async () => {
|
it("records worktree setup and provision operations when a recorder is provided", async () => {
|
||||||
const repoRoot = await createTempRepo();
|
const repoRoot = await createTempRepo();
|
||||||
const { recorder, operations } = createWorkspaceOperationRecorderDouble();
|
const { recorder, operations } = createWorkspaceOperationRecorderDouble();
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue