Git, CI/CD & GitOpsCIII · Infrastructure Repository Anti-PatternsAntiPatterns
Generated files in the repository — why build output does not belong in Git
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
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 everyinit. - 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/excludeor 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-filesfrompre-commit. Blocks files larger than a configurable threshold from being added.- A custom hook that runs
git check-ignoreon staged paths and rejects any path the.gitignorecovers.
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
.gitignoreis the first commit. Noterraform init, nonpm install, nocargo buildbefore the ignore rules are in place.- Pre-commit enforces the discipline.
check-added-large-filesand agit check-ignorehook reject accidental additions. - Generated files that must be tracked use LFS. A
.gitattributesrule withfilter=lfskeeps the working tree honest about exceptions. - 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
.envrcand.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
Q1. A team has committed `dist/` for six months and now adds `dist/` to .gitignore. Has the directory been removed from the repository?
Q2. A pre-commit hook that runs `git check-ignore` on staged paths prevents accidental commits of files the .gitignore lists.
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.
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.