# How CI Monitor Timeout Re-Arms on Base Branch Advances in no-mistakes

> Learn how the CI monitor timeout in no-mistakes re-arms on base branch advances to keep green PRs monitored and eventually time out stalled ones.

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

---

**The CI monitor in `no-mistakes` restarts its idle timeout countdown by updating the `timeoutAnchor` to the current time whenever the base branch tip advances, allowing green PRs to remain monitored indefinitely while stalling ones eventually time out.**

The `no-mistakes` repository implements a sophisticated **CI monitor** (the `CIStep`) that babysits open pull requests until they merge, close, or hit an idle timeout. Understanding how the **CI monitor timeout** re-arms when the base branch advances is essential for maintaining long-running green PRs that track active development branches without premature termination.

## CITimeout Configuration and Initial Anchor Setup

The timeout duration is controlled by `Config.CITimeout`, which parses the `ci_timeout` entry from the global configuration file. In [`internal/config/config.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/config/config.go) (lines 22‑28), this duration is loaded and validated, with support for standard duration strings or the keyword `"unlimited"`.

When the `CIStep` initializes, it establishes 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):

```go
started := now()
timeoutAnchor := started

```

The `started` value remains fixed for the lifetime of the step, providing a stable reference for poll intervals and grace-period pacing. The `timeoutAnchor`, however, is the mutable reference point that determines when the idle timeout fires.

## How Base Branch Advances Trigger Timeout Re-Arming

The re-arming logic operates on a polling cycle, checking the upstream default branch for new commits that would invalidate the current merge base.

### Resolving the Base Branch Tip

On every poll (default 30‑second window), the monitor calls `baseBranchTip` to resolve the current SHA of the upstream default branch. This function returns the latest tip along with a resolution status, allowing the monitor to detect any movement in the base branch since the last check.

### Detecting Advances and Updating the Anchor

In [`internal/pipeline/steps/ci.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/ci.go) (lines 56‑74), the monitor compares the resolved tip against `lastBaseTip`. When the SHA differs, indicating the base branch has advanced, the code logs the advance and immediately updates the timeout reference:

```go
timeoutAnchor = now()

```

This assignment effectively **re-arms the timeout**, restarting the idle countdown from zero. Only base branch advances trigger this update; other activities such as CI checks re-running or status updates do not affect the anchor.

### The Idle Timeout Check

Before each poll and after the base-branch validation, the monitor evaluates whether the idle limit has been exceeded:

```go
if now().Sub(timeoutAnchor) >= timeout {
    // terminate with timeout outcome
}

```

Because the anchor moves forward only when the base branch advances, a PR that continuously rebases onto an active default branch can remain monitored indefinitely (or until the configured limit if not set to `"unlimited"`). Conversely, a PR whose base has stalled will eventually trigger the timeout and terminate.

## Configuring the CI Timeout

You can configure the re-arming behavior through the global configuration file. To set a 7‑day idle timeout:

```yaml

# ~/.no-mistakes/config.yaml

ci_timeout: "168h"

```

To disable the timeout entirely and allow the monitor to run forever:

```yaml
ci_timeout: "unlimited"

```

When testing the re-arming logic, you can override the poll interval and stub the base branch resolver. In [`internal/pipeline/steps/ci_test.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/ci_test.go), the test suite validates this behavior by simulating base branch advances and verifying that `timeoutAnchor` updates only when the tip changes:

```go
step := &steps.CIStep{
    pollIntervalOverride: 5 * time.Second,
    // baseBranchTip can be stubbed to simulate advances
}

```

## Summary

- **CITimeout** is parsed from `ci_timeout` in [`internal/config/config.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/config/config.go) (lines 22‑28) and controls the idle duration.
- The `timeoutAnchor` initializes to the step start time in [`internal/pipeline/steps/ci.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/ci.go) (lines 25‑30) but mutates independently of the fixed `started` timestamp.
- Base branch advances trigger re-arming by updating `timeoutAnchor` to `now()` in [`internal/pipeline/steps/ci.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/ci.go) (lines 56‑74).
- Only tip changes restart the countdown; CI check re-runs do not affect the timeout.
- The monitor polls every 30 seconds by default, checking `now().Sub(timeoutAnchor) >= timeout` to determine termination.

## Frequently Asked Questions

### What triggers the re-arming of the CITimeout?

The timeout re-arms exclusively when the **base branch tip** advances. When the SHA returned by `baseBranchTip` differs from the previously recorded tip, the monitor updates `timeoutAnchor` to the current time, effectively resetting the idle countdown to zero.

### How do I configure an unlimited CI timeout?

Set `ci_timeout: "unlimited"` in your `~/.no-mistakes/config.yaml`, or use any non‑positive duration value. This configuration prevents the idle timeout from firing regardless of how long the base branch remains static, allowing the monitor to run indefinitely.

### Does the timeout re-arm when CI checks re-run?

No. The re-arming logic specifically ignores CI check activity and only responds to base branch tip changes. As implemented in [`internal/pipeline/steps/ci.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/ci.go), the anchor update occurs only when the default branch SHA moves forward, ensuring that transient CI failures or re-runs do not extend the monitoring window.

### Where is the re-arming logic implemented?

The core re-arming implementation resides in [`internal/pipeline/steps/ci.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/ci.go) (lines 56‑74), which contains the comparison between `lastBaseTip` and the current resolved tip, along with the `timeoutAnchor` update. The timeout duration configuration is defined in [`internal/config/config.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/config/config.go) (lines 22‑28), and comprehensive tests validating the behavior are located in [`internal/pipeline/steps/ci_test.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/pipeline/steps/ci_test.go).