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}"
|
||||
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
|
||||
echo "Base workspace does not exist: $base_cwd" >&2
|
||||
|
|
@ -14,6 +20,37 @@ if [[ ! -d "$worktree_cwd" ]]; then
|
|||
exit 1
|
||||
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
|
||||
[[ -n "$relative_path" ]] || continue
|
||||
source_path="$base_cwd/$relative_path"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { execFile } from "node:child_process";
|
|||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
|
|
@ -13,6 +14,7 @@ import {
|
|||
stopRuntimeServicesForExecutionWorkspace,
|
||||
type RealizedExecutionWorkspace,
|
||||
} from "../services/workspace-runtime.ts";
|
||||
import { resolvePaperclipConfigPath } from "../paths.ts";
|
||||
import type { WorkspaceOperation } from "@paperclipai/shared";
|
||||
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");
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const repoRoot = await createTempRepo();
|
||||
const { recorder, operations } = createWorkspaceOperationRecorderDouble();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue