How the no-mistakes CI Monitor Handles Rebase-Aware Timeouts and Merge Conflict Detection

The no-mistakes CI monitor implements an idle timeout that automatically resets when the upstream default branch advances, preventing premature termination during rebases, while simultaneously querying the SCM provider's mergeable state to detect conflicts and report them as distinct failure outcomes.

The kunchenguid/no-mistakes repository provides a sophisticated CI pipeline automation tool that monitors open pull requests until they merge, close, or expire. Unlike naive timeout mechanisms, the CIStep implementation in internal/pipeline/steps/ci.go distinguishes between stalled automation and active development workflows involving frequent rebases, while also surfacing structural blockers like merge conflicts before they silently timeout.

Rebase-Aware Timeout Handling in CIStep

The CI monitor avoids killing long-running PRs undergoing legitimate rebases by tracking the upstream default branch's advancement and re-arming its timeout anchor accordingly.

Initialization of Timeout Anchors

When monitoring begins, the step records two critical timestamps in internal/pipeline/steps/ci.go (lines 25–30):

  • started: Captures the absolute beginning of the CI monitoring session
  • timeoutAnchor: Initially set to the same value as started, but mutable based on upstream activity
started := now()
timeoutAnchor := started

The timeout evaluation checks elapsed time against timeoutAnchor, not started, enabling the re-arming behavior without losing the original session context.

Detecting Base Branch Advancements

During each polling iteration, the monitor resolves the current tip of the default branch using the baseBranchTip helper. To prevent the resolution itself from exhausting the timeout budget, the code limits the operation to a 30-second window (defaultBaseBranchTipResolveWindow), dynamically shortened if the remaining timeout is smaller (lines 64–70):

resolveWindow := defaultBaseBranchTipResolveWindow
if remaining := timeout - now().Sub(timeoutAnchor); remaining <= 0 {
    return timeoutOutcome()
} else if remaining < resolveWindow {
    resolveWindow = remaining
}
tipCtx, cancel := context.WithTimeout(ctx, resolveWindow)
tip, resolved := baseBranchTip(tipCtx)
cancel()

Re-Arming the Timeout Logic

When the resolved tip differs from the previously recorded lastBaseTip, the monitor detects a rebase or upstream advancement and moves timeoutAnchor forward to the current time, effectively resetting the idle countdown (lines 71–74):

if resolved && tip != "" {
    if lastBaseTip == "" {
        lastBaseTip = tip
    } else if tip != lastBaseTip {
        sctx.Log(fmt.Sprintf(
            "base branch advanced (%s..%s), re‑arming CI monitor timeout",
            shortSHA(lastBaseTip), shortSHA(tip)))
        timeoutAnchor = now()  // Reset the timeout reference point
        lastBaseTip = tip
    }
}

This mechanism ensures that timeout only applies to idle periods without upstream activity, allowing PRs to remain open indefinitely while the default branch evolves.

Graceful Timeout Completion

When the idle duration finally exceeds the configured limit relative to timeoutAnchor, the timeoutOutcome() function (lines 36–45) evaluates the collected state to determine whether to report a CI failure, a merge conflict, or a pure monitoring timeout, preserving diagnostic context for the user.

Merge Conflict Detection Through SCM Integration

The monitor proactively detects unresolvable structural conflicts by querying the SCM provider's native mergeability APIs, distinguishing between temporary CI delays and permanent merge blockers.

Capability-Based Provider Queries

Before attempting conflict detection, the code verifies that the SCM host supports mergeable state queries via host.Capabilities().MergeableState (line 200). This abstraction allows the monitor to work across different providers (GitHub, GitLab, etc.) while gracefully degrading where the capability is unavailable.

if host.Capabilities().MergeableState {
    mergeState, mergeErr := host.GetMergeableState(ctx, pr)
    // ... conflict evaluation
}

Interpreting Mergeable State

