Skip to content
Merged
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
2 changes: 2 additions & 0 deletions src/CodeyBox.Api/AgentConfigHotReload.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1648,6 +1648,8 @@ internal static string SerializePipelineTuning(PipelineTuningOptions opts) =>
opts.AgentSessionResumeMaxAttempts,
opts.MaxRetainedAgentTurnSandboxes,
opts.AutoMergeRaceRecoveryMaxAttempts,
AutoMergeSettleTimeoutSeconds = opts.AutoMergeSettleTimeout.TotalSeconds,
AutoMergeSettlePollIntervalSeconds = opts.AutoMergeSettlePollInterval.TotalSeconds,
opts.MergeLandingMaxAttempts,
opts.MergeGuardReworkMaxAttempts,
opts.MergeResultBuildVerificationEnabled,
Expand Down
45 changes: 44 additions & 1 deletion src/CodeyBox.Core/IUpstreamRemote.cs
Original file line number Diff line number Diff line change
Expand Up @@ -303,11 +303,20 @@ Task<AuditCheckPublicationResult> PublishAuditCheckAsync(
$"Upstream kind '{Name}' does not publish audit check runs."));
}

/// <summary>
/// Forge-reported state of one pull request. <see cref="HeadSha"/>,
/// <see cref="Mergeable"/> and <see cref="MergeableState"/> are null when
/// the upstream kind does not report them (or the forge has not computed
/// mergeability yet) — callers must treat null as unknown, never as clean.
/// </summary>
public sealed record UpstreamPullRequestState(
int Number,
string Url,
PullRequestStatus Status,
string? MergeCommitSha);
string? MergeCommitSha,
string? HeadSha = null,
bool? Mergeable = null,
string? MergeableState = null);

/// <summary>
/// One commit in a branch's exclusive range (<c>&lt;base&gt;..&lt;head&gt;</c>),
Expand Down Expand Up @@ -753,6 +762,24 @@ public sealed record UpstreamCompletionRequest
/// <summary>Merge strategy: "merge", "squash", or "rebase". Matches <c>Upstream.MergeMethod</c>.</summary>
public string MergeMethod { get; init; } = "merge";

/// <summary>
/// Post-push mergeability settle bound for forges that compute
/// mergeability asynchronously after a push (GitHub: <c>mergeable</c>
/// stays null until the computation finishes, and the PR head lags the
/// just-pushed tip). When set, the implementation polls the PR until
/// its head equals the pushed tip and mergeability is known, then
/// merges only when mergeable. Null (legacy callers) skips the settle
/// and merges immediately.
/// </summary>
public TimeSpan? MergeSettleTimeout { get; init; }

/// <summary>
/// Initial interval between mergeability settle polls; the delay backs
/// off exponentially within <see cref="MergeSettleTimeout"/>. Ignored
/// when <see cref="MergeSettleTimeout"/> is null.
/// </summary>
public TimeSpan? MergeSettlePollInterval { get; init; }

/// <summary>
/// PR number already opened on the forge from a prior CompleteAsync attempt.
/// When set, the implementation skips PR creation and proceeds directly to
Expand Down Expand Up @@ -813,6 +840,22 @@ public sealed record UpstreamCompletionOutcome
/// </summary>
public bool AutoMergeRaced { get; init; }

/// <summary>
/// Forge-reported mergeability observed around the merge attempt
/// (GitHub <c>mergeable</c>). Null when the forge had not computed it
/// yet or the upstream kind does not report it. Lets the orchestrator
/// tell a genuine conflict (false with an unchanged base — re-merging
/// cannot help) from a still-computing PR (null — retry the merge call
/// without re-running the merger) without guessing from the base sha.
/// </summary>
public bool? Mergeable { get; init; }

/// <summary>
/// Forge-reported mergeability detail (GitHub <c>mergeable_state</c>,
/// e.g. "clean", "dirty", "blocked", "unknown"). Diagnostic only.
/// </summary>
public string? MergeableState { get; init; }

/// <summary>
/// Tip sha of the work branch the implementation actually pushed (plain
/// or lease-guarded). The orchestrator persists it as the work item's
Expand Down
381 changes: 322 additions & 59 deletions src/CodeyBox.Orchestrator/PipelineRunner.UpstreamPush.cs

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/CodeyBox.Orchestrator/PipelineTuningHotReloadPolicy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ public static class PipelineTuningHotReloadPolicy
nameof(PipelineTuningOptions.AgentSessionResumeMaxAttempts),
nameof(PipelineTuningOptions.MaxRetainedAgentTurnSandboxes),
nameof(PipelineTuningOptions.AutoMergeRaceRecoveryMaxAttempts),
nameof(PipelineTuningOptions.AutoMergeSettleTimeout),
nameof(PipelineTuningOptions.AutoMergeSettlePollInterval),
nameof(PipelineTuningOptions.MergeLandingMaxAttempts),
nameof(PipelineTuningOptions.MergeResultBuildVerificationEnabled),
nameof(PipelineTuningOptions.MergeResultBuildVerificationTimeout),
Expand Down
33 changes: 33 additions & 0 deletions src/CodeyBox.Orchestrator/PipelineTuningOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,27 @@ public sealed class PipelineTuningOptions
/// </summary>
public int AutoMergeRaceRecoveryMaxAttempts { get; set; } = 3;

/// <summary>
/// Upper bound for the post-push mergeability settle before each GitHub
/// auto-merge call: after pushing, the orchestrator asks the remote to
/// poll the PR until its head equals the pushed tip and mergeability is
/// computed, then merges only when mergeable. GitHub recomputes
/// mergeability asynchronously after a push, so merging without this
/// wait observes the previous head and returns a spurious 405. A settle
/// that never converges falls through to the merge call best-effort.
/// Default 60 seconds. Hot-reloaded with the rest of
/// <c>PipelineTuning</c>; read fresh on every upstream-push attempt.
/// </summary>
public TimeSpan AutoMergeSettleTimeout { get; set; } = TimeSpan.FromSeconds(60);

/// <summary>
/// Initial interval between post-push mergeability settle polls; the
/// delay backs off exponentially within
/// <see cref="AutoMergeSettleTimeout"/>. Default 2 seconds.
/// Hot-reloaded with the rest of <c>PipelineTuning</c>.
/// </summary>
public TimeSpan AutoMergeSettlePollInterval { get; set; } = TimeSpan.FromSeconds(2);

/// <summary>
/// Maximum times a single merge phase re-queues its landing after
/// detecting the base branch moved between composition and the
Expand Down Expand Up @@ -655,6 +676,18 @@ public void Validate()
nameof(MergeResultBuildVerificationInfraMaxAttempts),
"MergeResultBuildVerificationInfraMaxAttempts must be >= 1");
}
if (AutoMergeSettleTimeout <= TimeSpan.Zero || AutoMergeSettleTimeout > TimeSpan.FromMinutes(10))
{
throw new ArgumentOutOfRangeException(
nameof(AutoMergeSettleTimeout),
"AutoMergeSettleTimeout must be positive and at most 10 minutes");
}
if (AutoMergeSettlePollInterval <= TimeSpan.Zero || AutoMergeSettlePollInterval > TimeSpan.FromMinutes(5))
{
throw new ArgumentOutOfRangeException(
nameof(AutoMergeSettlePollInterval),
"AutoMergeSettlePollInterval must be positive and at most 5 minutes");
}
if (EarlyEndedTurnMaxNudges < 0)
{
throw new ArgumentOutOfRangeException(
Expand Down
Loading
Loading