How the no-mistakes CI Monitor Handles Idle Timeouts: Re-arming Logic Explained

The no-mistakes CI monitor tracks idle timeouts against a movable anchor that resets whenever the upstream default branch advances, ensuring active PRs do not time out while stale PRs are cleaned up.

The continuous integration monitor in the kunchenguid/no-mistakes repository implements a sophisticated idle timeout mechanism that distinguishes between stalled pull requests and those progressing against an active base branch. Implemented by the CIStep type in internal/pipeline/steps/ci.go, the monitor uses a re-arming anchor pattern to dynamically extend timeouts when the default branch advances. Understanding this logic is essential for configuring CI pipelines that balance responsiveness with tolerance for upstream activity.

Timeout Configuration and Semantics

The CI monitor watches a PR until one of three terminal conditions occurs: the PR is merged or closed, the monitor is cancelled, or the configured idle timeout elapses. The timeout duration is controlled by config.CITimeout, which supports three distinct behaviors based on its numeric value. This logic is implemented around lines 101–108 of internal/pipeline/steps/ci.go.

Parsing the CITimeout Value

The monitor interprets the timeout configuration using the following rules:

  • Negative values (< 0): Disable the idle timeout completely, causing the monitor to run indefinitely until the PR closes or the process is cancelled.
  • Zero: Falls back to config.DefaultCITimeout, a sensible default defined in the configuration package.
  • Positive values: Define a finite idle timeout measured in seconds (or the relevant time unit).

The Re-arming Anchor Pattern

Rather than measuring idle time from a fixed start point, the monitor tracks two separate timestamps to distinguish between overall execution time and idle time relative to base branch activity.

Tracking started and timeoutAnchor

The step initializes two critical timestamps at lines 124–130:

  • started: Captures the moment the CI step begins and remains fixed for the entire run.
  • timeoutAnchor: Initially set to the same value as started, but designed to move forward.

The idle timeout is calculated as now() - timeoutAnchor. When the upstream default branch advances, timeoutAnchor is moved forward to the current time, effectively resetting the idle timer without restarting the entire step.

Re-arming When the Base Branch Advances

After each polling iteration, the step resolves the current tip SHA of the upstream default branch (baseBranchTip) within a resolution window of defaultBaseBranchTipResolveWindow = 30s. If the resolved tip differs from the previously observed tip (lastBaseTip), the monitor re-arms by setting timeoutAnchor = now() (lines 156–176).

If the remaining timeout is shorter than the standard 30-second window, the resolution window is automatically trimmed to prevent exceeding the timeout during the check. This re-arming logic ensures that PRs tracking an actively evolving base branch remain open, while PRs stalled against a static branch eventually time out.

Polling Loop and Timeout Enforcement

Each iteration of the polling loop first checks whether the elapsed time since timeoutAnchor exceeds the configured timeout (lines 152–154). If the threshold is exceeded, timeoutOutcome() constructs the appropriate terminal state (merged, closed, CI failure, or simple timeout).

Grace Period for Empty CI Checks

Before declaring a timeout based on missing CI data, the step respects a checksGracePeriod (defaulting to 60 seconds). This grace period, referenced at lines 141–149, prevents premature termination when CI providers have not yet registered checks or delivered webhooks. The monitor only treats "no checks reported" as a terminal condition after this grace period has elapsed.

Practical Implementation Examples

The following examples demonstrate how to configure the CIStep and simulate its re-arming behavior in tests.

// Example: Creating a CIStep with a custom timeout and base branch resolver
ciStep := &steps.CIStep{
    // The timeout is typically sourced from config.CITimeout.
    // A negative value disables the timeout.
    // A zero value uses config.DefaultCITimeout.
    // Here we illustrate a custom resolver for the base branch tip.
    baseBranchTip: func(ctx context.Context) (string, bool) {
        // Custom resolver reading from a remote-tracking branch.
        tip, err := git.GetRemoteTip(ctx, repoURL, "origin/main")
        return tip, err == nil
    },
}
// Example: Simulating timeout re-arming in a unit test
func TestCIMonitorRearmsOnBaseBranchAdvance(t *testing.T) {
    step := &steps.CIStep{
        now: func() time.Time { return time.Unix(0, 0) },
    }

    // Mock baseBranchTip to return advancing SHAs.
    call := 0
    step.baseBranchTip = func(ctx context.Context) (string, bool) {
        if call == 0 {
            call++
            return "sha1", true // Initial tip
        }
        return "sha2", true // Tip advanced → triggers re-arm
    }

    // Execution would verify that timeoutAnchor moves forward
    // when the base branch SHA changes from sha1 to sha2.
}

Summary

  • The CIStep type in internal/pipeline/steps/ci.go implements idle timeout logic using a re-arming anchor pattern that distinguishes between global execution time and idle time relative to base branch activity.
  • Timeout values originate from config.CITimeout, where negative values disable timeouts, zero triggers the config.DefaultCITimeout fallback, and positive values specify finite durations.
  • The monitor maintains a fixed started timestamp and a movable timeoutAnchor that resets to now() whenever the upstream default branch tip advances (lines 156–176).
  • Each poll iteration validates the idle duration against timeoutAnchor (lines 152–154), with a 30-second resolution window for tip detection.
  • A 60-second checksGracePeriod (lines 141–149) prevents false timeouts during CI provider initialization latency.

Frequently Asked Questions

What happens if the CITimeout configuration is set to a negative value?

When config.CITimeout is less than zero, the monitor runs without an idle timeout limit, continuing to poll indefinitely until the pull request is merged, closed, or the process receives a cancellation signal.

How does the re-arming mechanism prevent timeouts on active base branches?

The monitor resolves the current base branch tip SHA after each poll; if it differs from lastBaseTip, the code updates timeoutAnchor to the current time (lines 156–176). This resets the idle timer measurement, allowing the PR to remain monitored while the base branch continues to evolve.

Where is the default timeout value defined when CITimeout is zero?

If config.CITimeout is zero, the system substitutes config.DefaultCITimeout, which is declared in the repository's configuration defaults (typically located in internal/config/defaults.go or an adjacent configuration file).

What is the purpose of the checksGracePeriod in the CI monitor?

The checksGracePeriod (defaulting to 60 seconds) provides a buffer after CI initiation before the monitor treats the absence of reported checks as a terminal failure condition. This accommodates network latency and webhook delivery delays from CI providers.

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 →