Skip to main content
RunBook Academy

Git, CI/CD & GitOpsCIII · Infrastructure Repository Anti-PatternsAntiPatterns

Generated files in the repository — why build output does not belong in Git

Intermediate⏱ ~22 mingit

What you'll learn

  • Identify the five categories of generated file that frequently appear in IaC repositories
  • Explain why .gitignore affects only untracked files and what that means for files already committed
  • Compose a .gitignore that blocks Terraform, Ansible, and Kubernetes generated outputs at the first commit
  • Configure pre-commit hooks that enforce the .gitignore discipline automatically

Prerequisites

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

A working tree should contain files a human authored and files a tool needs to be runnable. Files a tool produces during a run are generated artifacts, and they do not belong in the repository. A Dockerfile output that the next CI stage consumes is fine; a .terraform/providers/... tree that terraform init will rebuild is not.

Five categories of generated file

The IaC repositories that accumulate generated files almost always have at least one of these:

  • Provider and module caches. .terraform/ in Terraform repos; Ansible collection mirrors. Re-downloaded on every init.
  • Plan and crash artifacts. *.tfplan, *.tfstate.backup, crash.log, terraform.tfstate. The output of a single apply, not a source of truth.
  • Language toolchain output. dist/, build/, node_modules/, target/. The output of a build the next CI step will repeat.
  • Generated manifests. Auto-rendered Helm output, Kustomize output. Reproducible from inputs that should be checked in instead.
  • Local override files. *.local.yaml, .envrc, IDE scratch files. Belong in .git/info/exclude or the user’s global gitignore.
flowchart LR
    A["authored: keep"] --> D["tracked"]
    B["needed: keep"] --> D
    C["generated: ignore"] --> E["working tree only"]
    C --> F["CI cache or rebuild"]

The boundary is sharper than it looks. If a rm followed by the build command reproduces the file, the file is generated. If the file is reproducible, it is not data; it is a rebuild.

Why .gitignore is preventive, not curative

A .gitignore rule affects only untracked files. The moment a dist/ directory is committed, adding dist/ to .gitignore does not remove it; the directory stays in history until a forced rewrite, and every clone still receives it on checkout. The discipline has to apply at the first commit, not after the mistake has compounded.

What the .gitignore should block

A practical .gitignore for an infrastructure monorepo:

.terraform/
*.tfstate
*.tfstate.*
*.tfplan
crash.log
.terraformrc
*.retry
.ansible/
charts/*/charts/
**/*.tgz
node_modules/
dist/
build/
target/
__pycache__/
.venv/
*.local
.envrc
.env

Each entry matches files the tooling will rebuild. The patterns come from github/gitignore, the canonical collection maintained by GitHub for language- and tool-specific templates.

How the discipline is enforced

A .gitignore file is data; the control is the hook that refuses to commit what it lists. Two standard hooks do this:

  • check-added-large-files from pre-commit. Blocks files larger than a configurable threshold from being added.
  • A custom hook that runs git check-ignore on staged paths and rejects any path the .gitignore covers.

The first catches the “git add . and committed 800 MB of node_modules/” failure mode. The second catches the “pattern in .gitignore but file already tracked” failure mode.

git lfs install
git config core.hooksPath .githooks
pre-commit install

The first prepares Git LFS for the kinds of large files that do belong in version control. The second redirects Git to the team’s hook directory. The third installs pre-commit so git commit runs the configured hooks automatically.

Production discipline

  1. .gitignore is the first commit. No terraform init, no npm install, no cargo build before the ignore rules are in place.
  2. Pre-commit enforces the discipline. check-added-large-files and a git check-ignore hook reject accidental additions.
  3. Generated files that must be tracked use LFS. A .gitattributes rule with filter=lfs keeps the working tree honest about exceptions.
  4. Reviews flag generated-file diffs. A reviewer who sees a regenerated dist/ change is reviewing the wrong artifact.

Cross-course references

  • This course, Part CII (LargeRepoPerf) covers the LFS, shallow-clone, and sparse-checkout disciplines.
  • This course, Part XXXV (SecretsInGit) covers the secret-exposure dimension of .envrc and .terraformrc.
  • Terraform for Production Sysadmins Parts IX-XII cover state and plan artifacts that should never reach the working tree.
  • Ansible for Production Sysadmins Part XXXVII covers Ansible retry files and collection caches.

Quiz

Knowledge check · 4 questions

  1. Q1. A team has committed `dist/` for six months and now adds `dist/` to .gitignore. Has the directory been removed from the repository?

  2. Q2. A pre-commit hook that runs `git check-ignore` on staged paths prevents accidental commits of files the .gitignore lists.

  3. Q3. Name the five categories of generated file that frequently appear in IaC repositories, and identify the one that is most often a credential leak.

  4. Q4. Diagnose a repository bloat incident and recommend the .gitignore discipline that prevents it.

    An infrastructure monorepo has grown to 4.2 GB. The first commit was clean; the second included `terraform init` output that was not ignored; the third included `node_modules/` from a generated TypeScript tool; the fourth included `*.tfplan` files from manual planning. The CI clone time has risen from 45 seconds to 22 minutes. A new engineer cannot run `git clone` over the office VPN.

Passing score: 75%. Answers are checked in this browser.