Steps
The exec step
An exec step runs a program: a migration tool, a seeding script, a CLI whose output is the
check. vero starts it as an argv list with no shell, in its own process group, with a capped
record of what it printed, and ends it when the step's deadline fires. There is nothing to quote
and nothing to inject, and a plan that wants a pipeline writes ["sh", "-c", "..."] and owns it.
An exec step runs with your privileges and is not a sandbox: a plan file is executable code,
and a plan from an untrusted source should not be run. vero run --help ends with the same
sentence.
| Key | Type | Meaning |
|---|---|---|
command |
list of strings | argv, templated; a string is refused, there is no shell |
workdir |
string | relative to the plan file |
env |
map of strings | templated; the child also gets PATH, HOME, LANG and nothing else |
stdin |
string | templated |
timeout |
duration | alternative to the step's timeout; not both |
maxOutput |
int | bytes kept of each of stdout and stderr, default 1 MiB |
A complete plan
exec.yaml runs a stand-in migration tool, examples/bin/migrate,
which prints what it did and fails on a bad DATABASE_URL the way a real one would. It needs no
fixture server.
tasks:
- name: migrate
steps:
- exec:
command: ["./bin/migrate", "up"]
workdir: "."
env:
DATABASE_URL: "{{ env.databaseUrl }}"
timeout: 60s
assert:
- exitCode == 0
- stderr not contains "FATAL"
- stdout contains "migrated"
$ bin/vero run examples/exec.yaml
run 01M3ZRX0T4X2QGJGPYVVHFB9P4 exec.yaml --jobs 4 lifecycle isolated
1 passed 0 failed 0 errored 0 timed out 0 skipped 0 blocked 0 not run (1 tasks, 0 requests, 0.0s)
An exec step sends no HTTP request, so the request count stays at zero. vero plan --explain migrate describes the step as exec ./bin/migrate up: no HTTP requests.
What the child sees
The argv is command with every element rendered. workdir is resolved against the plan file's
directory, so "." is the directory the plan sits in, and an absolute path is used as written.
The environment is built from scratch. The child gets PATH, HOME and LANG copied from
vero's own environment when they are set, then the step's env in sorted key order. Nothing else
crosses: a secret in vero's environment reaches the child only if the plan writes it into env,
and then it is scrubbed from the child's output like any other secret. A script step's child also
gets VERO_SCRIPT_TOKEN; an exec step's child does not.
stdin is written to the child when set. Without it, stdin is empty.
Every value in command, env, stdin and workdir takes templates (templates).
Output and its cap
Both streams are captured, each up to maxOutput bytes, 1 MiB by default. Past the cap vero
keeps reading and discards, so a chatty child never blocks on a full pipe. What was cut is
recorded, and an assertion on a cut stream carries the note
stdout was truncated at 1048576 bytes of 2097152; anything past the cap was not looked at, so
"not present" stays distinct from "not looked at". truncated.stdout and truncated.stderr say
the same thing to an assertion.
Deadline and process group
The deadline is the step's timeout, or exec.timeout, 30 s when neither is set. Setting both
is a load error: set the timeout once, on the step or inside exec, not both.
The child starts in a process group of its own. When the deadline fires, vero sends SIGTERM to
the whole group, waits 5 s, then sends SIGKILL to the group. Once the leader has exited, a final
SIGKILL goes to the group so a child the leader started does not outlive it holding a lock. The
step is timed_out, its assertions are not evaluated, and exitCode is nil, never -1:
TIMEOUT sleeper › step 1 exec-sleep.yaml
the step's deadline fired and the process group was killed (SIGTERM sent to process group 330856)
exec ["sh", "-c", "sleep 30"] in .
exit code none (killed or never started), 1001.6ms
stdout
stderr
0 passed 0 failed 0 errored 1 timed out 0 skipped 0 blocked 0 not run (1 tasks, 0 requests, 1.0s)
The same happens on a run timeout or a first SIGINT, with the cause named in place of the
step's deadline. A child killed by a signal vero did not send ends the step errored with the
wait error. Process groups and group signals are Linux and macOS only.
Names in scope
| Name | Value |
|---|---|
exitCode |
the exit code, or nil when the process was killed or never started |
stdout, stderr |
what was kept of each stream, as strings |
truncated.stdout, truncated.stderr |
whether each stream hit maxOutput |
duration.total |
wall time from start to exit; the only phase an exec step has |
env, tasks, run |
as in every step |
duration alone is refused, as on every step kind: name duration.total. The usual string
operators apply to the streams, as in stderr not contains "FATAL" and
stdout matches "^migrated \\d+ files". See assertions.
What a failure shows
The failure block prints the argv as it ran, the working directory, the exit code and the time,
then each stream with its lines prefixed by |, then every assertion that did not hold with the
values it read:
FAIL migrate › step 1 › assert #1 exec-fail.yaml:13
exec ["./bin/migrate", "up"] in .
exit code 1, 9.0ms
stdout
stderr
| FATAL: DATABASE_URL is not set
assert exitCode == 0 failed
exitCode = 1
exec-fail.yaml:13
assert stdout contains "migrated" failed
stdout = ""
exec-fail.yaml:13
0 passed 1 failed 0 errored 0 timed out 0 skipped 0 blocked 0 not run (1 tasks, 0 requests, 0.0s)
A truncated stream is marked on its line, as stdout (truncated: 2097152 bytes written, 1048576 kept). The event log carries the same record as an exec event (vero.events/v1).
Load errors
The one most people hit first is a command written as one string. vero shows the list it thinks you meant:
exec-string.yaml:9: tasks.migrate.steps[0].exec.command: command is a string; exec takes an argv list with no shell: write ["./bin/migrate", "up"], or ["sh", "-c", "./bin/migrate up"] if you need a shell
The others:
| Plan | Load error |
|---|---|
no command |
an exec step needs command, an argv list such as ["./bin/migrate", "up"] |
command: [""] |
the program to run is empty |
timeout on the step and in exec |
set the timeout once, on the step or inside exec, not both |
a negative maxOutput |
maxOutput is a byte count, got -1 |
an env key such as x-y |
environment variable name "x-y" must match ^[A-Za-z_][A-Za-z0-9_]*$ |
repeat on the step |
repeat works on http steps only |
An exec step may sit in a task that holds a resource, and a lifecycle.reset hook may be an exec
step; a reset step with no assertions is held to exitCode == 0.
This page is docs/content/steps/exec.md in the repository.