Anvil reads CI pipeline definitions from a .anvil.yml file at the root of your repository. This page documents every field the parser accepts, the values it validates, and the runtime behavior of each.
When a repository has no pipeline configured and a push includes a .anvil.yml, Anvil parses it and creates an enabled pipeline automatically. The whole file is validated before any step runs; on a parse error the run is recorded with an invalid status and no jobs execute.
Minimal example
A pipeline is a list of steps. Each step has a name and a run command. depends_on orders steps and lets independent steps run in parallel.
steps:
- name: test
run: mix test
- name: build
run: mix release
depends_on: [test]
Full example
image: hexpm/elixir:1.20-erlang-29.0.1-debian-trixie-20260518
prepare:
- apt-get update && apt-get install -y build-essential
- mix local.hex --force && mix local.rebar --force
env:
MIX_ENV: test
DATABASE_URL: postgres://postgres:postgres@localhost/myapp_test
steps:
- name: deps
run: mix deps.get
timeout_seconds: 600
- name: compile
run: mix compile --warnings-as-errors
depends_on: [deps]
- name: test
run: mix test
depends_on: [compile]
allow_failure: false
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: myapp_test
artifacts:
- name: test-results
path: _build/test/lib/*/test-results.xml
- name: lint
run: mix format --check-formatted
depends_on: [deps]
paths:
- "lib/**"
- "test/**"
- name: build
run: mix release
depends_on: [test, lint]
runs_on: self-hosted
Top-level fields
| Field | Type | Default | Description |
|---|---|---|---|
steps |
list | — (required) | List of step definitions. Must be a non-empty list. |
image |
string | none | Default container image for every step. A step can override it, or set image: bare to run on the host. |
prepare |
list of strings | none | Shell commands run once after the image starts, before any steps. Requires image to be set; the parser rejects prepare without an image. |
env |
map | {} |
Environment variables available to all steps. Keys and values must both be strings. Step-level env overrides matching keys. |
There is no top-level runs_on. Runner targeting is set per step (see Step fields).
Step fields
A step is a YAML mapping. name and run are required; everything else is optional. The parser rejects unknown keys (see Validation).
| Field | Type | Default | Description |
|---|---|---|---|
name |
string | — (required) | Unique step identifier shown in the UI and referenced by depends_on. Must be non-empty; duplicate names are rejected. |
run |
string | — (required) | Shell command or multi-line script to execute. Must be non-empty. |
depends_on |
list of strings | [] |
Steps that must pass before this step runs. Steps with no shared dependency run in parallel. Every name listed must exist; cycles are rejected. |
image |
string | inherits top-level image |
Container image for this step. Set image: bare to run on the host instead of a container. |
env |
map | inherits top-level env |
Step-level environment variables, merged over the top-level env. Step keys override pipeline keys. Keys and values must both be strings. |
runs_on |
string or list of strings | any runner | Runner label(s) to target. A single string is accepted and treated as a one-element list. Use self-hosted for your own runners. |
timeout_seconds |
positive integer | 600 |
Maximum seconds before the step is killed. |
allow_failure |
boolean | false |
When true, a non-zero exit code does not fail the pipeline. |
condition |
string | none | Expression that must evaluate true for the step to run (see Conditions). |
paths |
list of strings | [] |
Run this step only if a changed file matches one of these glob patterns (see Path filters). |
paths_ignore |
list of strings | [] |
Skip this step if every changed file matches one of these patterns. |
strategy |
map | none | Matrix build configuration. Expands one step into multiple jobs (see Matrix strategies). |
artifacts |
list | [] |
Files to preserve after the step completes (see Artifacts). |
services |
map | none | Sidecar containers to start alongside this step (see Services). |
Conditions
A condition is a boolean expression evaluated against the run context before the step starts. When the expression is false, the step is skipped with reason condition. An empty or absent condition always runs.
The evaluator is a recursive-descent parser with no code evaluation. It supports only the operators below.
| Operator | Example | Meaning |
|---|---|---|
== |
branch == 'main' |
String equality |
!= |
trigger != 'manual' |
String inequality |
contains |
branch contains 'main' |
Left string contains the right string |
&& |
branch contains 'main' && trigger == 'push' |
Boolean AND |
|| |
branch contains 'main' || branch contains 'develop' |
Boolean OR |
! |
!(trigger == 'manual') |
Boolean NOT |
( ) |
(branch contains 'main' || branch contains 'develop') && trigger == 'push' |
Grouping |
String literals use single or double quotes. The literals true and false are also recognized. An unknown identifier resolves to the empty string, and an empty string is treated as false.
The context exposes these variables:
| Variable | Value |
|---|---|
branch |
The ref of the run. For push-triggered runs this is the full ref, e.g. refs/heads/main. For manual runs it is the bare branch name, e.g. main. See the note below. |
trigger |
What started the run: push, manual, or pull_request. |
event_type |
Alias of trigger. |
status |
Fixed to success while conditions are evaluated. Steps are gated before they run, so prior-step status is not yet known. |
Because branch holds the full refs/heads/<name> form on push and the bare <name> form on manual runs, an == comparison that works for one trigger can miss the other. Use contains to match a branch name regardless of trigger:
steps:
- name: deploy
run: mix deploy
depends_on: [test]
condition: "branch contains 'main' && trigger == 'push'"
Path filters
paths and paths_ignore skip a step based on which files changed in the push. When a step is skipped this way, its skip reason is path_filter. If the changed-file list is unavailable (for example a new branch with no parent, or a diff that fails), path filters are treated as satisfied and the step runs.
A step runs when at least one changed file matches a paths pattern, unless every changed file also matches paths_ignore.
steps:
- name: lint-frontend
run: npm run lint
paths:
- "assets/**"
paths_ignore:
- "assets/vendor/**"
Matrix strategies
strategy.matrix expands one step into several jobs, one per combination of the listed dimension values. Each value of every dimension is combined with every value of every other dimension. Reference a dimension in run with ${{ matrix.<dimension> }}; Anvil substitutes the value before the command runs.
steps:
- name: test
run: mix test
strategy:
matrix:
elixir: ["1.16", "1.17"]
otp: ["26", "27"]
The example above produces four jobs. Each expanded job is named after the base name with its values appended, for example test [1.16, 26]. A depends_on that names a matrix step is rewritten to wait on every expanded combination of that step.
exclude removes specific combinations from the product; include either adds extra keys to combinations that already match or appends a new standalone combination:
steps:
- name: test
run: mix test
strategy:
matrix:
os: [ubuntu, macos]
elixir: ["1.16", "1.17"]
exclude:
- os: macos
elixir: "1.16"
include:
- os: ubuntu
elixir: "1.18"
Expansion is capped at 256 combinations. A configuration that would exceed the cap is rejected at parse time.
Artifacts
Artifact specs preserve build outputs for download after the run. Each entry is a mapping with name and path.
artifacts:
- name: test-results
path: _build/test/lib/*/test-results.xml
- name: coverage
path: cover/
| Field | Type | Description |
|---|---|---|
name |
string | Display name for the artifact. Must be non-empty. |
path |
string | File path or glob pattern relative to the workspace root. Must be non-empty. |
The parser keeps only name and path from each spec; any other key (including expire_in) is rejected as unknown. Artifact retention is fixed and not configurable per step.
Services
Services are containers started as sidecars before a step runs and stopped after it exits. Use them for test databases, message queues, and similar dependencies.
services is a mapping keyed by service name. Each value is a mapping; image is required and the rest are optional.
steps:
- name: test
run: mix test
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: myapp_test
redis:
image: redis:7
| Field | Type | Description |
|---|---|---|
image |
string | Container image. Required; must be non-empty. |
env |
map | Environment variables for the service container. |
ports |
list of strings | Port mappings in host:container form, e.g. "5432:5432". |
port |
integer | A single host port to publish. Use instead of ports for one port. |
command |
string or list of strings | Overrides the image’s default command. A string is split on whitespace; a list is passed verbatim. |
For each declared service, the step environment receives <NAME>_HOST (always 127.0.0.1) and <NAME>_PORT. The port is taken from an explicit port, the first entry of ports, or a well-known default for recognized images (postgres 5432, mysql/mariadb 3306, redis 6379, memcached 11211, mongo/mongodb 27017, elasticsearch 9200, rabbitmq 5672, minio 9000). Custom env keys are added to the step environment as well. Names are upper-cased, so a service named postgres exposes POSTGRES_HOST and POSTGRES_PORT.
Unknown keys on a service (anything other than image, env, ports, port, command) are rejected.
Validation
The parser validates the whole file before any step runs and reports all errors at once. It rejects:
- Missing, non-list, or empty
steps. - Steps missing
nameorrun, or with empty values for them. - Wrong types (for example a string where a list is required).
depends_onreferencing a step that does not exist.- Dependency cycles.
- Duplicate step names.
preparewithout a top-levelimage.- A
conditionthat is present but not a non-empty string. - A service without an
image, or with an unknown key. - A matrix that would expand to more than 256 combinations.
- Unknown keys, with a “did you mean?” suggestion based on edit distance.
For example, timout_seconds produces:
step 'test': unknown key 'timout_seconds' (did you mean 'timeout_seconds'?)
Accepted but not yet implemented
Two step fields are validated structurally but have no runtime effect. They are accepted so existing files keep parsing, and the run is flagged with a warning in the UI.
| Field | Status |
|---|---|
cache |
Validated (requires key and a non-empty paths list), but does not cache anything yet. |
coverage |
Validated (requires a reports list), but does not collect coverage yet. |
CI secrets
Secrets are stored encrypted and injected as environment variables at run time. They are never written to logs. Manage them with the CLI:
anvil ci set-secret --name API_KEY --value s3cr3t
anvil ci secrets # list names only
anvil ci delete-secret --name API_KEY
Pass --env <environment> to set-secret to scope a secret to a specific environment, and --repo <org/repo> to target a repository other than the one inferred from your current directory.
Reference a secret in a step with ordinary shell syntax:
steps:
- name: publish
run: mix hex.publish --yes
env:
HEX_API_KEY: $API_KEY