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 sessiontimeoutAnchor: Initially set to the same value asstarted, 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:
- Start: Record
startedand initializetimeoutAnchor - 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
timeoutAnchorwhenbaseBranchTipchanges
- 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
timeoutAnchorwhenever the default branch advances, measured relative tolastBaseTipininternal/pipeline/steps/ci.go. - Merge conflict detection queries
host.GetMergeableState()whenCapabilities().MergeableStateis true, settingtimeoutMergeConflictto 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →