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

> Discover how the no-mistakes CI monitor prevents premature termination with rebase-aware timeouts and accurately detects merge conflicts using SCM provider data.

- Repository: [Kun Chen/no-mistakes](https://github.com/kunchenguid/no-mistakes)
- Tags: deep-dive
- Published: 2026-07-04

---

**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`](https://github.com/kunchenguid/no-mistakes/blob/main/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`](https://github.com/kunchenguid/no-mistakes/blob/main/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

```go
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):

```go
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):

```go
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.

```go
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):

```go
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`](https://github.com/kunchenguid/no-mistakes/blob/main/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`](https://github.com/kunchenguid/no-mistakes/blob/main/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.