Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
48 changes: 30 additions & 18 deletions packages/isolation/src/providers/worktree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -556,12 +556,17 @@ export class WorktreeProvider implements IIsolationProvider {
await mkdirAsync(join(worktreeBase, owner, repo), { recursive: true });
}

const timeout =
typeof worktreeConfig?.timeout === 'number' && worktreeConfig.timeout > 0
? worktreeConfig.timeout
: 30000;

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 +727,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 +741,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 +761,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 +775,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 +794,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 +812,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 +844,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 +888,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 +906,7 @@ export class WorktreeProvider implements IIsolationProvider {
'git',
['-C', repoPath, 'worktree', 'add', worktreePath, '-b', branchName, startPoint],
{
timeout: 30000,
timeout,
}
);
} catch (error) {
Expand All @@ -911,7 +923,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
Loading