From 1d73f3ad574e65faaa3c14aee4e0244698cc16ca Mon Sep 17 00:00:00 2001 From: "@permadeath.com" Date: Thu, 6 Aug 2026 20:03:48 -0400 Subject: [PATCH] perf: scope tofu validate to the directories that changed Replace the local fmt and validate hooks with upstream pre-commit-terraform. Both work per directory on the files that changed rather than over all nine roots, and validate skips init once .terraform holds modules and providers. A commit touching one root goes from ~25s to ~2-3s. Per-root validation stops at the module boundary, so a local hook validates the env roots that compose modules whenever a module changes. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 11 +++++--- docs/runbook.md | 6 ++-- prek.toml | 73 +++++++++++++++++++++++++++++-------------------- 3 files changed, 54 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 13864c1..32447fd 100644 --- a/README.md +++ b/README.md @@ -38,10 +38,13 @@ Hooks are managed by [prek](https://prek.j178.dev/) and configured in uv tool install prek prek install --prepare-hooks -They then run on every commit; `prek run --all-files` runs them by hand. The -hooks are also where formatting and validation live: `tofu-fmt` rewrites -unformatted files, and `tofu-validate` init-and-validates every root without -touching AWS. There are no separate scripts for either. +They then run on every commit; `prek run --all-files` runs them by hand. +Formatting and validation live here rather than in scripts. Both come from +[pre-commit-terraform](https://github.com/antonbabenko/pre-commit-terraform): +`terraform_fmt` rewrites unformatted files, and `terraform_validate` +init-and-validates the roots holding the files that changed, without touching +AWS. A local `tofu-validate-callers` hook adds back the one thing per-root +validation misses — a module edit that breaks the env roots composing it. ## Layout diff --git a/docs/runbook.md b/docs/runbook.md index 4e6387d..71fed9a 100644 --- a/docs/runbook.md +++ b/docs/runbook.md @@ -98,9 +98,9 @@ Applying the **saved plan** rather than re-planning means what you read is what runs. The plan file is gitignored (`*.tfplan`) because a plan can embed resolved variable values, including secrets passed on the command line. -Formatting is prek's job — the `tofu fmt` hook rewrites unformatted files on -every commit (a commit that needed formatting fails; stage the result and -commit again); `prek run --all-files` runs it by hand. +Formatting is prek's job — the `terraform_fmt` hook runs `tofu fmt` over the +files that changed on every commit (a commit that needed formatting fails; +stage the result and commit again); `prek run --all-files` runs it by hand. ## After the first apply diff --git a/prek.toml b/prek.toml index 8083ce8..08d869e 100644 --- a/prek.toml +++ b/prek.toml @@ -22,47 +22,62 @@ rev = "v0.11.0.1" # info-level notes do not gate. hooks = [{ id = "shellcheck", args = ["-x", "--severity=warning"] }] +# fmt and validate come from upstream. Both work per directory on the files +# that changed, so a commit touching one module no longer runs over all nine +# roots. tofu and terraform format and validate identically; --tf-path names +# the binary so neither hook depends on which of the two is on PATH. [[repos]] -repo = "local" +repo = "https://github.com/antonbabenko/pre-commit-terraform" +rev = "v1.99.5" -# tofu and terraform format identically; tofu is what is installed here. # Rewrites files in place; a commit that needed formatting fails so the # result can be staged and committed again. [[repos.hooks]] -id = "tofu-fmt" -name = "tofu fmt" -language = "system" -entry = "tofu fmt -recursive ." -pass_filenames = false -files = '(\.tf|\.tfvars)$' +id = "terraform_fmt" +args = ["--hook-config=--tf-path=tofu"] -# Syntax- and schema-check every root without touching AWS. `init -# -backend=false` needs no state bucket and no credentials, but it does -# download providers, so the first run needs the network; the shared plugin -# cache keeps that to one copy of the AWS provider instead of one per root. -# What it catches: HCL errors, bad references, unknown or missing arguments. -# What it does not: whether AWS accepts the combination - only a real plan does. +# Syntax- and schema-check the roots holding changed files, without touching +# AWS. What it catches: HCL errors, bad references, unknown or missing +# arguments. What it does not: whether AWS accepts the combination - only a +# real plan does. # -# Each root gets its own scratch TF_DATA_DIR. Without that, `init` reuses the -# .terraform/terraform.tfstate a real init left behind, which still names the -# S3 backend - so -backend=false is ignored and the hook fails with "No valid -# credential sources found" on exactly the machines that have run an apply. -# The scratch dir also keeps the hook from disturbing a working .terraform. +# The hook inits and validates in place and skips `init` once .terraform holds +# both modules and providers. That keeps repeat runs to validate alone, and on +# a machine that has applied it means never re-running the init that reads the +# S3 backend recorded in .terraform/terraform.tfstate and fails for want of +# credentials - `-backend=false` does not prevent that. +# +# The plugin cache path is relative because the hook runs from inside each +# root, and every root here is two deep: modules// and envs//. It +# buys one shared copy of the AWS provider, symlinked, instead of a gigabyte +# per root. A root at any other depth would quietly get its own cache. +[[repos.hooks]] +id = "terraform_validate" +args = ["--hook-config=--tf-path=tofu"] +env = { TF_PLUGIN_CACHE_DIR = "../../.work/plugin-cache" } + +[[repos]] +repo = "local" + +# Validating only the directory that changed stops at the module boundary, so +# an edit that breaks a module's callers - a renamed variable, a dropped +# output - would pass. The env roots that compose modules are few and quick, +# so any module change validates them too. Env roots that use no modules +# (bootstrap) are skipped; ones whose own files changed are covered by the +# hook above and validate twice, which costs a couple of seconds. [[repos.hooks]] -id = "tofu-validate" -name = "tofu validate" +id = "tofu-validate-callers" +name = "tofu validate (module callers)" language = "system" pass_filenames = false -files = '(\.tf|\.tfvars)$' +files = '^modules/.*(\.tf|\.tfvars)$' +env = { TF_PLUGIN_CACHE_DIR = ".work/plugin-cache" } entry = '''bash -c ' set -euo pipefail -export TF_PLUGIN_CACHE_DIR="${TF_PLUGIN_CACHE_DIR:-$PWD/.work/plugin-cache}" -mkdir -p "$TF_PLUGIN_CACHE_DIR" -for dir in modules/*/ envs/*/; do - ls "$dir"*.tf >/dev/null 2>&1 || continue +for dir in envs/*/; do + grep -rqs "source *= *\"\.\./\.\./modules/" "$dir" || continue echo "=== $dir" - export TF_DATA_DIR="$PWD/.work/validate/${dir//\//-}" - mkdir -p "$TF_DATA_DIR" - tofu -chdir="$dir" init -backend=false -input=false >/dev/null + [ -d "$dir/.terraform/providers" ] || + tofu -chdir="$dir" init -backend=false -input=false >/dev/null tofu -chdir="$dir" validate done' ''' -- 2.51.2