feat(worker): approve in a company repo verifies in the worktree, pushes and opens a draft PR
This commit is contained in:
@@ -3,6 +3,7 @@ using ClaudeDo.Data;
|
||||
using ClaudeDo.Data.Git;
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Data.Repositories;
|
||||
using ClaudeDo.Worker.AzureDevOps;
|
||||
using ClaudeDo.Worker.Hub;
|
||||
using ClaudeDo.Worker.Runner;
|
||||
using ClaudeDo.Worker.State;
|
||||
@@ -15,13 +16,15 @@ namespace ClaudeDo.Worker.Lifecycle;
|
||||
public sealed record MergeResult(
|
||||
string Status,
|
||||
IReadOnlyList<string> ConflictFiles,
|
||||
string? ErrorMessage);
|
||||
string? ErrorMessage,
|
||||
string? PullRequestUrl = null);
|
||||
|
||||
public sealed record MergeTargets(
|
||||
string DefaultBranch,
|
||||
IReadOnlyList<string> LocalBranches,
|
||||
string DefaultCommitMessage,
|
||||
bool TargetDirty = false);
|
||||
bool TargetDirty = false,
|
||||
bool IsCompanyRepo = false);
|
||||
|
||||
// VerifyExitCode/VerifyDurationMs/VerifyOutputTail are only populated when the caller asked for
|
||||
// a verify run (runVerify=true) AND the list has a verify command configured AND the preview came
|
||||
@@ -76,6 +79,11 @@ public sealed class TaskMergeService
|
||||
// autostash: true) without matching on prose.
|
||||
public const string StatusDirtyTarget = "dirty_target";
|
||||
|
||||
// Company repo: approve pushed the unit branch and opened (or reused) its draft PR; the task stays
|
||||
// WaitingForReview, nothing landed locally.
|
||||
public const string StatusPrCreated = "pr_created";
|
||||
public const string StatusPrUpdated = "pr_updated";
|
||||
|
||||
public const string StatusReverted = "reverted";
|
||||
public const string StatusConflictAborted = "conflict_aborted";
|
||||
|
||||
@@ -118,6 +126,7 @@ public sealed class TaskMergeService
|
||||
private readonly IVerifyCommandRunner _verify;
|
||||
private readonly ILogger<TaskMergeService> _logger;
|
||||
private readonly UnitBranchService? _unitBranch;
|
||||
private readonly PullRequestPublisher? _pullRequests;
|
||||
|
||||
public TaskMergeService(
|
||||
IDbContextFactory<ClaudeDoDbContext> dbFactory,
|
||||
@@ -126,7 +135,8 @@ public sealed class TaskMergeService
|
||||
ITaskStateService state,
|
||||
IVerifyCommandRunner verify,
|
||||
ILogger<TaskMergeService> logger,
|
||||
UnitBranchService? unitBranch = null)
|
||||
UnitBranchService? unitBranch = null,
|
||||
PullRequestPublisher? pullRequests = null)
|
||||
{
|
||||
_dbFactory = dbFactory;
|
||||
_git = git;
|
||||
@@ -135,6 +145,7 @@ public sealed class TaskMergeService
|
||||
_verify = verify;
|
||||
_logger = logger;
|
||||
_unitBranch = unitBranch;
|
||||
_pullRequests = pullRequests;
|
||||
}
|
||||
|
||||
private async Task<(TaskEntity Task, ListEntity List, WorktreeEntity? Worktree, string? VerifyCommand)> LoadMergeContextAsync(
|
||||
@@ -881,7 +892,8 @@ public sealed class TaskMergeService
|
||||
var current = await ResolveTargetAsync(list, null, ct);
|
||||
var branches = await _git.ListLocalBranchesAsync(list.WorkingDir, ct);
|
||||
var dirty = await _git.HasChangesAsync(list.WorkingDir, includeUntracked: false, ct);
|
||||
return new MergeTargets(current, branches, defaultMessage, dirty);
|
||||
var company = _pullRequests is not null && await _pullRequests.IsCompanyRepoAsync(list.WorkingDir, ct);
|
||||
return new MergeTargets(current, branches, defaultMessage, dirty, company);
|
||||
}
|
||||
|
||||
/// <summary>Leeres Ziel → <c>list_config.base_branch</c>, sonst der ausgecheckte Branch des Main-Checkouts.</summary>
|
||||
@@ -1042,6 +1054,11 @@ public sealed class TaskMergeService
|
||||
}
|
||||
}
|
||||
|
||||
if (wt is { State: WorktreeState.Active } && _pullRequests is not null
|
||||
&& !string.IsNullOrWhiteSpace(list.WorkingDir)
|
||||
&& await _pullRequests.IsCompanyRepoAsync(list.WorkingDir, ct))
|
||||
return await ApproveCompanyAsync(task, list, wt, verifyCommand, ct, progress);
|
||||
|
||||
if (wt is not null && wt.State == WorktreeState.Kept)
|
||||
{
|
||||
// Kept means either RevertMergeAsync undid a previous merge, or the user manually
|
||||
@@ -1125,6 +1142,33 @@ public sealed class TaskMergeService
|
||||
return await MergeAsync(taskId, target, removeWorktree: true, commitMessage: "", leaveConflictsInTree, ct, progress, skipVerify, autostash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Company repo: verify in the unit root's worktree (nothing lands on a local target, so there is no
|
||||
/// merge to defer the verify to — <c>skipVerify</c> does not apply), then push + draft PR. Red verify
|
||||
/// pushes nothing. Status, worktree state and ticket stay as they are.
|
||||
/// </summary>
|
||||
private async Task<MergeResult> ApproveCompanyAsync(
|
||||
TaskEntity task, ListEntity list, WorktreeEntity wt, string? verifyCommand, CancellationToken ct,
|
||||
IProgress<ProgressNotificationValue>? progress)
|
||||
{
|
||||
if (await _pullRequests!.CheckReadyAsync(list.WorkingDir!, ct) is { } notReady)
|
||||
return Blocked(notReady);
|
||||
|
||||
var ranVerify = !string.IsNullOrWhiteSpace(verifyCommand);
|
||||
if (ranVerify)
|
||||
{
|
||||
await _broadcaster.OperationProgress(task.Id, PhaseVerifying, 0, 0);
|
||||
var failed = await RunVerifyGateAsync(
|
||||
verifyCommand, wt.Path, ct, progress,
|
||||
elapsed => _ = _broadcaster.OperationProgress(task.Id, PhaseVerifying, (int)elapsed.TotalSeconds, 0));
|
||||
if (failed is not null) return failed;
|
||||
}
|
||||
|
||||
return await ProgressReporter.RunAsync(
|
||||
_pullRequests.PublishAsync(task, list, wt, ranVerify ? verifyCommand : null, ct),
|
||||
ProgressReportInterval, progress, "push + draft PR running");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Redoes a merge that <see cref="RevertMergeAsync"/> undid (or that predates it, if the branch
|
||||
/// was already merged before its worktree reached <see cref="WorktreeState.Kept"/>), by
|
||||
|
||||
Reference in New Issue
Block a user