mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 02:26:12 +02:00
Handle slow and stale workspace startup
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
1dce216cb3
commit
ab8682d2aa
3 files changed
+178
-7
No files matched your search
@@ -404,14 +404,49 @@ if [[ -f "$worktree_cwd/package.json" && -f "$worktree_cwd/pnpm-lock.yaml" ]]; t
|
||||
done
|
||||
}
|
||||
|
||||
(
|
||||
cd "$worktree_cwd"
|
||||
pnpm install --frozen-lockfile
|
||||
) || {
|
||||
restore_moved_symlinks
|
||||
exit 1
|
||||
run_pnpm_install() {
|
||||
local stdout_path stderr_path
|
||||
stdout_path="$(mktemp)"
|
||||
stderr_path="$(mktemp)"
|
||||
|
||||
if (
|
||||
cd "$worktree_cwd"
|
||||
pnpm install "$@"
|
||||
) >"$stdout_path" 2>"$stderr_path"; then
|
||||
cat "$stdout_path"
|
||||
cat "$stderr_path" >&2
|
||||
rm -f "$stdout_path" "$stderr_path"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local exit_code=$?
|
||||
cat "$stdout_path"
|
||||
cat "$stderr_path" >&2
|
||||
if grep -q "ERR_PNPM_OUTDATED_LOCKFILE" "$stdout_path" "$stderr_path"; then
|
||||
rm -f "$stdout_path" "$stderr_path"
|
||||
return 90
|
||||
fi
|
||||
|
||||
rm -f "$stdout_path" "$stderr_path"
|
||||
return "$exit_code"
|
||||
}
|
||||
|
||||
if run_pnpm_install --frozen-lockfile; then
|
||||
:
|
||||
else
|
||||
install_exit_code=$?
|
||||
if [[ "$install_exit_code" -eq 90 ]]; then
|
||||
echo "pnpm-lock.yaml is out of date in this execution workspace; retrying install without --frozen-lockfile." >&2
|
||||
run_pnpm_install --no-frozen-lockfile || {
|
||||
restore_moved_symlinks
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
restore_moved_symlinks
|
||||
exit "$install_exit_code"
|
||||
fi
|
||||
fi
|
||||
|
||||
cleanup_moved_symlinks
|
||||
fi
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
realizeExecutionWorkspace,
|
||||
releaseRuntimeServicesForRun,
|
||||
resetRuntimeServicesForTests,
|
||||
resolveWorkspaceRuntimeReadinessTimeoutSec,
|
||||
resolveShell,
|
||||
sanitizeRuntimeServiceBaseEnv,
|
||||
startRuntimeServicesForWorkspaceControl,
|
||||
@@ -1191,6 +1192,81 @@ describe("realizeExecutionWorkspace", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("retries worktree-local pnpm install without a frozen lockfile when the lockfile is outdated", async () => {
|
||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-worktree-outdated-lockfile-"));
|
||||
const baseRoot = path.join(tempRoot, "base");
|
||||
const worktreeRoot = path.join(tempRoot, "worktree");
|
||||
const fakeBin = path.join(tempRoot, "bin");
|
||||
const fakePnpmPath = path.join(fakeBin, "pnpm");
|
||||
const scriptPath = path.join(worktreeRoot, "provision-worktree.sh");
|
||||
|
||||
try {
|
||||
await fs.mkdir(path.join(baseRoot, "node_modules"), { recursive: true });
|
||||
await fs.mkdir(worktreeRoot, { recursive: true });
|
||||
await fs.mkdir(fakeBin, { recursive: true });
|
||||
await fs.copyFile(provisionWorktreeScriptPath, scriptPath);
|
||||
await fs.chmod(scriptPath, 0o755);
|
||||
await fs.writeFile(
|
||||
path.join(worktreeRoot, "package.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "workspace-root",
|
||||
private: true,
|
||||
packageManager: "pnpm@9.15.4",
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(worktreeRoot, "pnpm-lock.yaml"),
|
||||
["lockfileVersion: '9.0'", "", "importers:", " .: {}", ""].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
fakePnpmPath,
|
||||
[
|
||||
"#!/bin/sh",
|
||||
"if [ \"$1\" = \"paperclipai\" ] && [ \"$2\" = \"--help\" ]; then",
|
||||
" exit 1",
|
||||
"fi",
|
||||
"if [ \"$1\" = \"install\" ] && [ \"$2\" = \"--frozen-lockfile\" ]; then",
|
||||
" echo \"ERR_PNPM_OUTDATED_LOCKFILE\" >&2",
|
||||
" exit 1",
|
||||
"fi",
|
||||
"if [ \"$1\" = \"install\" ] && [ \"$2\" = \"--no-frozen-lockfile\" ]; then",
|
||||
" mkdir -p \"$PWD/node_modules\"",
|
||||
" : > \"$PWD/node_modules/.retry-success\"",
|
||||
" exit 0",
|
||||
"fi",
|
||||
"exit 0",
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
await fs.chmod(fakePnpmPath, 0o755);
|
||||
|
||||
const result = await execFileAsync(scriptPath, [], {
|
||||
cwd: worktreeRoot,
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: `${fakeBin}:${process.env.PATH ?? ""}`,
|
||||
PAPERCLIP_WORKSPACE_BASE_CWD: baseRoot,
|
||||
PAPERCLIP_WORKSPACE_CWD: worktreeRoot,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.stderr).toContain("retrying install without --frozen-lockfile");
|
||||
await expect(fs.readFile(path.join(worktreeRoot, "node_modules", ".retry-success"), "utf8")).resolves.toBe("");
|
||||
await expect(fs.readFile(path.join(worktreeRoot, ".paperclip", "config.json"), "utf8")).resolves.toContain(
|
||||
"\"database\"",
|
||||
);
|
||||
} finally {
|
||||
await fs.rm(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it(
|
||||
"provisions worktree-local pnpm node_modules instead of reusing base-repo links",
|
||||
async () => {
|
||||
@@ -2501,6 +2577,51 @@ describe("buildWorkspaceRuntimeDesiredStatePatch", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveWorkspaceRuntimeReadinessTimeoutSec", () => {
|
||||
it("extends the default readiness timeout for dev-server commands", () => {
|
||||
expect(
|
||||
resolveWorkspaceRuntimeReadinessTimeoutSec({
|
||||
command: "pnpm dev",
|
||||
readiness: {
|
||||
type: "http",
|
||||
urlTemplate: "http://127.0.0.1:{{port}}",
|
||||
},
|
||||
}),
|
||||
).toBe(90);
|
||||
expect(
|
||||
resolveWorkspaceRuntimeReadinessTimeoutSec({
|
||||
command: "npm run dev -- --host 127.0.0.1",
|
||||
readiness: {
|
||||
type: "http",
|
||||
urlTemplate: "http://127.0.0.1:{{port}}",
|
||||
},
|
||||
}),
|
||||
).toBe(90);
|
||||
});
|
||||
|
||||
it("keeps explicit readiness timeouts and non-dev defaults unchanged", () => {
|
||||
expect(
|
||||
resolveWorkspaceRuntimeReadinessTimeoutSec({
|
||||
command: "pnpm dev",
|
||||
readiness: {
|
||||
type: "http",
|
||||
timeoutSec: 12,
|
||||
urlTemplate: "http://127.0.0.1:{{port}}",
|
||||
},
|
||||
}),
|
||||
).toBe(12);
|
||||
expect(
|
||||
resolveWorkspaceRuntimeReadinessTimeoutSec({
|
||||
command: "node server.js",
|
||||
readiness: {
|
||||
type: "http",
|
||||
urlTemplate: "http://127.0.0.1:{{port}}",
|
||||
},
|
||||
}),
|
||||
).toBe(30);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveShell (shell fallback)", () => {
|
||||
const originalShell = process.env.SHELL;
|
||||
const originalPlatform = process.platform;
|
||||
|
||||
@@ -1697,6 +1697,21 @@ function resolveServiceScopeId(input: {
|
||||
return { scopeType: "run" as const, scopeId: input.runId };
|
||||
}
|
||||
|
||||
function looksLikeWorkspaceDevServerCommand(command: string) {
|
||||
const normalized = command.trim().toLowerCase();
|
||||
if (!normalized) return false;
|
||||
return /(?:^|\s)(?:pnpm|npm|yarn|bun)\s+(?:run\s+)?dev(?:\s|$)/.test(normalized);
|
||||
}
|
||||
|
||||
export function resolveWorkspaceRuntimeReadinessTimeoutSec(service: Record<string, unknown>) {
|
||||
const readiness = parseObject(service.readiness);
|
||||
const explicitTimeoutSec = asNumber(readiness.timeoutSec, 0);
|
||||
if (explicitTimeoutSec > 0) {
|
||||
return Math.max(1, explicitTimeoutSec);
|
||||
}
|
||||
return looksLikeWorkspaceDevServerCommand(asString(service.command, "")) ? 90 : 30;
|
||||
}
|
||||
|
||||
async function waitForReadiness(input: {
|
||||
service: Record<string, unknown>;
|
||||
url: string | null;
|
||||
@@ -1704,7 +1719,7 @@ async function waitForReadiness(input: {
|
||||
const readiness = parseObject(input.service.readiness);
|
||||
const readinessType = asString(readiness.type, "");
|
||||
if (readinessType !== "http" || !input.url) return;
|
||||
const timeoutSec = Math.max(1, asNumber(readiness.timeoutSec, 30));
|
||||
const timeoutSec = resolveWorkspaceRuntimeReadinessTimeoutSec(input.service);
|
||||
const intervalMs = Math.max(100, asNumber(readiness.intervalMs, 500));
|
||||
const deadline = Date.now() + timeoutSec * 1000;
|
||||
let lastError = "service did not become ready";
|
||||
|
||||
Reference in new issue
Block a user