How the CI Monitor Timeout and Re-Arm Mechanism Works in no-mistakes
The CI monitor in no-mistakes uses a configurable idle timeout that automatically resets (re-arms) whenever the base branch advances, ensuring long-running pull requests stay monitored while abandoning stagnant ones after 7 days by default.
The no-mistakes repository provides a CI pipeline with a "babysit" step that monitors open pull requests until they merge, close, or exceed an idle timeout. Understanding the CI monitor timeout and re-arm mechanism is essential for configuring long-running integration workflows and ensuring your automation handles both active and dormant pull requests appropriately.
Understanding the CI Monitor Timeout and Re-Arm Mechanism
CITimeout Configuration
The timeout duration is controlled by the CITimeout setting defined in internal/config/config.go. The system recognizes the DefaultCITimeout constant of 7 days (7 * 24 * time.Hour) and supports special string values including "unlimited", "none", "off", or "never" to disable automatic termination. When parsing via parseCITimeout, any non-positive duration results in an unlimited monitor that runs until manual interruption or PR closure.
The Re-Arm Trigger
The re-arm mechanism resets the idle timer whenever the default branch tip advances. This ensures that actively rebased pull requests never time out. The logic resides in internal/pipeline/steps/ci.go within the CIStep.Execute method, where the timeoutAnchor variable tracks the last reset time.
Implementation Details in CIStep.Execute
The CIStep.Execute function implements the polling loop and timeout management through the following flow:
1. Load and validate the timeout (lines 101-108):
timeout := sctx.Config.CITimeout
unlimited := timeout < 0
if timeout == 0 {
timeout = config.DefaultCITimeout
}
2. Initialize time anchors:
started := now() // Fixed start moment for logging
timeoutAnchor := started // Mutable anchor for idle calculation
3. Check timeout condition in the main polling loop:
if !unlimited && now().Sub(timeoutAnchor) >= timeout {
return timeoutOutcome()
}
4. Detect base branch movement and re-arm (lines 165-174):
tip, resolved := baseBranchTip(tipCtx)
if resolved && tip != "" {
if lastBaseTip == "" {
lastBaseTip = tip
} else if tip != lastBaseTip {
// Base branch advanced → re-arm the timeout
timeoutAnchor = now()
lastBaseTip = tip
}
}
The helper resolveDefaultBranchTip (called via baseBranchTip) queries the remote for the latest SHA of the default branch. When the SHA changes, the monitor treats this as a re-base event and resets timeoutAnchor to the current time.
5. Dynamic polling intervals: The loop sleeps for a dynamic pollInterval. If the remaining time before timeout is shorter than the standard interval, the sleep duration adjusts to ensure the timeout fires precisely without overshooting.
Configuring the CI Monitor Timeout
Customize the behavior in your global configuration file at ~/.no-mistakes/config.yaml:
# Standard 7-day timeout (default)
ci_timeout: "168h"
# Unlimited monitoring (never self-terminate)
ci_timeout: "unlimited"
The configuration comment in config.go clarifies the semantics:
"CITimeout is interpreted by the CI step as the maximum time to babysit an open PR with no base-branch movement before giving up. The monitor re-arms this timer every time the base branch advances..."
Practical Code Examples
Inspect the effective timeout at runtime:
cfg, _ := config.LoadGlobal("/home/user/.no-mistakes/config.yaml")
fmt.Printf("CI timeout = %v (unlimited = %v)\n",
cfg.CITimeout,
cfg.CITimeout < 0)
Simulate a re-arm event programmatically:
step := steps.CIStep{}
step.baseBranchTip = func(ctx context.Context) (string, bool) {
// Simulate changing tip to trigger re-arm
return fmt.Sprintf("sha-%d", time.Now().Unix()), true
}
Execute with custom configuration:
no-mistakes run --config ~/.no-mistakes/config.yaml
Summary
- The CI monitor timeout defaults to 7 days but accepts "unlimited" to run indefinitely.
- The re-arm mechanism resets the idle timer when
baseBranchTipdetects a new SHA in the default branch. - Implementation lives in
internal/pipeline/steps/ci.go(CIStep.Execute) andinternal/config/config.go. - The
timeoutAnchorvariable tracks the last reset time, compared againstnow()each polling cycle. - Dynamic polling intervals ensure precise timeout enforcement without overshooting the configured duration.
Frequently Asked Questions
What is the default CI monitor timeout in no-mistakes?
The default timeout is 7 days (168 hours), defined as the DefaultCITimeout constant in internal/config/config.go. This value applies when no explicit ci_timeout is configured or when the configuration value is set to zero.
How does the re-arm mechanism detect base branch changes?
The mechanism calls baseBranchTip (which invokes resolveDefaultBranchTip) to query the remote for the latest SHA of the default branch. When the returned SHA differs from lastBaseTip, the code sets timeoutAnchor = now() in internal/pipeline/steps/ci.go, effectively resetting the idle timer.
Can I disable the CI monitor timeout entirely?
Yes. Set ci_timeout to "unlimited", "none", "off", "never", or any non-positive duration in your ~/.no-mistakes/config.yaml. This sets the internal unlimited flag to true, causing the monitor to skip all timeout checks and run until the PR merges, closes, or the process receives an abort signal.
Where is the timeout logic implemented in the source code?
The timeout logic is implemented in two primary locations: internal/config/config.go handles the CITimeout structure, parsing, and defaults, while internal/pipeline/steps/ci.go contains the CIStep.Execute method that manages the polling loop, timeoutAnchor tracking, and re-arming logic.
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 →