Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/core/src/config/config-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,13 @@ export interface RepoConfig {
* @example [".env", ".archon", "data/fixtures/"]
*/
copyFiles?: string[];

/**
* Timeout in milliseconds for worktree creation (git worktree add, fetch, checkout).
* Increase for repos with heavy post-checkout hooks.
* @default 30000
*/
timeout?: number;
};

/**
Expand Down
45 changes: 27 additions & 18 deletions packages/isolation/src/providers/worktree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -556,12 +556,14 @@ export class WorktreeProvider implements IIsolationProvider {
await mkdirAsync(join(worktreeBase, owner, repo), { recursive: true });
}

const timeout = worktreeConfig?.timeout ?? 30000;

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
if (isPRIsolationRequest(request)) {
// For PRs: fetch and checkout the PR branch (actual or synthetic)
await this.createFromPR(request, worktreePath);
await this.createFromPR(request, worktreePath, timeout);
} else {
// For issues, tasks, threads: create new branch
await this.createNewBranch(request, repoPath, worktreePath, branchName, baseBranch);
await this.createNewBranch(request, repoPath, worktreePath, branchName, baseBranch, timeout);
}

// Copy git-ignored files based on repo config
Expand Down Expand Up @@ -722,7 +724,11 @@ export class WorktreeProvider implements IIsolationProvider {
* When prSha is provided, the worktree is initially created at the specific
* commit (detached HEAD), then a local tracking branch is created.
*/
private async createFromPR(request: PRIsolationRequest, worktreePath: string): Promise<void> {
private async createFromPR(
request: PRIsolationRequest,
worktreePath: string,
timeout: number
): Promise<void> {
// Clean up any orphan directory before creating worktree
await this.cleanOrphanDirectoryIfExists(worktreePath);

Expand All @@ -732,10 +738,10 @@ export class WorktreeProvider implements IIsolationProvider {
try {
if (!request.isForkPR) {
// Same-repo PR: Use the actual branch so changes push directly to PR
await this.createFromSameRepoPR(repoPath, worktreePath, request.prBranch);
await this.createFromSameRepoPR(repoPath, worktreePath, request.prBranch, timeout);
} else {
// Fork PR: Use synthetic review branch
await this.createFromForkPR(repoPath, worktreePath, prNumber, request.prSha);
await this.createFromForkPR(repoPath, worktreePath, prNumber, timeout, request.prSha);
}
} catch (error) {
// Clean up orphaned git-registered worktree from partial failure
Expand All @@ -752,11 +758,12 @@ export class WorktreeProvider implements IIsolationProvider {
private async createFromSameRepoPR(
repoPath: string,
worktreePath: string,
prBranch: string
prBranch: string,
timeout: number
): Promise<void> {
// Fetch the PR's actual branch
await execFileAsync('git', ['-C', repoPath, 'fetch', 'origin', prBranch], {
timeout: 30000,
timeout,
});

// Try to create worktree with the branch
Expand All @@ -765,14 +772,14 @@ export class WorktreeProvider implements IIsolationProvider {
await execFileAsync(
'git',
['-C', repoPath, 'worktree', 'add', worktreePath, '-b', prBranch, `origin/${prBranch}`],
{ timeout: 30000 }
{ timeout }
);
} catch (error) {
const err = error as Error & { stderr?: string };
// Branch already exists locally - use it directly
if (err.stderr?.includes('already exists')) {
await execFileAsync('git', ['-C', repoPath, 'worktree', 'add', worktreePath, prBranch], {
timeout: 30000,
timeout,
});
} else {
throw error;
Expand All @@ -784,7 +791,7 @@ export class WorktreeProvider implements IIsolationProvider {
await execFileAsync(
'git',
['-C', worktreePath, 'branch', '--set-upstream-to', `origin/${prBranch}`],
{ timeout: 30000 }
{ timeout }
);
} catch (trackingError) {
getLog().warn({ err: trackingError, worktreePath, prBranch }, 'upstream_tracking_failed');
Expand All @@ -802,26 +809,27 @@ export class WorktreeProvider implements IIsolationProvider {
repoPath: string,
worktreePath: string,
prNumber: string,
timeout: number,
prSha?: string
): Promise<void> {
const reviewBranch = `pr-${prNumber}-review`;

if (prSha) {
// SHA provided: create at specific commit for reproducible reviews
await execFileAsync('git', ['-C', repoPath, 'fetch', 'origin', `pull/${prNumber}/head`], {
timeout: 30000,
timeout,
});

await execFileAsync('git', ['-C', repoPath, 'worktree', 'add', worktreePath, prSha], {
timeout: 30000,
timeout,
});

// Create a local tracking branch so it's not detached HEAD
await this.createBranchWithStaleRetry(
repoPath,
() =>
execFileAsync('git', ['-C', worktreePath, 'checkout', '-b', reviewBranch, prSha], {
timeout: 30000,
timeout,
}),
reviewBranch
);
Expand All @@ -833,13 +841,13 @@ export class WorktreeProvider implements IIsolationProvider {
execFileAsync(
'git',
['-C', repoPath, 'fetch', 'origin', `pull/${prNumber}/head:${reviewBranch}`],
{ timeout: 30000 }
{ timeout }
),
reviewBranch
);

await execFileAsync('git', ['-C', repoPath, 'worktree', 'add', worktreePath, reviewBranch], {
timeout: 30000,
timeout,
});
}
}
Expand Down Expand Up @@ -877,7 +885,8 @@ export class WorktreeProvider implements IIsolationProvider {
repoPath: string,
worktreePath: string,
branchName: string,
baseBranch: string
baseBranch: string,
timeout: number
): Promise<void> {
// Clean up any orphan directory before creating worktree
await this.cleanOrphanDirectoryIfExists(worktreePath);
Expand All @@ -894,7 +903,7 @@ export class WorktreeProvider implements IIsolationProvider {
'git',
['-C', repoPath, 'worktree', 'add', worktreePath, '-b', branchName, startPoint],
{
timeout: 30000,
timeout,
}
);
} catch (error) {
Expand All @@ -911,7 +920,7 @@ export class WorktreeProvider implements IIsolationProvider {
);
}
await execFileAsync('git', ['-C', repoPath, 'worktree', 'add', worktreePath, branchName], {
timeout: 30000,
timeout,
});
} else {
throw error;
Expand Down
1 change: 1 addition & 0 deletions packages/isolation/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ export interface IsolationEnvironmentRow {
export interface WorktreeCreateConfig {
baseBranch?: string;
copyFiles?: string[];
timeout?: number;
}

export type RepoConfigLoader = (repoPath: string) => Promise<WorktreeCreateConfig | null>;
Expand Down