Terraform Plan File Management#
Atlantis produces .tfplan binary files during the plan phase and must carefully track their lifecycle to prevent stale plans from being applied. Because .tfplan files are untracked by git (and commonly listed in .gitignore), they survive git reset --hard operations β requiring explicit cleanup logic whenever the working directory is updated to a new commit.
File Naming and Path Conventions#
Plan file paths are assembled by helpers in server/core/runtime/runtime.go:
| Function | Purpose |
|---|---|
GetPlanFilename(workspace, projName) | Returns {workspace}.tfplan or {projectName}-{workspace}.tfplan; path slashes in project names are replaced with :: |
GetPlanFileDir(ctx, projectPath) | Returns plan directory, accounting for the optional LocalSharePlanDir separate plan store |
GetPlanFilePath(ctx, projectPath) | Combines the two above into a full path |
GetPlanPullDir(localSharePlanDir, repo, pr) | Returns root directory for all plan files for a given pull request |
When LocalSharePlanDir is not set (default), plan files live alongside source code inside the per-PR clone directory. When a separate plan store is configured, files are stored under {LocalSharePlanDir}/repos/{repo}/{pull_num}/{workspace}/{rel_dir}/.
Stale Plan Cleanup on Working Directory Update#
When a new commit is pushed to a PR, Atlantis updates the existing clone via updateToRef rather than recloning from scratch (a performance optimization introduced in a prior refactor). Since git reset --hard does not remove untracked files, plan files from earlier commits would persist and could be picked up by a subsequent bare atlantis apply.
The fix (merged in PR #6358) added cleanStalePlanFiles, called at the end of updateToRef:
- Co-located plans (default): runs
git clean -f -x -e .terragrunt-cache -- ':(glob)**/*.tfplan'. The-xflag is required because plan files are typically git-ignored..terragrunt-cacheis explicitly excluded from cleanup to preserve cached provider downloads. - Separate plan store (
LocalSharePlanDirβDataDir): callsremoveTfPlanFiles(planWorkspaceDir)to walk and delete.tfplanfiles from the plan store directory for the PR workspace, skipping.terragrunt-cache.
A full reclone (triggered by a base-branch change under merge strategy, or on git errors) also implicitly removes all plan files by wiping the entire working directory, as verified in TestClone_ReCloneOnBaseChangeForMergeStrategy.
Note:
MergeAgain(re-merge on a diverged base without a new head commit) intentionally does not purge plan files β the head commit has not changed, so existing plans remain valid.
Git-Ignore Behavior for .tfplan Files#
Atlantis does not write a repository .gitignore; instead, plan files in user repos are often already covered by the repo's own .gitignore. The test harness models this precisely using .git/info/exclude:
// ignorePlanFiles simulates a repo that git-ignores *.tfplan
func ignorePlanFiles(t *testing.T, cloneDir string) {
os.WriteFile(filepath.Join(cloneDir, ".git/info/exclude"), []byte("*.tfplan\n"), 0600)
}
This is why git clean -x (include ignored files) is necessary β a plain git clean would leave ignored plan files in place.
Cleanup in the Apply Validation Path#
Stale plan removal also happens outside the working directory layer. rejectProjectPlan deletes the plan file on any validation failure (e.g. status check failure, hash mismatch), ensuring that an invalid plan does not linger on disk for a future apply attempt.
Test Helpers#
Tests in server/events/working_dir_test.go verify end-to-end plan cleanup behavior with two small helpers:
createPlanFile(t, path)β creates a dummy.tfplanfile at an arbitrary (potentially nested) path.ignorePlanFiles(t, cloneDir)β writes to.git/info/excludeto simulate a repo whose.gitignoreexcludes plan files.
Key test scenarios:
TestClone_ResetOnWrongCommitβ verifies root and nested.tfplanfiles are deleted after a branch-strategy reset; asserts.terragrunt-cacheplan files are preserved.TestClone_ResetOnWrongCommitWithMergeStrategyβ same assertions under the merge strategy path.TestClone_MasterHasDivergedβ asserts plan files are not deleted byMergeAgain(re-merge on diverged base), only by ref updates that change the PR head commit.TestClone_DoNotReCloneOnBaseChangeForBranchStrategyβ asserts plan files are cleaned even in the branch (non-merge) strategy when the head commit changes.