Report safe diagnostics when a saved Git workspace cannot reopen

A saved workspace reopen could report only a generic conflict when its upstream was offline or its saved commit was no longer available. Carry a fixed restore stage and failure category through the existing rejection and server log. Keep the comment unsaved and retain the original restoration message.

Classify bounded error text in memory. Preserve inner restore steps, distinguish network and filesystem errors, and keep raw Git output, paths and repository credentials out of the response.

On supported Node 25.6.1, the two real Git/Postgres regressions fail before the fix and pass after it. Server typechecking and 174 selected tests pass, including both full reopen route/service suites. The conservative local verify selected 2284 files and was interrupted; broad applicable CI remains required before merge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
authored and Devin Foley committed 2026-11-06 20:48:56 -08:00
1 parent efc0b359da
commit 9ac5e10717
7 files changed
+180 -23

No files matched your search

+6
View File
@@ -4533,6 +4533,12 @@ retryable restoration failure. It never substitutes the default branch.
Reopening failures reject the consuming request with HTTP 409 before saving a
comment or changing its task. The response preserves the restoration message, so
the composer keeps an editable draft and permits retry after access is restored.
It also carries `diagnostic: { step, kind }`, and the server log records the same
pair. Both are closed values: `step` names the failed stage (for example
`source_materialize`, `restore_point` or `saved_commit`), and `kind` names the
failure class (`remote_unavailable`, `auth_rejected`, `commit_unavailable`,
`timeout`, `verification`, `filesystem`, `git_command` or `unknown`). They never
contain paths, repository URLs or Git output.
Both enable switches default off. Rollback disables further expiration while
leaving saved recipes and restoration support intact. Recovery archives, runner
@@ -986,13 +986,32 @@ describeEmbeddedPostgres("reconstructable Git expiration", () => {
const f = await fixture();
expect((await f.expire()).baseExpired).toBe(true);
await fs.rename(f.remote, `${f.remote}.offline`);
expect(await f.reopen()).toMatchObject({ ok: false, code: "rebuild_failed", message: "The saved workspace commit could not be restored. Check repository access and retry." });
const failed = await f.reopen();
expect(failed).toMatchObject({ ok: false, code: "rebuild_failed", message: "The saved workspace commit could not be restored. Check repository access and retry.",
diagnostic: { step: "source_materialize", kind: "remote_unavailable" } });
expect(JSON.stringify(failed)).not.toContain(root);
expect(await f.reopen(randomUUID())).toMatchObject({ ok: false, code: "not_reopenable" });
expect(await exists(f.cwd)).toBe(false);
await fs.rename(`${f.remote}.offline`, f.remote);
expect(await f.reopen()).toMatchObject({ ok: true });
expect(git(f.cwd, "rev-parse", "HEAD")).toBe(f.commitA);
});
it("reports the failed step and kind when the saved commit is gone from upstream", async () => {
const f = await fixture();
expect((await f.expire()).baseExpired).toBe(true);
git(f.source, "checkout", "--orphan", "rewritten");
await fs.writeFile(path.join(f.source, "marker.txt"), "rewritten\n");
git(f.source, "add", "."); git(f.source, "commit", "-m", "Rewritten");
git(f.source, "push", "--force", "origin", "rewritten:main");
git(f.remote, "reflog", "expire", "--expire=now", "--all"); git(f.remote, "gc", "--prune=now");
const failed = await f.reopen();
expect(failed).toMatchObject({ ok: false, code: "rebuild_failed", message: "The saved workspace commit could not be restored. Check repository access and retry.",
diagnostic: { step: "saved_commit", kind: "commit_unavailable" } });
expect(JSON.stringify(failed)).not.toContain(root);
expect(JSON.stringify(failed)).not.toContain(f.commitA);
expect((await f.row()).metadata?.gitCacheRestoration).toMatchObject({ commit: f.commitA });
});
it("serializes concurrent reopening after deletion and publishes one transition", async () => {
const f = await fixture();
let release!: () => void, entered!: () => void;
@@ -0,0 +1,55 @@
import { describe, expect, it } from "vitest";
import { classifyWorkspaceRestorationError, GitCacheRestorationFailure } from "../services/git-workspace-restoration.js";
import { workspaceReopenDiagnostic } from "../services/execution-workspaces.js";
import { GitConnectionFailureError } from "../services/git-connection-failure.js";
const gitError = (stderr: string, extra: Record<string, unknown> = {}) =>
Object.assign(new Error(`Command failed: git fetch\n${stderr}`), { code: 128, stderr, ...extra });
describe("workspace restoration diagnostic", () => {
it.each([
["fatal: Authentication failed for 'https://user:sk-secret@example.test/repo.git/'", "auth_rejected"],
["fatal: could not read Username for 'https://github.com': terminal prompts disabled", "auth_rejected"],
["fatal: remote error: upload-pack: not our ref 9a204a56", "commit_unavailable"],
["fatal: couldn't find remote ref refs/heads/gone", "commit_unavailable"],
["fatal: unable to access 'https://example.test/repo.git/': Could not resolve host: example.test", "remote_unavailable"],
["fatal: '/srv/repo.git' does not appear to be a git repository", "remote_unavailable"],
["fatal: bad revision", "git_command"],
])("classifies %s", (stderr, kind) => {
expect(classifyWorkspaceRestorationError(gitError(stderr))).toBe(kind);
});
it("classifies timeouts, typed connection failures, filesystem errors and checks", () => {
expect(classifyWorkspaceRestorationError(gitError("", { killed: true, signal: "SIGTERM" }))).toBe("timeout");
expect(classifyWorkspaceRestorationError(new GitConnectionFailureError("clone failed",
{ schemaVersion: 1, provider: "git", operation: "clone", reason: "authentication_failed" }))).toBe("auth_rejected");
expect(classifyWorkspaceRestorationError(new GitConnectionFailureError("clone failed",
{ schemaVersion: 1, provider: "git", operation: "clone", reason: "dns_failure" }))).toBe("remote_unavailable");
expect(classifyWorkspaceRestorationError(Object.assign(new Error("EACCES: permission denied, open '/x'"), { code: "EACCES" })))
.toBe("filesystem");
expect(classifyWorkspaceRestorationError(Object.assign(new Error("ENOSPC: no space left"), { code: "ENOSPC" }))).toBe("filesystem");
expect(classifyWorkspaceRestorationError(new GitCacheRestorationFailure())).toBe("verification");
expect(classifyWorkspaceRestorationError("not an error")).toBe("unknown");
});
it("keeps the innermost restoration step and never carries Git output", () => {
const inner = new GitCacheRestorationFailure({ step: "saved_commit", kind: "commit_unavailable" });
expect(workspaceReopenDiagnostic("source_verify", inner)).toEqual({ step: "saved_commit", kind: "commit_unavailable" });
const diagnostic = workspaceReopenDiagnostic("managed_base", gitError("fatal: Authentication failed for 'https://user:sk-secret@example.test/r.git/'"));
expect(diagnostic).toEqual({ step: "managed_base", kind: "auth_rejected" });
expect(JSON.stringify(diagnostic)).not.toContain("sk-secret");
expect(workspaceReopenDiagnostic("publish", undefined)).toEqual({ step: "publish", kind: "unknown" });
expect(inner.message).toBe("The saved workspace commit could not be restored. Check repository access and retry.");
});
it("bounds command text and distinguishes network failures from filesystem failures", () => {
const text = "x".repeat(16_384) + "Authentication failed for a private repository";
expect(classifyWorkspaceRestorationError(gitError(text))).toBe("git_command");
expect(classifyWorkspaceRestorationError(gitError("", { stderr: Buffer.from(text) }))).toBe("git_command");
expect(classifyWorkspaceRestorationError(Object.assign(new Error("network failure"), { code: "ECONNRESET" }))).toBe("remote_unavailable");
expect(classifyWorkspaceRestorationError(Object.assign(new Error("resolver failure"), { code: "EAI_AGAIN" }))).toBe("remote_unavailable");
expect(classifyWorkspaceRestorationError(Object.assign(new Error("timeout"), { name: "TimeoutError" }))).toBe("timeout");
expect(classifyWorkspaceRestorationError(gitError("", { code: "ERR_CHILD_PROCESS_STDIO_MAXBUFFER", killed: true }))).toBe("git_command");
expect(classifyWorkspaceRestorationError(Object.assign(new Error("unknown"), { code: "EUNRECOGNIZED" }))).toBe("unknown");
});
});
@@ -343,6 +343,7 @@ describe("closed isolated workspace issue routes", () => {
ok: false,
code: "rebuild_failed",
message,
diagnostic: { step: "saved_commit", kind: "commit_unavailable" },
});
const app = request(createApp());
@@ -351,7 +352,7 @@ describe("closed isolated workspace issue routes", () => {
: await app.patch(`/api/issues/${issueId}`).send({ comment: "continue" });
expect(res.status).toBe(409);
expect(res.body.error).toBe(message);
expect(res.body).toEqual({ error: message, diagnostic: { step: "saved_commit", kind: "commit_unavailable" } });
expect(mockIssueService.addComment).not.toHaveBeenCalled();
expect(mockIssueService.update).not.toHaveBeenCalled();
});
+1 -1
View File
@@ -8014,7 +8014,7 @@ export function issueRoutes(
"This issue is linked to a closed workspace that cannot be reopened.",
});
} else {
res.status(409).json({ error: result.message });
res.status(409).json({ error: result.message, ...(result.diagnostic ? { diagnostic: result.diagnostic } : {}) });
}
return null;
}
+37 -15
View File
@@ -20,7 +20,8 @@ import { hasPendingNestedCloneMigration, isNestedCloneMigrationCandidate, nextNe
NESTED_CLONE_MIGRATION_RETRY_REASONS, type NestedCloneMigrationRotation } from "./nested-clone-migration.js";
import type { StorageService } from "../storage/types.js";
import { getStorageService } from "../storage/index.js";
import { readGitCacheRestoration, restoreGitCacheBase, recoverGitCacheExpirationLocks, GitCacheRestorationFailure, gitRestorePointExpired } from "./git-workspace-restoration.js";
import { readGitCacheRestoration, restoreGitCacheBase, recoverGitCacheExpirationLocks, GitCacheRestorationFailure, gitRestorePointExpired,
classifyWorkspaceRestorationError, type GitCacheRestorationStep, type WorkspaceRestorationFailureKind } from "./git-workspace-restoration.js";
export const GIT_RESTORE_POINT_EXPIRED_WARNING = "The saved state of this workspace expired, so it was reopened as a fresh workspace on its branch. Unpushed and uncommitted work from before it was cleaned up is no longer available.";
import { parseObject } from "../adapters/utils.js";
@@ -327,7 +328,16 @@ export type ReopenClosedIsolatedExecutionWorkspaceResult =
// generation, so a stale actor never clears a newer reopen's fence.
| { ok: true; workspace: ExecutionWorkspace; reopened: true; generation: number; warning?: string }
| { ok: true; workspace: ExecutionWorkspace; reopened: false; generation: number }
| { ok: false; code: "not_reopenable" | "rebuild_failed"; message: string };
| { ok: false; code: "not_reopenable" | "rebuild_failed"; message: string; diagnostic?: WorkspaceReopenDiagnostic };
export type WorkspaceReopenFailureStep = GitCacheRestorationStep | "managed_base" | "copy_identity" | "expiration_locks"
| "workspace_rebuild" | "publish" | "transaction";
export type WorkspaceReopenDiagnostic = { step: WorkspaceReopenFailureStep; kind: WorkspaceRestorationFailureKind };
export function workspaceReopenDiagnostic(step: WorkspaceReopenFailureStep, error: unknown): WorkspaceReopenDiagnostic {
if (error instanceof GitCacheRestorationFailure && error.step) return { step: error.step, kind: classifyWorkspaceRestorationError(error) };
return { step, kind: error === undefined ? "unknown" : classifyWorkspaceRestorationError(error) };
}
export type ExecutionWorkspaceServiceOptions = {
pluginWorkerManager?: WorkspaceWorkers;
@@ -4345,9 +4355,16 @@ export function executionWorkspaceService(db: Db, opts: ExecutionWorkspaceServic
});
}
await assertTaskWorkspaceExecutionAccess(db, input.authorizationActor, issue.companyId, { issueId: issue.id, workspaceId: snapshot.id });
let recipe: ReturnType<typeof readGitCacheRestoration>;
let recipe: ReturnType<typeof readGitCacheRestoration> = null;
const rebuildFailed = (step: WorkspaceReopenFailureStep, error: unknown, restoration = recipe !== null): ReopenClosedIsolatedExecutionWorkspaceResult => {
const diagnostic = workspaceReopenDiagnostic(step, error);
logger.warn({ event: "execution_workspace.reopen", outcome: "rebuild_failed", executionWorkspaceId: snapshot.id,
issueId: issue.id, companyId: issue.companyId, ...diagnostic }, "execution workspace reopen rebuild failed");
return { ok: false, code: "rebuild_failed", diagnostic,
message: restoration ? new GitCacheRestorationFailure().message : "Failed to rebuild the execution workspace" };
};
try { recipe = readGitCacheRestoration(snapshot); }
catch { return { ok: false, code: "rebuild_failed", message: new GitCacheRestorationFailure().message }; }
catch (error) { return rebuildFailed("recipe", error, true); }
const freshStart = recipe !== null && isClosedExecutionWorkspaceStatus(snapshot.status) && gitRestorePointExpired(recipe);
const gitAuth = createIssueGitRemoteAuthProvider(db, snapshot.companyId, issue.id);
const [
@@ -4383,9 +4400,8 @@ export function executionWorkspaceService(db: Db, opts: ExecutionWorkspaceServic
resolvedBaseCwd = recipe && isClosedExecutionWorkspaceStatus(snapshot.status)
? await restoreGitCacheBase(snapshot, recipe, gitAuth, undefined, { freshStart })
: await verifiedRecordedIsolatedRemoteSource(snapshot) ?? projectWorkspace?.cwd ?? null;
} catch {
return { ok: false, code: "rebuild_failed", message: recipe
? new GitCacheRestorationFailure().message : "Failed to rebuild the execution workspace" };
} catch (error) {
return rebuildFailed("source_verify", error);
}
if (isClosedExecutionWorkspaceStatus(snapshot.status) && resolvedBaseCwd == null && snapshot.strategyType === "git_worktree" && snapshot.projectId) {
try {
@@ -4409,8 +4425,8 @@ export function executionWorkspaceService(db: Db, opts: ExecutionWorkspaceServic
resolveGitAuth: createIssueGitRemoteAuthProvider(db, snapshot.companyId, issue.id),
});
resolvedBaseCwd = managedWorkspace.cwd;
} catch {
return { ok: false, code: "rebuild_failed", message: recipe ? new GitCacheRestorationFailure().message : "Failed to rebuild the execution workspace" };
} catch (error) {
return rebuildFailed("managed_base", error);
}
}
let copyIdentity: { sourceCwd: string; configuredSourceCwd: string; targetCwd: string } | null = null;
@@ -4427,8 +4443,8 @@ export function executionWorkspaceService(db: Db, opts: ExecutionWorkspaceServic
sourceCwd: await fs.realpath(sourceRoot),
configuredSourceCwd: await fs.realpath(resolvedBaseCwd), targetCwd,
};
} catch {
return { ok: false, code: "rebuild_failed", message: recipe ? new GitCacheRestorationFailure().message : "Failed to rebuild the execution workspace" };
} catch (error) {
return rebuildFailed("copy_identity", error);
}
}
const result = await db.transaction(async (tx): Promise<ReopenClosedIsolatedExecutionWorkspaceResult> => {
@@ -4478,7 +4494,7 @@ export function executionWorkspaceService(db: Db, opts: ExecutionWorkspaceServic
}
}
try { await recoverGitCacheExpirationLocks(row); }
catch { return { ok: false, code: "rebuild_failed", message: new GitCacheRestorationFailure().message }; }
catch (error) { return rebuildFailed("expiration_locks", error, true); }
const verifyCopyIdentity = copyIdentity ? async (sourceCwd: string, targetCwd: string) => {
if (await fs.realpath(sourceCwd) !== copyIdentity.sourceCwd
|| await fs.realpath(resolvedBaseCwd!) !== copyIdentity.configuredSourceCwd
@@ -4525,6 +4541,7 @@ export function executionWorkspaceService(db: Db, opts: ExecutionWorkspaceServic
});
let rebuildError: string | null = null;
let rebuildDiagnostic: WorkspaceReopenDiagnostic | null = null;
let taskDirectoryWarning: string | null = null;
const taskDirectoryState = readTaskDirectoryRetention(row);
try {
@@ -4644,9 +4661,11 @@ export function executionWorkspaceService(db: Db, opts: ExecutionWorkspaceServic
});
if (!realized) {
rebuildError = "Execution workspace could not be rebuilt";
rebuildDiagnostic = workspaceReopenDiagnostic("workspace_rebuild", undefined);
}
} catch (error) {
rebuildError = error instanceof Error ? error.message : String(error);
rebuildDiagnostic = workspaceReopenDiagnostic("workspace_rebuild", error);
}
if (rebuildError) {
@@ -4677,6 +4696,7 @@ export function executionWorkspaceService(db: Db, opts: ExecutionWorkspaceServic
actorType: actor.actorType,
actorAgentId: actor.agentId ?? null,
generation: nextGeneration,
...rebuildDiagnostic,
error: rebuildError,
},
"execution workspace reopen rebuild failed",
@@ -4693,9 +4713,11 @@ export function executionWorkspaceService(db: Db, opts: ExecutionWorkspaceServic
issueId: issue.id,
outcome: "rebuild_failed",
generation: nextGeneration,
...rebuildDiagnostic,
},
});
return { ok: false, code: "rebuild_failed", message: recipe ? new GitCacheRestorationFailure().message : "Failed to rebuild the execution workspace" };
return { ok: false, code: "rebuild_failed", ...(rebuildDiagnostic ? { diagnostic: rebuildDiagnostic } : {}),
message: recipe ? new GitCacheRestorationFailure().message : "Failed to rebuild the execution workspace" };
}
// The rebuild succeeded. Publish the row as active in one write, and clear
@@ -4730,7 +4752,7 @@ export function executionWorkspaceService(db: Db, opts: ExecutionWorkspaceServic
.returning()
.then((rows) => rows[0] ?? null);
if (!activeRow) {
return { ok: false, code: "rebuild_failed", message: recipe ? new GitCacheRestorationFailure().message : "Failed to rebuild the execution workspace" };
return rebuildFailed("publish", undefined);
}
logger.info(
{
@@ -4770,7 +4792,7 @@ export function executionWorkspaceService(db: Db, opts: ExecutionWorkspaceServic
...(reopenWarning ? { warning: reopenWarning } : {}) };
}).catch((error): ReopenClosedIsolatedExecutionWorkspaceResult => {
logger.warn({ error, executionWorkspaceId: snapshot.id }, "Execution workspace reopen failed");
return { ok: false, code: "rebuild_failed", message: recipe ? new GitCacheRestorationFailure().message : "Failed to rebuild the execution workspace" };
return rebuildFailed("transaction", error);
});
return result;
};
@@ -8,7 +8,8 @@ import path from "node:path";
import { execFile as execFileCallback } from "node:child_process";
import { createHash } from "node:crypto";
import { resolveManagedProjectWorkspaceDir, resolvePaperclipInstanceRoot } from "../home-paths.js";
import { materializeManagedProjectWorkspace } from "./managed-repository-checkout.js";
import { materializeManagedProjectWorkspace, RepositoryRefNotFoundError } from "./managed-repository-checkout.js";
import { readGitConnectionFailure } from "./git-connection-failure.js";
import { promisify } from "node:util";
import type { GitRemoteAuthProvider } from "./git-credentials.js";
import { isRuntimeOwnedGitBranch } from "./execution-workspace-branch-ownership.js";
@@ -85,8 +86,54 @@ export async function verifiedManagedGitObjectSource(workspace: Workspace, sourc
return sourcePath;
}
export type GitCacheRestorationStep = "recipe" | "trash_recovery" | "source_check" | "source_materialize" | "source_verify"
| "restore_point" | "saved_commit";
export type WorkspaceRestorationFailureKind = "remote_unavailable" | "auth_rejected" | "commit_unavailable" | "timeout"
| "verification" | "filesystem" | "git_command" | "unknown";
export class GitCacheRestorationFailure extends Error {
constructor() { super("The saved workspace commit could not be restored. Check repository access and retry."); }
readonly step: GitCacheRestorationStep | null;
readonly kind: WorkspaceRestorationFailureKind | null;
constructor(diagnostic: { step: GitCacheRestorationStep; kind: WorkspaceRestorationFailureKind } | null = null) {
super("The saved workspace commit could not be restored. Check repository access and retry.");
this.step = diagnostic?.step ?? null;
this.kind = diagnostic?.kind ?? null;
}
}
export function classifyWorkspaceRestorationError(error: unknown): WorkspaceRestorationFailureKind {
if (error instanceof GitCacheRestorationFailure) return error.kind ?? "verification";
if (!(error instanceof Error)) return "unknown";
const connection = readGitConnectionFailure(error);
if (connection) {
if (connection.reason === "authentication_failed") return "auth_rejected";
return connection.reason === "connection_timeout" ? "timeout" : "remote_unavailable";
}
if (error instanceof RepositoryRefNotFoundError) return "commit_unavailable";
const details = error as Error & { code?: unknown; killed?: unknown; stderr?: unknown };
const code = typeof details.code === "string" ? details.code : null;
if (details.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") return "git_command";
if (details.killed === true || details.code === "ETIMEDOUT" || error.name === "AbortError" || error.name === "TimeoutError") return "timeout";
if (["ENOTFOUND", "EAI_AGAIN", "ECONNREFUSED", "ECONNRESET", "EHOSTUNREACH", "ENETUNREACH"].includes(code ?? "")) return "remote_unavailable";
if (["ENOENT", "ENOTDIR", "EISDIR", "EACCES", "EPERM", "ENOSPC", "EROFS", "EMFILE", "ENFILE", "ELOOP", "ENAMETOOLONG", "EBUSY", "EIO", "EDQUOT"].includes(code ?? "")) return "filesystem";
const stderr = typeof details.stderr === "string" ? details.stderr.slice(0, 16_384)
: Buffer.isBuffer(details.stderr) ? details.stderr.subarray(0, 16_384).toString("utf8") : error.message.slice(0, 16_384);
if (/authentication failed|could not read (username|password)|terminal prompts disabled|permission denied|returned error: 40[13]/i.test(stderr)) {
return "auth_rejected";
}
if (/not our ref|couldn't find remote ref|no such remote ref|unadvertised object|did not send all necessary objects/i.test(stderr)) {
return "commit_unavailable";
}
if (/could not resolve host|unable to access|could not read from remote repository|does not appear to be a git repository|repository .*not found|connection (refused|timed out|reset)/i.test(stderr)) {
return "remote_unavailable";
}
if (typeof details.stderr === "string" || typeof details.code === "number") return "git_command";
return "unknown";
}
function restorationFailureAt(step: GitCacheRestorationStep, error: unknown) {
if (error instanceof GitCacheRestorationFailure && error.step) return error;
return new GitCacheRestorationFailure({ step, kind: classifyWorkspaceRestorationError(error) });
}
export function readGitCacheRestoration(workspace: Workspace): GitCacheRestoration | null {
@@ -719,9 +766,12 @@ export function gitRestorePointExpired(recipe: GitCacheRestoration) {
export async function restoreGitCacheBase(workspace: Workspace, recipe: GitCacheRestoration, resolveGitAuth: GitRemoteAuthProvider,
storage?: StorageService, options: { freshStart?: boolean } = {}) {
if (readGitCacheRestoration(workspace)?.commit !== recipe.commit) throw new GitCacheRestorationFailure();
let step: GitCacheRestorationStep = "recipe";
try {
if (readGitCacheRestoration(workspace)?.commit !== recipe.commit) throw new GitCacheRestorationFailure();
step = "trash_recovery";
await finishGitCacheTrash(workspace);
step = "source_check";
const present = await fs.lstat(recipe.sourcePath).then(() => true, (error: NodeJS.ErrnoException) => {
if (error.code === "ENOENT") return false;
throw error;
@@ -736,21 +786,25 @@ export async function restoreGitCacheBase(workspace: Workspace, recipe: GitCache
if (!entry.isDirectory() || entry.isSymbolicLink() || await fs.realpath(parent) !== parent) throw new GitCacheRestorationFailure();
}
}
step = present ? "source_verify" : "source_materialize";
const cwd = present ? await verifiedGitCacheSource(workspace, recipe.sourcePath)
: recipe.sourceKind === "isolated_remote"
? await materializeIsolatedRemoteWorkspaceSource({ companyId: recipe.companyId, issueId: recipe.sourceIssueId,
repoUrl: recipe.repoUrl, resolveGitAuth })
: (await materializeManagedProjectWorkspace(recipe.sourcePath, { repoUrl: recipe.repoUrl, resolveGitAuth })).cwd;
step = "source_verify";
await verifiedGitCacheSource(workspace, recipe.sourcePath);
if (!cwd) throw new GitCacheRestorationFailure();
if (options.freshStart) return cwd;
step = "restore_point";
if (recipe.restorePoint) {
await ensureGitRestorePointCommit({ storage: storage ?? getStorageService(), workspace: { companyId: workspace.companyId, id: workspace.id },
ref: recipe.restorePoint, repositoryCwd: cwd });
}
step = "saved_commit";
await ensureSavedGitCommit(cwd, recipe.repoUrl, recipe.commit, resolveGitAuth);
return cwd;
} catch { throw new GitCacheRestorationFailure(); }
} catch (error) { throw restorationFailureAt(step, error); }
}
export async function ensureSavedGitCommit(cwd: string, repoUrl: string, commit: string, resolveGitAuth: (url: string) => Promise<{ configArgs: string[]; env: Record<string, string> } | null>) {
@@ -762,7 +816,7 @@ export async function ensureSavedGitCommit(cwd: string, repoUrl: string, commit:
timeout: 60_000, maxBuffer: 1024 * 1024, env: { ...process.env, ...auth?.env, GIT_TERMINAL_PROMPT: "0" },
});
if ((await gitCacheCommand(cwd, ["rev-parse", `${commit}^{commit}`])).trim() !== commit) throw new GitCacheRestorationFailure();
} catch { throw new GitCacheRestorationFailure(); }
} catch (error) { throw restorationFailureAt("saved_commit", error); }
}
/** Runtime callers must bind the recipe to the selected workspace and base. */