When supported, the monitor calls host.GetMergeableState(ctx, pr) and interprets the returned MergeableState object to determine conflict status (lines 201–219):

mergeState, mergeErr := host.GetMergeableState(ctx, pr)
if mergeErr != nil {
    sctx.Log(fmt.Sprintf("warning: could not check mergeable state: %v", mergeErr))
    mergeabilityKnown = false
} else {
    mergeConflict = mergeState.Conflict()
    mergeabilityKnown = mergeState.Resolved()
    if !mergeabilityKnown {
        sctx.Log(fmt.Sprintf("mergeable state still pending: %s", mergeState))
        mergeabilityBlockedReason = fmt.Sprintf(
            "PR mergeability remained unresolved before timeout: %s", mergeState)
    } else {
        mergeabilityBlockedReason = ""
        timeoutMergeConflict = mergeConflict  // Flag for timeout outcome
    }
}

If mergeState.Conflict() returns true, the monitor sets timeoutMergeConflict, ensuring that when the idle timeout eventually fires, the timeoutOutcome() function returns a merge-conflict outcome rather than a generic timeout failure. This prevents silent dismissal of PRs that require manual conflict resolution.

Implementation Flow Overview

The complete monitoring lifecycle follows this sequence:

  1. Start: Record started and initialize timeoutAnchor
  2. Poll Loop (repeating):
    • Check for PR merge/close events
    • Query mergeable state if the provider supports it
    • Evaluate CI check status; wait if pending
    • Attempt auto-fix on CI failures where applicable
    • Re-arm timeoutAnchor when baseBranchTip changes
  3. Exit: Trigger on merge, close, successful CI completion, or timeout expiration

Supporting files include internal/scm/* for provider-specific implementations of GetMergeableState, and internal/cimonitor/cimonitor.go for status message constants like ChecksPassedMsg and NoChecksPassedMsg.

Summary

  • Rebase-aware timeouts prevent premature termination by resetting timeoutAnchor whenever the default branch advances, measured relative to lastBaseTip in internal/pipeline/steps/ci.go.
  • Merge conflict detection queries host.GetMergeableState() when Capabilities().MergeableState is true, setting timeoutMergeConflict to distinguish conflicts from idle timeouts.
  • The 30-second resolution window (defaultBaseBranchTipResolveWindow) ensures base branch queries cannot exhaust the remaining timeout budget.
  • Provider-agnostic design allows graceful degradation on SCM platforms lacking mergeable state APIs.

Frequently Asked Questions

How does the timeout distinguish between an idle PR and one undergoing active rebases?

The monitor uses a movable anchor pattern. While started remains fixed to track total session duration, timeoutAnchor updates to the current time whenever baseBranchTip detects a change in the default branch SHA. This means the idle timeout only counts periods where the upstream branch has not advanced, allowing indefinite monitoring during active rebase workflows.

What happens when a merge conflict is detected during CI monitoring?

When mergeState.Conflict() returns true, the monitor sets timeoutMergeConflict = true. If the PR subsequently times out, the timeoutOutcome() function returns a merge-conflict failure rather than a standard monitoring timeout. This surfaces the conflict to the user immediately, preventing the PR from being silently dropped due to timeout while requiring manual resolution.

Is merge conflict detection supported for all SCM providers?

No. The code explicitly checks host.Capabilities().MergeableState before attempting detection. Providers that do not expose mergeable state APIs (or where the capability is disabled) will skip conflict detection and rely solely on CI check status and idle timeout logic. The warning is logged when GetMergeableState returns an error, allowing the monitor to continue operation.

How long does the monitor wait when resolving the base branch tip?

The monitor uses a dynamic window approach with a default of 30 seconds (defaultBaseBranchTipResolveWindow). However, if the remaining timeout budget is smaller than this window, the code shortens the resolution context to remaining time. This ensures that resolving the base branch tip cannot consume the entire timeout allocation, preserving the monitor's ability to exit gracefully if the SCM provider is slow to respond.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →