Skip to content

project.toml

A project.toml declares a CI project: a buildable/testable unit, usually one per module or service. garden discovers project.toml files across the tree and uses them to scope what builds, what tests run, and how changes map to work (test selection).

[project]
name = "api" # project name
language = "go" # primary language (go, typescript, ...)
path = "services/api" # repo-relative root; "" or "." = the file's dir
[test]
timeout = "10m" # optional; Go duration (e.g. 30s, 10m)
images = ["postgres:17-alpine"] # containers the tests need (see below)
# Only for languages with no test inference (see "Running tests"). Go,
# TypeScript and Python ignore this — the runner builds its own invocation
# from the packages selection picked.
command = "cargo test"
[deps]
internal = ["libs/auth"] # other projects this one depends on,
# so a change to a dep selects this project too
[generate]
commands = ["buf generate"] # codegen commands for this project
triggers = ["proto/**"] # globs that should re-run the codegen
[owners]
team = "platform" # owning team (optional metadata)
# Zero or more extra rules: run a command when matching files change.
# Repeat the [[rule]] block for each.
[[rule]]
name = "lint"
match = ["**/*.go"] # globs that trigger this rule
exclude = ["**/gen/**"] # globs to exclude from the match
run = "golangci-lint run"
timeout = "5m" # optional; Go duration

Only [project] is required in practice; the rest are optional. CI runs the relevant projects on each revision and after a change lands; failures surface as inline review threads on the exact failing lines. Check a PR’s checks with garden pr checks <pr>.

For Go, TypeScript and Python, you don’t say how to run the tests. garden reads the import graph, works out which packages your change can affect, and runs exactly those — so [test].command is ignored (garden warns if you set one). Leave it out.

Other languages have no inference backend yet, so they need an explicit [test].command to have a test check at all.

[[rule]] blocks are independent of all this: a rule fires when a changed file matches its globs and runs its own command. Linting, formatting and codegen checks belong there, in any language.

Tests run inside a sealed VM with no network, so a test that starts a container can’t pull one. Declare what it needs and garden fetches it outside the sandbox and loads it in before the tests run:

[test]
images = ["postgres:17-alpine"]

Any registry reference works — a tag, a digest pin, or a private image in your own registry (us-central1-docker.pkg.dev/proj/repo/img:v1).

This is a setting about the environment, not about the tests. It does not make your project command-driven and it does not change test selection: inference still decides which tests run, the images are just there when they do.

If your tests use testcontainers, that’s all you need — DOCKER_HOST is pointed at the in-VM daemon for you, and the Ryuk reaper is disabled (the VM is thrown away after the run, so there is nothing to reap).

Notes:

  • Every image a test starts must be declared. Nothing can be pulled at test time, so an undeclared image fails with “no such image”.
  • Images are pulled from a fixed set of public registries (Docker Hub, ghcr.io, quay.io, gcr.io, *.pkg.dev, ECR Public, MCR, registry.k8s.io) and are subject to a total size limit.
  • A language with no dependency prefetch can’t carry images, so images requires one of the supported languages.