Skip to content

feat: add configurable plan timeout to expire stale plans#6317

Open
demarsdouglas wants to merge 1 commit intorunatlantis:mainfrom
demarsdouglas:add-plan-timeout-issue-270
Open

feat: add configurable plan timeout to expire stale plans#6317
demarsdouglas wants to merge 1 commit intorunatlantis:mainfrom
demarsdouglas:add-plan-timeout-issue-270

Conversation

@demarsdouglas
Copy link
Copy Markdown

@demarsdouglas demarsdouglas commented Mar 16, 2026

what

  • Adds a --plan-timeout server flag that sets a maximum age for Terraform plans. When atlantis apply is run after the timeout has elapsed, the plan is discarded, the lock is released, and the user sees a message asking them to re-plan.
  • Default behavior is unchanged - if the flag is not set (or set to empty/zero), plans never expire.
  • Includes documentation, flag registration, unit tests, and validation for the new option.

why

  • Long-lived plans can become stale and dangerous to apply. This gives administrators a knob to enforce plan freshness and automatically free locks held by forgotten plans.

how

  • ProjectLock already stores a Time field when the lock is created during atlantis plan.
  • The new --plan-timeout flag is parsed as a time.Duration (e.g. 15m, 2h30m).
  • When atlantis apply runs, doApply() checks time.Since(lockTime) against the configured timeout.
  • If expired: the .tfplan file is removed, the lock is released, and a failure message is returned.
  • If not expired (or timeout is zero): behavior is identical to before.

tests

TestDefaultProjectCommandRunner_ApplyPlanExpired - expired plan is rejected, file deleted, lock released
TestDefaultProjectCommandRunner_ApplyPlanNotExpired - recent plan proceeds normally
TestDefaultProjectCommandRunner_ApplyNoPlanTimeout - zero timeout preserves backward compatibility
TestDefaultProjectCommandRunner_ApplyPlanExpiredZeroLockTime - zero lock time skips the check
TestDefaultProjectLocker_TryLockPopulatesLockTime - lock time propagated for fresh and re-acquired locks
TestParsePlanTimeout - valid durations, empty input, invalid strings, and negative durations

references

closes #270

Copilot AI review requested due to automatic review settings March 16, 2026 04:01
@dosubot dosubot Bot added feature New functionality/enhancement go Pull requests that update Go code labels Mar 16, 2026
@github-actions github-actions Bot added the docs Documentation label Mar 16, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an optional server-side “plan freshness” control to Atlantis by introducing a configurable plan timeout that prevents applying stale Terraform plans and automatically releases associated locks.

Changes:

  • Introduces --plan-timeout / ATLANTIS_PLAN_TIMEOUT (Go time.Duration syntax) and wires it through server config.
  • Implements apply-time plan expiration: delete the .tfplan file, release the project lock, and return a re-plan message when expired.
  • Adds unit tests for timeout parsing, lock time propagation, and apply behavior; updates server configuration docs.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
server/user_config.go Adds plan-timeout to UserConfig for Viper/mapstructure unmarshalling.
cmd/server.go Registers --plan-timeout as a server flag (string) with help text and env var support.
cmd/server_test.go Extends flag test coverage to include plan-timeout.
server/server.go Adds ParsePlanTimeout, parses config at startup, and injects the duration into DefaultProjectCommandRunner.
server/server_test.go Adds unit tests for ParsePlanTimeout.
server/events/project_locker.go Exposes lock creation time via TryLockResponse.LockTime for downstream expiration checks.
server/events/project_locker_test.go Adds test validating lock time propagation through the project locker wrapper.
server/events/project_command_runner.go Enforces plan expiration during apply and performs cleanup (plan file + lock release).
server/events/project_command_runner_test.go Adds tests for expired/not-expired/disabled timeout and zero lock-time behavior.
runatlantis.io/docs/server-configuration.md Documents --plan-timeout usage and semantics.

Comment on lines +690 to +701
if p.PlanTimeout > 0 && !lockAttempt.LockTime.IsZero() && time.Since(lockAttempt.LockTime) > p.PlanTimeout {
ctx.Log.Info("plan has expired (created %s ago, timeout %s), discarding", time.Since(lockAttempt.LockTime).Round(time.Second), p.PlanTimeout)
planFile := filepath.Join(absPath, runtime.GetPlanFilename(ctx.Workspace, ctx.ProjectName))
if removeErr := os.Remove(planFile); removeErr != nil && !os.IsNotExist(removeErr) {
ctx.Log.Err("failed to remove expired plan file: %v", removeErr)
}
if unlockErr := lockAttempt.UnlockFn(); unlockErr != nil {
ctx.Log.Err("failed to release lock for expired plan: %v", unlockErr)
}
return "", fmt.Sprintf("The plan has expired (created %s ago, timeout is %s). Please run `atlantis plan` again.",
time.Since(lockAttempt.LockTime).Round(time.Second), p.PlanTimeout), nil
}
Comment on lines +254 to +258
if tt.lockAcquired {
mockLocker.EXPECT().Unlock(lockKey).Return(nil, nil)
} else {
mockLocker.EXPECT().Unlock(lockKey).Return(nil, nil)
}
tests := []struct {
name string
lockAcquired bool
description string
Signed-off-by: demarsdouglas <demarsdouglas@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation feature New functionality/enhancement go Pull requests that update Go code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Configurable plan timeouts

2 participants