Atlantis Plan State Management#
Overview#
Atlantis plan state management ensures that an atlantis apply can only execute against a Terraform plan that was generated from the current PR head commit and base branch. This prevents scenarios where a PR's code changes after a plan runs — leaving a stale plan file and outdated DB status that no longer reflect what will actually be applied.
The mechanism has three pillars:
- Pre-apply validation —
apply_plan_validator.gochecks live VCS identity against storedPullStatusbefore allowing apply execution. - BoltDB storage with
HeadCommitas a freshness key —boltdb.gouses the PR's head commit (and base branch) to decide whether stored plan state is current or must be reset. - DB write filtering —
db_updater.gosuppresses writes from stale autoplan or apply results that belong to a different head than what is currently recorded.
These were hardened significantly in PR #6605 to close race windows and fail-closed on any ambiguous or stale state.
Data Model: PullStatus and Freshness Fields#
models.PullStatus is the top-level container written to the database. It holds:
Pull— themodels.PullRequestsnapshot taken at plan time, includingHeadCommitandBaseBranch.Projects— a slice ofProjectStatus, each carrying aProjectPlanStatusenum and policy check results.
Two fields of models.PullRequest drive freshness decisions:
| Field | Description |
|---|---|
HeadCommit | SHA of the PR's head. Bitbucket Cloud truncates this to 12 characters. |
BaseBranch | Target branch name. Same-head retargets (e.g., changing the merge target) produce a different plan and must be replanned. |
Apply-eligible ProjectPlanStatus values :
| Status | Meaning |
|---|---|
PlannedPlanStatus | Plan succeeded; not yet applied |
PlannedNoChangesPlanStatus | Plan returned no changes |
PassedPolicyCheckStatus | All policy checks passed or approved |
ErroredApplyStatus | Apply previously errored; retry is allowed |
Any other status — including ErroredPlanStatus, ErroredPolicyCheckStatus, DiscardedPlanStatus, AppliedPlanStatus — blocks apply execution .
BoltDB Storage and Freshness Logic#
Atlantis uses an embedded BoltDB database stored as atlantis.db in the data directory . Pull statuses live in the "pulls" bucket , keyed by hostname::repo::pullNum .
UpdatePullWithResults is the main write path. On each plan or apply run it:
- Reads the existing
PullStatusfor the PR. - Calls
pullStatusOutdatedForPullto decide whether to reset or merge:- If
statusPull.HeadCommit != pull.HeadCommit→ reset: discard all project statuses, start fresh with the new results. - If
BaseBranchchanged → also reset (same-head retargets require a new plan). - Otherwise → merge: update only the projects present in
newResults, preserving others.
- If
- When resetting, policy check statuses from the previous entry are copied forward for matching projects . This preserves policy approvals during the window between a plan write and the subsequent policy-check write.
- Overwrites the DB entry with the new
PullStatus.
GetPullStatus — simple read; returns nil if no entry exists for the pull key.
Pre-Apply Validation#
DefaultApplyPlanValidator implements two interfaces:
ApplyCommandStartValidator.ValidateCommandStartHead— called before building apply commands; fetches the live PR identity and fails immediately if head SHA or base branch has changed since the command started.ApplyPlanValidator.ValidateProjectPlan— the per-project gate executed immediately before each apply step.
ValidateProjectPlan execution flow#
- Load
PullStatusfrom DB (or fromctx.PullStatusfor API paths). - Fetch live PR identity via
LivePullHeadFetcher— a real-time VCS API call returning the currentHeadCommit+BaseBranchfor all supported platforms (GitHub, GitLab, Azure DevOps, Gitea, Bitbucket Cloud/Server). - Freshness check via
pullStatusApplyEligibilityError: compare storedPullStatus.Pullagainst the live identity. Rejects if:- Stored
HeadCommitis empty while the live head is known. - Stored
HeadCommitdiffers from the live head. - Stored
BaseBranchis empty or differs from the live base.
- Stored
- Command-start identity check via
validateCommandStartIdentity: wraps the error inerrStaleCommandHeadif the head or base drifted between command start and now. - Project lookup: find the matching project in
PullStatus; reject with "no matching plan status" if absent. - Status check via
statusAllowedForApplyExecution. - Plan file existence check on disk.
- SHA-256 hash verification if
ctx.ExpectedPlanHashis set — hashes are captured under the apply lock and rechecked immediately before each apply step.
On any validation failure, rejectProjectPlan deletes the plan file in addition to returning the error, ensuring stale plan files do not persist.
DB Write Filtering (db_updater.go)#
DBUpdater.updateDB is the central DB write path invoked after any plan or apply command completes. It filters results before they reach BoltDB.UpdatePullWithResults:
Stale empty autoplan result : If results is empty and the current DB entry is for a different head/base than the command ran against, the write is skipped. This prevents a no-op autoplan on a stale head from erasing a valid plan state for the current head.
Stale apply result : staleApplyResultForCurrentPull returns true whenever the result set contains any apply-command result and HeadCommit is known. If the current DB entry is for a different head/base, the write is suppressed — apply results from one head cannot overwrite plan state from another.
errStaleCommandHead filtering : Per-project results that carry errStaleCommandHead are dropped before the DB write. If all results are stale-head errors, updateDB returns the current DB status unchanged instead of writing an empty result set.
DirNotExistErr filtering : Results from projects whose directory no longer exists are always dropped before writing; they would never be apply-eligible and would permanently mark the PR as errored.