A production-grade Continuous Integration (CI) pull-request quality gate for Terraform monorepos. Every PR must pass 6 blocking gates before merge, and the results are posted back to the PR as a single sticky comment (updated in place, never spammed):
| Gate | Tool | What it blocks |
|---|---|---|
| 🎨 Format | terraform fmt -check |
unformatted code |
| 🧱 Validate | terraform init -backend=false + validate per workspace |
broken config, schema errors |
| 🧪 Unit tests | terraform test per module (mock providers, offline) |
behavioural regressions |
| 🔍 Lint | TFLint (curated root .tflint.hcl) |
provider misuse, dead code |
| 🛡️ Security | Checkov (curated .checkov.yaml) |
insecure resources |
| 🔑 Secret scanning | Gitleaks (separate workflow) | leaked credentials |
- No cloud credentials. Everything runs offline (
-backend=false,mock_providerin tests) — a malicious PR can't exfiltrate secrets that aren't there.GITHUB_TOKENis read-only except for the comment step. - Minutes-frugal, but never blind. The Terraform gates are path-filtered;
Gitleaks deliberately is not — a secret can land in any file, so it runs
on every PR and every push to
mainas its own workflow. - One sticky comment. The report finds its previous comment by a hidden marker and PATCHes it, so the PR timeline stays clean. Fork PRs (read-only token) skip the comment instead of failing.
- Reproducible runs. Terraform version from
.terraform-version, TFLint and Checkov pinned, provider plugins cached by.terraform.lock.hclhash. - Failure-friendly reporting. Per-module test counts survive a red job (single-line output), and init failures surface with logs instead of masquerading as test failures.
Copy .github/workflows/ into a Terraform repo laid out as:
modules/<name>/tests/*.tftest.hcl # unit tests (mock providers)
environments/<env>/ # deployable workspaces
.terraform-version .tflint.hcl .checkov.yaml
Open a PR that touches any .tf file — the gates run, and the bot comment
above appears on the PR.
These gates are deliberately credential-free: they prove the code is
correct without ever talking to a cloud account. The stateful half of the
pipeline — terraform plan and apply — runs in a separate lane on
Atlantis, with
Infracost attached:
- The same PR triggers
atlantis plan— a real plan against real state, posted to the PR, with per-workspace state locking so two PRs can't race the same environment. - Infracost posts a cost diff alongside it — a price tag on every PR before anyone approves ("this change adds ~$140/month").
- Only after the gates above are green and the plan is reviewed does a
maintainer comment
atlantis apply— apply happens from the PR, before merge, so what lands onmainis exactly what was applied.
Cloud credentials live only in the Atlantis runner (Workload Identity — no long-lived keys in GitHub), which is why this repo's gates can stay read-only.
