fix(worker): clean up orphaned worktree when the DB row insert fails

If WorktreeAddAsync succeeds but the worktrees-row insert throws, the
worktree was left on disk and branch undeleted with nothing tracking it.
Wrap the insert in try/catch and best-effort remove the worktree+branch
(non-cancellable) before rethrowing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-06-04 11:21:40 +02:00
parent bcf5e2f51f
commit 71ac48162a
3 changed files with 67 additions and 21 deletions

View File

@@ -92,21 +92,37 @@ public sealed class WorktreeManager
}
// Insert worktrees row AFTER git succeeds — if git throws, no row is created.
using var context = _dbFactory.CreateDbContext();
var wtRepo = new WorktreeRepository(context);
// Drop any stale row from a prior run (force-remove may have left the DB side behind).
await wtRepo.DeleteAsync(task.Id, ct);
await wtRepo.AddAsync(new WorktreeEntity
// If the insert itself fails, the worktree is already on disk with nothing
// tracking it: remove it (best-effort, non-cancellable) before rethrowing.
try
{
TaskId = task.Id,
Path = worktreePath,
BranchName = branchName,
BaseCommit = baseCommit,
HeadCommit = null,
DiffStat = null,
State = WorktreeState.Active,
CreatedAt = DateTime.UtcNow,
}, ct);
using var context = _dbFactory.CreateDbContext();
var wtRepo = new WorktreeRepository(context);
// Drop any stale row from a prior run (force-remove may have left the DB side behind).
await wtRepo.DeleteAsync(task.Id, ct);
await wtRepo.AddAsync(new WorktreeEntity
{
TaskId = task.Id,
Path = worktreePath,
BranchName = branchName,
BaseCommit = baseCommit,
HeadCommit = null,
DiffStat = null,
State = WorktreeState.Active,
CreatedAt = DateTime.UtcNow,
}, ct);
}
catch (Exception dbEx)
{
_logger.LogError(dbEx,
"Failed to record worktree row for task {TaskId}; removing orphaned worktree at {Path}",
task.Id, worktreePath);
try { await _git.WorktreeRemoveAsync(workingDir, worktreePath, force: true, CancellationToken.None); }
catch (Exception rmEx) { _logger.LogWarning(rmEx, "Failed to remove orphaned worktree at {Path}", worktreePath); }
try { await _git.BranchDeleteAsync(workingDir, branchName, force: true, CancellationToken.None); }
catch (Exception delEx) { _logger.LogWarning(delEx, "Failed to delete orphaned branch {Branch}", branchName); }
throw;
}
_logger.LogInformation("Created worktree for task {TaskId} at {Path} (branch {Branch}, base {Base})",
task.Id, worktreePath, branchName, baseCommit);