projects.yml schema¶
projects.yml at the repo root tells the engine what to discover and which hooks
to run. There are three discovery modes. Terragrunt mode is exclusive; the two
plain-Terraform modes can be combined.
The engine's vocabulary is project; in Terragrunt mode a project is exactly a unit, and this site uses the two words interchangeably.
Terragrunt mode¶
terragrunt:
root: live # directory that holds root.hcl
include_dependents: true # re-plan a changed unit's downstream dependents
exclude: # optional globs, relative to root
- "**/scratch/**"
tool: terragrunt # optional
The shipped configuration has no excludes, on purpose: the foundation units are the highest-impact part of the tree, and excluding anything from discovery hides it from the plan, the gates, and the merge gate.
Discovery uses native Terragrunt (terragrunt find with dependencies and the DAG).
A changed unit impacts that unit plus every downstream dependent, in topological
order, so apply runs in the right sequence.
Explicit projects (plain Terraform)¶
The label is <dir>/<name>.
Generated projects (plain Terraform)¶
Every folder with *.tf under an include glob (minus excludes and shared modules)
becomes a project, labelled by its path. Globs support **.
The drift block¶
Scopes or disables the daily drift sweep. With no drift: block, every discovered
unit is swept.
The steps block¶
steps:
post_plan:
- bash "$TFPR_ENGINE_DIR/scripts/conftest-gate.sh"
- CHECKOV_SOFT_FAIL=1 bash "$TFPR_ENGINE_DIR/scripts/checkov-gate.sh"
- bash "$TFPR_ENGINE_DIR/scripts/infracost-gate.sh"
$TFPR_ENGINE_DIR points at the engine checkout the reusable workflow provides. It
defaults to the workspace, so the same paths resolve in vendored mode.
Hooks run at fixed seams around init, plan, and apply. A global steps: applies to
every unit. An individual project may set its own steps:, which fully replaces the
global one (there is no merge). See the hook and step model.
Discovery needs PyYAML
Project selection runs on the hosted runner, which has Python. A steps: or
drift: block requires PyYAML for parsing and the selector fails loudly if it is
missing, rather than silently ignoring your config.