Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Blame · docs/reference/workflow-reference.md

Line attribution on main · 1000 line(s)

70b70264
Keenan Nemetz
15d ago
1 # Workflow reference
2
3 This page lists every field Vole accepts in `.vole/workflows/*.yaml`, with its
4 defaults and limits. For a task-oriented introduction, see [Workflows](../workflows.md).
5 For `.vole/images` definitions, see [Image reference](image-reference.md).
6
7 Unknown keys are rejected. A workflow is validated when it is pushed and again by
8 `vole workflow check`, so a typo fails before a job is queued.
9
10 ## File location and limits
11
12 | Item | Value |
13 |---|---|
14 | Location | Directly under `.vole/workflows/`; subdirectories are rejected |
15 | Extension | `.yaml` or `.yml` |
16 | Maximum file size | 256 KiB |
17 | Schema version | `1` |
18 | Maximum jobs per workflow | 50 |
19 | Maximum steps per job | 100 |
20 | Maximum artifact declarations per job | 32 |
6bfc4294
Keenan Nemetz
12d ago
21 | Maximum artifact files per run | 256 |
22 | Maximum artifact bytes per run | 4 GiB |
23 | Maximum attempt log | 32 MiB (then truncated with a note) |
70b70264
Keenan Nemetz
15d ago
24 | Maximum `runs-on` labels per job | 8 |
25 | Maximum step command size | 64 KiB |
4ac42680
Keenan Nemetz
7d ago
26 | Maximum secret value | 128 KiB (`NAME=value`, the kernel's environment limit) |
27 | Maximum secret name | 64 bytes |
1cde8b9e
Keenan Nemetz
8d ago
28 | Maximum display name | 80 bytes |
29 | Maximum description | 500 bytes |
eb07ff56
Keenan Nemetz
8d ago
30 | Maximum path filters | 20 per list |
727df386
Keenan Nemetz
8d ago
31 | Maximum accounts per event filter | 10 per list |
32 | Maximum mentions read from one body | 32 |
eb07ff56
Keenan Nemetz
8d ago
33 | Maximum schedules | 5 |
34 | Maximum manual inputs | 10 |
35 | Maximum event payload | 64 KiB |
e7aa0785
Keenan Nemetz
6d ago
36 | Default step timeout | 10 minutes, when the job has no budget |
37 | Maximum step timeout | 4 hours |
38 | Maximum job timeout | 4 hours |
70b70264
Keenan Nemetz
15d ago
39
40 ## Top-level fields
41
42 | Field | Type | Required | Default | Meaning |
43 |---|---|---|---|---|
44 | `version` | integer | yes | — | Schema version. Must be `1`. |
57dc79af
Keenan Nemetz
6d ago
45 | `name` | string | no | the workflow file's base name | Display name on the run page. |
1cde8b9e
Keenan Nemetz
8d ago
46 | `description` | string | no | none | One-line summary of what the workflow is for. |
eb07ff56
Keenan Nemetz
8d ago
47 | `on` | mapping | no | — | Trigger configuration. |
48 | `on.push.branches` | list of strings | no | `["*"]` for a workflow with no other trigger | Branch patterns that trigger the workflow. |
70b70264
Keenan Nemetz
15d ago
49 | `on.push.tags` | list of strings | no | none | Tag patterns that trigger the workflow. |
eb07ff56
Keenan Nemetz
8d ago
50 | `on.push.paths` | list of strings | no | none | Path patterns a push must touch. |
51 | `on.push.paths-ignore` | list of strings | no | none | Path patterns a push must not change. |
52 | `on.change-request` | mapping or empty | no | none | Change-request actions that trigger the workflow. |
53 | `on.issue` | mapping or empty | no | none | Issue actions that trigger the workflow. |
54 | `on.comment` | mapping or empty | no | none | Comment actions that trigger the workflow. |
727df386
Keenan Nemetz
8d ago
55 | `on.change-request.mentioned` | string or list | no | none | Body must mention one of these accounts. |
56 | `on.change-request.author` | mapping, string or list | no | none | Sender name allowlist and/or minimum role. |
57 | `on.issue.mentioned` | string or list | no | none | Body must mention one of these accounts. |
58 | `on.issue.assigned-to` | string or list | no | none | This event must assign one of these accounts. |
59 | `on.issue.author` | mapping, string or list | no | none | Sender name allowlist and/or minimum role. |
60 | `on.comment.mentioned` | string or list | no | none | Body must mention one of these accounts. |
61 | `on.comment.replied-to` | string or list | no | none | Parent comment must be by one of these accounts. |
62 | `on.comment.author` | mapping, string or list | no | none | Sender name allowlist and/or minimum role. |
eb07ff56
Keenan Nemetz
8d ago
63 | `on.schedule` | list of mappings | no | none | Cron schedules, in UTC. |
64 | `on.workflow-dispatch` | mapping or empty | no | none | A manual run, optionally with typed inputs. |
70b70264
Keenan Nemetz
15d ago
65 | `jobs` | mapping | yes | — | One or more jobs, keyed by job ID. |
66
67 ### `version`
68
69 The only supported value is `1`:
70
71 ```yaml
72 version: 1
73 ```
74
1cde8b9e
Keenan Nemetz
8d ago
75 ### `name`
76
77 A display name for the workflow. The file path stays the identity — it is what a
78 run records and what `vole workflow run` selects — while `name` is what a reader
57dc79af
Keenan Nemetz
6d ago
79 sees on a run page:
1cde8b9e
Keenan Nemetz
8d ago
80
81 ```yaml
82 name: Verify main
83 ```
84
85 When `name` is absent or empty, the workflow is shown by its file's base name, so
86 `.vole/workflows/verify.yaml` is shown as `verify`.
87
88 The name is trimmed and must be one line of text, at most 80 bytes. It is display
89 metadata; two workflows may share a name.
90
91 ### `description`
92
57dc79af
Keenan Nemetz
6d ago
93 A one-line summary of what the workflow is for, shown under its name on each
94 run page:
1cde8b9e
Keenan Nemetz
8d ago
95
96 ```yaml
97 description: Build, test and lint every push to main.
98 ```
99
100 The description is trimmed and must be one line of text, at most 500 bytes. It is
101 optional; an absent description shows nothing.
102
70b70264
Keenan Nemetz
15d ago
103 ### `on.push.branches`
104
105 A list of branch patterns. The default depends on what else is present:
106
eb07ff56
Keenan Nemetz
8d ago
107 - A workflow with no `on` trigger at all is an ordinary push ci and runs on every
108 branch.
70b70264
Keenan Nemetz
15d ago
109 - `branches` present: only matching branches trigger it.
110 - `tags` present without `branches`: the workflow runs only on matching tags.
eb07ff56
Keenan Nemetz
8d ago
111 - A workflow with only event triggers (`change_request`, `issue`, `comment`,
112 `schedule`, `workflow_dispatch`) does not run on a branch push.
113 - `push:` with neither `branches` nor `tags` runs on every branch.
70b70264
Keenan Nemetz
15d ago
114
115 Patterns use `path.Match` semantics with one special case: a lone `*` matches every
116 branch, including names containing `/`. Other patterns keep the one-segment rule,
117 so `release/*` does not match `release/1.0/hotfix`.
118
119 Patterns may contain letters, digits, `*`, `?`, `[`, `]`, `!`, `.`, `_`, `/` and
120 `-`. Empty patterns and patterns containing `..` are rejected.
121
eb07ff56
Keenan Nemetz
8d ago
122 A leading `!` excludes what an earlier pattern included. The last matching pattern
123 wins, so `["*", "!main"]` runs everywhere except `main`:
124
70b70264
Keenan Nemetz
15d ago
125 ```yaml
126 on:
127 push:
128 branches: [main, "release/*"]
129 ```
130
eb07ff56
Keenan Nemetz
8d ago
131 ```yaml
132 on:
133 push:
134 branches: ["*", "!main"]
135 ```
136
70b70264
Keenan Nemetz
15d ago
137 An explicitly empty list means no branches trigger the workflow.
138
139 ### `on.push.tags`
140
141 A list of tag patterns. Tags have no default: a workflow runs on a tag only when
142 `tags:` is present and the tag matches.
143
144 ```yaml
145 on:
146 push:
147 tags: ["v*"]
148 ```
149
eb07ff56
Keenan Nemetz
8d ago
150 Patterns follow the same rules as branch patterns, including `!` exclusion. A
151 workflow with `tags` and no `branches` runs on tags only. A workflow with both runs
152 on matching branches and matching tags.
153
154 ### `on.push.paths`
155
156 A push runs only when at least one changed path matches one of these patterns.
157 Patterns are relative to the repository root. `*` stays inside one path segment;
158 `**` spans zero or more segments, so `docs/**` covers `docs/` and everything under
159 it. At most 20 patterns may be declared.
160
161 ```yaml
162 on:
163 push:
164 paths: ["src/**", "cmd/**", "go.mod"]
165 ```
166
167 An empty pattern, an absolute path, or a pattern containing `..` is rejected. A
168 push that changes no files does not match a workflow with a path filter.
169
170 ### `on.push.paths-ignore`
171
172 Paths that do not trigger the workflow. An ignored path is removed before the
173 allow list is consulted, so an ignored file cannot make a workflow run:
174
175 ```yaml
176 on:
177 push:
178 paths-ignore: ["docs/**", "**/*.md"]
179 ```
180
181 `paths` and `paths-ignore` may be combined. When every changed path is ignored,
182 nothing runs. Both filters are evaluated against the range the push changed; a new
183 branch or tag has no previous tree, so every file counts as changed.
184
185 ### `on.change-request`
186
187 Runs when a change request changes. The run is checked out at the request's base
188 branch tip, not at the proposed head: an event run must not be a way to execute an
189 unapproved contributor commit. The proposed head and the changed files are in the
190 event payload.
191
192 ```yaml
193 on:
194 change-request:
195 types: [opened, updated, ready]
196 ```
197
198 | Action | Meaning |
199 |---|---|
200 | `opened` | A request was created. |
201 | `updated` | Its head, base, title, description, or followed branch changed. |
202 | `ready` | The required approvals for the exact base and head were reached. |
203 | `closed` | It was closed without landing. |
204 | `merged` | It landed. |
205
206 The default is `[opened, updated, ready]`. An empty value means the default:
207 `change-request:` and `change-request: {}` are the same.
208
209 ### `on.issue`
210
211 Runs when an issue changes, at the default branch tip.
212
213 ```yaml
214 on:
215 issue:
216 types: [opened, closed]
217 ```
218
727df386
Keenan Nemetz
8d ago
219 Actions are `opened`, `edited`, `assigned`, `closed`, and `reopened`. The default is `[opened]`.
220 An empty value means the default. `assigned` is queued when an edit adds an
221 assignee. An issue opened with an assignee already set queues `opened`, and its
222 `assignees_added` list makes that an assignment for `assigned-to`.
eb07ff56
Keenan Nemetz
8d ago
223
224 ### `on.comment`
225
226 Runs when a comment on an issue or a change request is created, edited, or deleted.
227 The payload names the comment and the issue or change request it belongs to.
228
229 ```yaml
230 on:
231 comment:
232 types: [created]
233 ```
234
235 Actions are `created`, `edited`, and `deleted`. The default is `[created]`. An
236 empty value means the default.
237
727df386
Keenan Nemetz
8d ago
238 ### Event filters
239
240 `on.change-request`, `on.issue`, and `on.comment` accept filter fields beside
241 `types`. They select within the event's action list; they do not change which
242 actions the trigger has to declare.
243
244 ```yaml
245 on:
246 issue:
247 types: [opened, assigned]
248 assigned-to: [coding-agent]
249 comment:
250 types: [created]
251 mentioned: [coding-agent]
252 replied-to: [coding-agent]
253 author:
254 name: [keenan]
255 role: maintain
256 ```
257
258 | Field | Availability | Meaning |
259 |---|---|---|
260 | `mentioned` | change request, issue, comment | The event body names one of these accounts. |
261 | `replied-to` | comment | The comment answers a comment written by one of these accounts. |
262 | `assigned-to` | issue | This event added one of these accounts as an assignee, including assignees set when the issue was opened. |
263 | `author.name` | change request, issue, comment | The account that caused the event is one of these. |
264 | `author.role` | change request, issue, comment | The account that caused the event holds at least this repository role. |
265
266 The account filters (`mentioned`, `replied-to`, `assigned-to`, and `author.name`)
267 take one account or a list of accounts, at most 10. Names follow the account-name
268 rule: 2–32 characters, lowercase letters, digits, `-` and `_`. A leading `@` is
269 accepted and removed. An explicitly empty list is rejected because a filter that
270 can never match has no visible symptom.
271
272 `author` may also be written as one account or a list, which is shorthand for
273 `author.name`:
274
275 ```yaml
276 author: keenan
277 author: [keenan, wilbur]
278 ```
279
280 `author.name`, `author.role`, and `assigned-to` must match when present.
281 `mentioned` and `replied-to` are alternatives: a comment qualifies if it mentions
282 an account or answers an account, so a workflow that should run for either can
283 declare both. A comment is never treated as addressed to its own author, which
284 keeps an agent's reply from waking itself.
285
286 `author.role` is the ACL filter. Its value is one role — `read`, `contribute`,
287 `maintain`, or `admin` — and it means "at least this role" for the effective
288 repository role of the account that caused the event. A `maintain` filter admits
289 maintainers and admins and refuses contributors and readers. `none` is rejected
290 because it would filter nothing. Use it when the rule is a capability rather than
291 a list of names:
292
293 ```yaml
294 on:
295 comment:
296 types: [created]
297 mentioned: [coding-agent]
298 author:
299 role: maintain
300 ```
301
302 The role is resolved when the event is queued, from the repository's access rules
303 and the instance-wide role attached to them. Changing the rules later does not
304 change a run that is already queued.
305
306 `author.role` is a policy gate on the recorded sender, not a substitute for
307 authentication. Contributors reach the metadata branch through the browser, which
308 records their session account, and writing that branch directly already requires
309 maintain. An above-maintain role is therefore not protection against a malicious
310 maintainer with direct push access.
311
312 The mention syntax is `@name`. Mentions inside inline code or a fenced code block
313 are ignored, as is the `@` in an email address or a URL. A body contributes at
314 most 32 distinct mentions to the event.
315
316 A filter is evaluated when the event is queued. `author` names the person whose
317 write caused the event: the comment author, the issue author or editor, or the
318 maintainer who assigned the issue.
319
eb07ff56
Keenan Nemetz
8d ago
320 ### `on.schedule`
321
322 Cron schedules in UTC. A schedule queues the workflow at the default branch tip.
323 The field syntax is five fields — minute, hour, day of month, month, day of week —
324 with `*`, lists, ranges, and a step on `*` or a range. Names and `@shorthands` are
325 not accepted.
326
327 ```yaml
328 on:
329 schedule:
330 - cron: "0 3 * * 1-5"
331 ```
332
333 At most 5 schedules may be declared. The run is queued once per matching minute;
334 a server that was not running for a slot does not replay it later.
335
336 ### `on.workflow-dispatch`
337
57dc79af
Keenan Nemetz
6d ago
338 Marks a workflow as runnable by hand. The CLI has `vole workflow dispatch`.
eb07ff56
Keenan Nemetz
8d ago
339
340 ```yaml
341 on:
342 workflow-dispatch:
343 inputs:
344 environment:
345 description: Where to deploy
346 type: choice
347 required: true
348 default: staging
349 options: [staging, production]
350 dry-run:
351 type: boolean
352 default: "true"
353 ```
354
355 | Input field | Meaning |
356 |---|---|
357 | `description` | One line shown beside the field. |
358 | `type` | `string`, `boolean`, `number`, or `choice`. Default `string`. |
359 | `required` | The run is refused when the input has no value and no default. |
360 | `default` | Value used when the input is omitted. |
361 | `options` | The allowed values for `choice`. |
362
363 At most 10 inputs may be declared. A value must be one line and at most 4 KiB.
364 `choice` inputs need at least one option, and the default must be one of them.
365 A manual run is available to maintainers and instance admins, and the selected
366 branch must exist.
367
368 ### Event data available to steps
369
370 Every run has a `trigger_kind` (`push`, `change_request`, `issue`, `comment`,
371 `schedule`, or `workflow_dispatch`) and an action. A run started by a non-push
372 trigger also gets three variables:
373
374 | Variable | Value |
375 |---|---|
376 | `VOLE_EVENT` | The trigger kind, such as `comment`. |
377 | `VOLE_EVENT_ACTION` | The action, such as `created`. |
378 | `VOLE_EVENT_PATH` | Path to a JSON file, `/run/vole/event.json`. |
379
380 The JSON is pinned when the run is queued. It is data: read it with `jq` or a
381 program, never by interpolating it into a shell command. Common fields are
382 `event`, `action`, `repository`, `ref`, `branch`, `commit`, `sender`, and for a
383 change request `base_sha`, `head_sha`, and `changed_files`. The entity is under
384 `issue`, `change_request`, or `comment`; a manual run puts its resolved values
727df386
Keenan Nemetz
8d ago
385 under `inputs`. An issue or change request carries `mentions` in its entity. An
386 issue also carries `assignees_added` and `assignees_removed`. A comment carries
387 `mentions`, `reply_to_author`, and `reply_to`. Long bodies are clipped at 16 KiB
388 and the payload marks that with `body_truncated`.
eb07ff56
Keenan Nemetz
8d ago
389
390 These variables are set by the runner and cannot be replaced by a workflow's
391 `env:`, the same rule as `CI` and `HOME`.
70b70264
Keenan Nemetz
15d ago
392
393 ### `jobs`
394
395 A mapping of job IDs to job definitions. Job IDs must match:
396
397 ```text
398 ^[a-z][a-z0-9_-]{0,31}$
399 ```
400
401 That is 1–32 characters: lowercase letters, digits, `_` and `-`, starting with a
402 letter. At least one job is required.
403
404 ## Job fields
405
406 | Field | Type | Required | Default | Meaning |
407 |---|---|---|---|---|
57dc79af
Keenan Nemetz
6d ago
408 | `name` | string | no | the job ID | Display name on the run page. |
1cde8b9e
Keenan Nemetz
8d ago
409 | `description` | string | no | none | One-line summary of what the job does. |
70b70264
Keenan Nemetz
15d ago
410 | `image` | string | one of `image` / `image-file` | — | Registry image the job runs in. |
411 | `image-file` | string | one of `image` / `image-file` | — | Name of a definition under `.vole/images/`. |
412 | `runs-on` | string or list of strings | no | any runner | Runner labels the job requires. |
d7958024
Keenan Nemetz
7d ago
413 | `network` | `none` or `bridge` | no | the runner's setting | Whether the job's container has a network. |
c1929d2f
Keenan Nemetz
5d ago
414 | `services` | mapping | no | none | Containers started beside the job, reached by name. |
4ac42680
Keenan Nemetz
7d ago
415 | `secrets` | string or list of strings | no | none | Repository secrets the job reads, by name. Each name is also the variable a step finds it in. |
ac541b26
Keenan Nemetz
6d ago
416 | `acts-as` | account name | no | none | A bot account the job acts as, with a credential for that run. |
3b39a84c
Keenan Nemetz
14d ago
417 | `env` | mapping | no | none | Environment every step of the job runs with. |
e7aa0785
Keenan Nemetz
6d ago
418 | `timeout` | duration string | no | none | Wall clock the job's steps share. |
70b70264
Keenan Nemetz
15d ago
419 | `steps` | list | yes | — | Commands the job runs. At least one is required. |
420 | `artifacts` | list | no | none | Files the job publishes. |
421 | `pages` | mapping | no | none | Directory published as the repository's website. |
422
423 Exactly one of `image` and `image-file` must be present. Declaring both, or
424 neither, is an error.
425
1cde8b9e
Keenan Nemetz
8d ago
426 ### `name`
427
57dc79af
Keenan Nemetz
6d ago
428 A display name for the job, shown on the run page in place of the job ID:
1cde8b9e
Keenan Nemetz
8d ago
429
430 ```yaml
431 jobs:
432 test:
433 name: Unit tests
434 ```
435
436 When `name` is absent or empty, the job is shown by its ID. The ID stays the key
437 that a run, a lease and `vole workflow run --job` use.
438
439 The name is trimmed and must be one line of text, at most 80 bytes.
440
441 ### `description`
442
443 An optional one-line summary of what the job does:
444
445 ```yaml
446 jobs:
447 test:
448 name: Unit tests
449 description: Runs the Go tests against the pinned toolchain.
450 ```
451
452 The description is trimmed and must be one line of text, at most 500 bytes.
453
70b70264
Keenan Nemetz
15d ago
454 ### `image`
455
456 An OCI image reference used exactly as written:
457
458 ```yaml
459 jobs:
460 build:
461 image: golang:1.27-bookworm@sha256:...
462 ```
463
464 The runner pulls and runs this image. Pin it by digest when reproducibility
465 matters; a mutable tag can change between runs.
466
467 References are limited to 255 bytes and must start with a letter or digit. They may
468 contain letters, digits, `.`, `_`, `/`, `:`, `@`, `+`, `~` and `-`. A leading `-`,
469 `..`, `://`, or a `file:` prefix is rejected. `file:` is reserved for
470 `image-file:`.
471
472 ### `image-file`
473
474 The name of a definition under `.vole/images/`:
475
476 ```yaml
477 jobs:
478 build:
479 image-file: go
480 ```
481
482 `image-file: go` resolves `.vole/images/go.yaml`. The `.yaml` or `.yml` extension
483 is optional in the reference; `image-file: go.yaml` is also accepted.
484
485 Names are 1–64 characters: lowercase letters, digits, `.`, `_` and `-`, starting
486 with a letter or digit. A definition must live directly under `.vole/images/`, not
487 in a subdirectory. Two files for the same name (`go.yaml` and `go.yml`) are
488 rejected as ambiguous.
489
490 The definition is validated and pinned when the run is queued. See
491 [Image reference](image-reference.md) for its fields and limits.
492
493 ### `runs-on`
494
495 A single label or a list of labels:
496
497 ```yaml
498 runs-on: linux
499 runs-on: [linux, arm64]
500 ```
501
502 A runner must carry every listed label to receive the job. An absent or empty
503 `runs-on` means any runner may take the job. Duplicate labels are ignored; at most
504 8 labels may be declared. Each label must match:
505
506 ```text
507 ^[a-z0-9][a-z0-9._-]{0,31}$
508 ```
509
510 That is 1–32 characters: lowercase letters, digits, `.`, `_` and `-`, starting
511 with a letter or digit.
512
513 See [Runners § Start a runner](../runner.md#start-a-runner) for how a machine
514 advertises labels.
515
d7958024
Keenan Nemetz
7d ago
516 ### `network`
517
518 Whether the job's container can reach anything outside it:
519
520 ```yaml
521 jobs:
522 fetch:
523 image: golang:1.27-bookworm
524 network: bridge
525 steps:
526 - run: go mod download
527 ```
528
529 `bridge` gives the container a network. `none` withholds it, and a step that tries
530 to reach out then fails to resolve a host, which is the answer a job that does not
531 need the network should get. Only these two values are accepted; there is no
532 `host`, because a job that shares the runner's network is not isolated from it.
533
629fdd87
Keenan Nemetz
6d ago
534 Both names are the modes every supported runtime takes for `--network`, so the same
535 workflow means the same thing on Docker, rootless Podman, and nerdctl. What the
536 network is underneath differs — Docker's own bridge, Podman's userspace stack,
537 nerdctl's CNI bridge — and that is the machine's business: the field says whether a
538 job may reach out, not how the runner provides the route.
539
d7958024
Keenan Nemetz
7d ago
540 The job decides, and the machine's own setting fills in only when the field is
541 absent:
542
543 | Job says | Runner can give bridge | Runner cannot |
544 |---|---|---|
545 | nothing | bridge, as the runner was started | none, as the runner was started |
546 | `none` | none | none |
547 | `bridge` | bridge | the job waits for a runner that can |
548
549 A job that asks for `bridge` is only leased to a runner started with
550 `--container-network bridge`, so it waits for a capable machine instead of running
551 without the network it asked for. A job that asks for `none` gets none from either
552 kind of runner, so the same workflow is isolated the same way everywhere.
553
554 A `vole workflow run` applies the same rule: the job's request wins over the
555 command line, and a `bridge` job is refused when the local run was started with
556 `--container-network none`.
557
c1929d2f
Keenan Nemetz
5d ago
558 ### `services`
559
560 Containers started beside the job, for the servers a test or a build talks to:
561
562 ```yaml
563 jobs:
564 test:
565 image: golang:1.27-bookworm
566 network: bridge
567 services:
568 db:
569 image: postgres:17
570 env:
571 POSTGRES_PASSWORD: test
572 PGDATA: /tmp/pgdata
573 steps:
574 - run: |
575 DATABASE_URL=postgres://postgres:test@db:5432/postgres go test ./...
576 ```
577
578 A service is a container, not a job: it has no steps, no checkout, no secrets, and
579 no host mount, and it runs the image's own command. The service's name is its
580 hostname on a private network the runner creates for the attempt. The job and its
581 services are the only members, so `db:5432` is the address a step uses and two
582 services may listen on the same port.
583
584 **A job with services must also declare `network: bridge`.** Without it the service
585 could only be reached by giving the job a network the workflow did not ask for,
586 which is the promise `network: none` exists to keep. A push that declares services
587 with `none` or with no `network:` is refused. A bridge job is already only leased to
588 a runner that advertises bridge, so no new runner capability is involved.
589
590 The runner starts the services after the job image is resolved and before the first
591 step. A service that is not running when the steps are about to start fails the job.
592 There is no health check: a step that needs a server to accept connections waits for
593 it, as `pg_isready` does in a postgres test. When the attempt fails, times out, or is
594 canceled, the last 256 KiB of each service's output is added to the log under
595 `=== service db ===`; a successful run does not carry it. The job stops first, then
596 the services, then the network. A runner killed mid-job leaves all three behind, and
597 its next start removes them by label.
598
599 | Constraint | Value |
600 |---|---|
601 | Maximum services per job | 3 |
602 | Service name | `^[a-z][a-z0-9-]{0,31}$`, and the hostname a step uses |
603 | Image | A registry reference, not `image-file:`. Pin it by digest for reproducibility. |
604 | Environment | At most 64 entries; the same name, value, and one-line rules as a job's `env` |
605 | Secrets | Not available to a service. A test credential is a literal in the workflow; a real one is for the job. |
606 | Isolation | The job's user, user namespace, capabilities, and resource limits; `/tmp` is a writable tmpfs |
607
608 The service image must run as the job's uid. A database image that expects to start
609 as root is not supported by this field; use an image that can run unprivileged, or
610 point its data directory at `/tmp` as the postgres example does. The resource flags
611 on `vole runner` apply to each container, so a job with services can use more than
612 one container's limit.
613
3b39a84c
Keenan Nemetz
14d ago
614 ### `env`
615
616 Environment variables for every step of the job, on top of the image's own. Later
617 values win, so a job can change something its image set:
618
619 ```yaml
620 jobs:
621 test:
622 image: golang:1.27-bookworm
623 env:
624 GOCACHE: /cache/go-build
625 GOFLAGS: "-mod=readonly"
626 steps:
627 - run: go test ./...
628 ```
629
630 | Constraint | Value |
631 |---|---|
632 | Maximum entries | 64 |
633 | Key syntax | `^[A-Za-z_][A-Za-z0-9_]*$` |
634 | Maximum value size | 8 KiB |
635 | Line endings | Values must be one line; `CR`/`LF` are rejected |
636
637 `CI`, `VOLE_CI`, `GIT_TERMINAL_PROMPT`, `HOME` and `TMPDIR` are set by the runner and
638 cannot be replaced: the sandbox depends on them, so the push that tries is refused
6e1fca51
Keenan Nemetz
6d ago
639 rather than silently disagreeing with it. `VOLE_RUN_ID`, `VOLE_REPO`,
640 `VOLE_SSH_COMMAND` and `VOLE_KNOWN_HOSTS` are reserved for the same reason: they point
641 a job at its credential, and a workflow that could rewrite them could redirect the
642 push.
3b39a84c
Keenan Nemetz
14d ago
643
644 This is how a job opts in to something the runner provides but does not decide for
645 it. The job cache is the one that matters today: the runner mounts a directory at
646 `/cache` and sets no variable for it, so pointing `GOCACHE` (or `CARGO_HOME`, or
647 `npm_config_cache`) there is what makes the cache shared. See
648 [Runners § Caching](../runner.md#caching).
649
4ac42680
Keenan Nemetz
7d ago
650 ### `secrets`
651
652 The repository secrets this job reads. Each entry is a name, never a value: the
653 instance supplies the values when the job is leased, and the name is also the
654 environment variable a step finds it in. One name may be written on its own:
655
656 ```yaml
657 jobs:
658 deploy:
659 image: alpine:3.22
660 secrets: DEPLOY_KEY
661 steps:
662 - run: ./deploy.sh
663 ```
664
665 or as a list:
666
667 ```yaml
668 secrets: [DEPLOY_KEY, REGISTRY_TOKEN]
669 ```
670
671 Names are upper-case letters, digits and `_`, at most 64 bytes, starting with a
672 letter. A name that repeats another entry is refused, and so is one the runner sets
673 itself (`CI`, `VOLE_CI`, `HOME`, `TMPDIR`, `GIT_TERMINAL_PROMPT`, and the
674 `VOLE_EVENT*` variables).
675
676 A declared name the repository has no value for is not an error at push time and
677 does not fail the job. The variable is left **unset** — a step can tell that apart
678 from empty — and the runner writes one line to the log:
679
680 ```
681 vole: DEPLOY_TOKEN is not set for this repository, so this job runs without it.
682 ```
683
684 A step that requires one says so with the shell's own check:
685 `: "${DEPLOY_TOKEN:?DEPLOY_TOKEN is not set}"`.
686
687 A value is at most 128 KiB, the kernel's limit for one `NAME=value` string in a
688 process environment, and it is not part of the workflow, of any container argument,
689 or of any run record. A record names the version of each value the job ran with.
690
691 The values reach the step through a file the runner writes into the run directory
692 mounted at `/run/vole`, and the runner sources it before each step. There is no
693 expressions layer: a value is a variable, and a step that builds a name at runtime
694 reads the file itself.
695
e82bbe7d
Keenan Nemetz
7d ago
696 The runner replaces each delivered value with `***` as the log is written, so a step
697 that prints one by accident does not put it in the log the instance stores. This is
698 best effort, and worth knowing exactly:
699
700 - a value that has been re-encoded — base64, hex, a URL escape, one field of a
701 connection string — no longer matches, and is not masked;
702 - a value shorter than 4 bytes is not masked at all, because replacing every
703 occurrence of `1` or `yes` would rewrite the log and protect nothing;
704 - an artifact, a site file or a release attachment is stored as the step wrote it,
705 so a secret a step writes to a file it publishes is published.
706
ac541b26
Keenan Nemetz
6d ago
707 ### `acts-as`
6e1fca51
Keenan Nemetz
6d ago
708
709 The bot account the job acts as:
710
711 ```yaml
712 jobs:
713 triage:
714 image-file: agent
ac541b26
Keenan Nemetz
6d ago
715 acts-as: coding-agent
6e1fca51
Keenan Nemetz
6d ago
716 steps:
717 - run: vole issue comment "$ISSUE" --reply-to "$COMMENT" -F reply.md
718 ```
719
720 The account must be a bot account (see
721 [Accounts and access § Bot accounts](../accounts-and-access.md#bot-accounts)), and
722 the instance issues the run a credential for it when the job is leased: a key
723 generated for that run, written into the job, and gone when the run ends. The job
724 inherits the account's role, so nothing here grants more than that account may do.
725
726 Two things change, and only two:
727
728 - writing to the issues branch no longer requires `maintain`. What a job may write
729 there is what its account may do, asked the same way the browser asks: an account
730 that may discuss files an issue and comments, an author manages the item it wrote,
731 a maintainer triages any of them, and an account that may approve may approve. The
732 credential adds no rule of its own; it removes the one requirement that made every
733 metadata write a maintainer's act;
734 - the job can push to the instance without a long-lived key in a repository secret.
735 The key is valid for this run, and a job whose account is retired or whose key has
736 expired writes nothing.
737
738 The credential is a file, not a variable, and the runner points git at it, so a step
739 does not need to know it exists:
740
741 | Variable | Value |
742 |---|---|
743 | `VOLE_RUN_ID` | The run this job is executing. |
744 | `VOLE_REPO` | The repository, as the instance names it. |
745 | `VOLE_SSH_COMMAND` | The ssh command that uses the run's key and known_hosts file. |
746 | `VOLE_KNOWN_HOSTS` | The known_hosts file with this instance's host key. |
747
748 These four are set by the runner and cannot be replaced by the workflow's `env:`,
ac541b26
Keenan Nemetz
6d ago
749 the same rule as `CI` and `HOME`. A job that names no `acts-as` gets none of them and no
6e1fca51
Keenan Nemetz
6d ago
750 credential, which is what every workflow had before this field existed.
751
e7aa0785
Keenan Nemetz
6d ago
752 ### `timeout`
753
754 A wall-clock budget for the job's steps, instead of a cap on each step separately:
755
756 ```yaml
757 jobs:
758 test:
759 image: golang:1.27-bookworm
760 timeout: 2h
761 steps:
762 - run: go test ./...
763 - run: go vet ./...
764 ```
765
766 The budget starts with the first step and is spent in order. A step that declares its
767 own `timeout:` is still cut short when the budget ends first, and a step that declares
768 none runs with whatever is left, so one long step is enough to say:
769
770 ```yaml
771 jobs:
772 agent:
773 image-file: agent
774 timeout: 4h
775 steps:
776 - run: run-the-session
777 ```
778
779 When the budget is spent before a step starts, that step does not run and the job
780 fails, with one line in the log saying so. The field is optional: without it each step
781 is bounded by its own timeout, which is how a workflow worked before the field
782 existed.
783
784 The image build is not part of the budget. It has its own cap, so a slow pull cannot
785 eat the time the steps were given.
786
787 `timeout` is at most 4 hours, and a step whose own timeout is longer than the job's is
788 refused at push time: the budget would end first, so the step could never use it.
789
70b70264
Keenan Nemetz
15d ago
790 ### `steps`
791
792 A non-empty list of shell steps, at most 100. Steps run in order. A step that exits
793 non-zero stops the job and becomes the job's exit code.
794
795 Steps run inside the job's image, in the disposable checkout. They share the
796 container and filesystem with the other steps in the same job.
797
798 #### Step fields
799
800 | Field | Type | Required | Default | Meaning |
801 |---|---|---|---|---|
802 | `name` | string | no | first line of `run` | Display name in the log. |
803 | `run` | string | yes | — | Shell command executed with `/bin/sh -c`. |
e7aa0785
Keenan Nemetz
6d ago
804 | `timeout` | duration string | no | the job's remaining budget, or `10m` | Maximum time for this step. |
70b70264
Keenan Nemetz
15d ago
805
806 ```yaml
807 steps:
808 - name: test
809 run: go test ./...
810 timeout: 20m
811 ```
812
813 `run` is trimmed and must not be empty. The maximum size is 64 KiB. `name` is
814 trimmed; when omitted it is derived from the first line of `run`, shortened to 60
815 characters.
816
817 `timeout` uses Go duration syntax (`90s`, `10m`, `1h`). It must be positive and no
e7aa0785
Keenan Nemetz
6d ago
818 more than 4 hours, and it may not be longer than the job's own `timeout` when that is
819 declared. When a step times out, the job fails and its container is stopped.
820
821 A step that declares no timeout of its own runs with the rest of the job's budget,
822 or 10 minutes when the job has no budget. See the job's [`timeout`](#timeout), which is the budget the step runs inside.
70b70264
Keenan Nemetz
15d ago
823
824 ### `artifacts`
825
826 A list of file patterns to publish when the steps succeed. At most 32 declarations
827 are allowed.
828
829 | Field | Type | Required | Default | Meaning |
830 |---|---|---|---|---|
831 | `path` | glob string | yes | — | Files to publish, relative to the checkout. |
832 | `release` | boolean | no | `false` | Also attach the matches to the tag's release. |
833
834 ```yaml
835 artifacts:
836 - path: dist/*
837 - path: dist/checksums.txt
838 release: true
839 ```
840
841 `path` rules:
842
843 - Must be relative to the checkout; absolute paths and `..` segments are rejected.
844 - Maximum 256 bytes.
845 - Must name files, not a directory: a trailing `/` is rejected.
846 - Uses one-segment glob semantics. `dist/*` matches files directly in `dist/` but
847 not files in `dist/sub/`.
848 - `**` is rejected; use `dist/*` or declare more patterns.
849 - A pattern that matches nothing fails the job.
850 - Two matches with the same base name are refused because they would publish under
851 the same download name.
852 - `.git` is never published; symlinks and non-regular files are skipped.
853
854 `release: true` requires the workflow to declare `on.push.tags`. The job must run
855 for an annotated tag; a branch run or a lightweight tag cannot publish a release.
856 Release artifacts are attached to the tag's release in addition to being run
857 artifacts. See [Releases](../releases.md).
858
859 ### `pages`
860
861 Publishes one directory as the repository's website.
862
863 | Field | Type | Required | Default | Meaning |
864 |---|---|---|---|---|
865 | `path` | directory string | yes | — | Directory to publish, relative to the checkout. |
866
867 ```yaml
868 pages:
869 path: site/
870 ```
871
872 `path` must be relative, at most 256 bytes, and must not climb out of the
873 checkout with a `..` path segment or contain glob characters (`*`, `?`, `[`). A trailing `/` is accepted and trimmed. The directory
874 is published after the steps pass, using the same rules as
875 `vole pages deploy`.
876
877 Only one job in a workflow may declare `pages`. The target branch is set by the
878 instance (`pages_ref`), not by the workflow. The deploy is refused if the run is no
879 longer the newest commit on its branch. See [Workflows § Publish a website](../workflows.md#publish-a-website).
880
390e3404
Keenan Nemetz
7d ago
881 ## Context and expressions
882
883 A workflow file is not a template. There is no `${{ }}` and no substitution in a
884 step's `run`: `env` values are literal, a step executes the shell it is given, and
885 everything the run knows arrives as an environment variable, a declared secret, or
886 the event file. A manual input is read from the event file like any other event
887 data:
888
889 ```yaml
890 on:
891 workflow-dispatch:
892 inputs:
893 environment:
894 type: choice
895 options: [staging, production]
896 jobs:
897 deploy:
898 image: alpine:3.22
899 steps:
900 - run: |
901 environment="$(jq -r '.inputs.environment' "$VOLE_EVENT_PATH")"
902 [ "$environment" = production ] || exit 0
903 ./deploy.sh
904 ```
905
906 `jq` is an example, not a dependency: the image is the dependency manifest, so
907 install the JSON reader the step uses.
908
909 This is a decision, not an unfinished feature:
910
911 - The shell is already the expression language. A second one would mean two ways
912 to write the same comparison, with its own parser, type rules, functions, and
913 escaping, all validated on every push.
914 - Event data is untrusted. A comment body, an issue title, or a manual input is
915 text someone else wrote; substituting it into `run` would turn data into code.
916 A file keeps it data, and the shell's quoting rules apply.
917 - A secret is a value, not a name to build. It already arrives as an environment
918 variable; an expression would invite it into a command line or a log line.
919 - The parts of a workflow that usually need expressions — `if`, `needs`, matrix
920 values, outputs — are not in the format. If they arrive, they can be typed
921 fields with their own small, checkable rules instead of a general language.
922
1cde8b9e
Keenan Nemetz
8d ago
923 ## What a workflow cannot express
924
eb07ff56
Keenan Nemetz
8d ago
925 The format deliberately supports push and event triggers with jobs of shell steps.
926 It does not support:
1cde8b9e
Keenan Nemetz
8d ago
927
928 - a job graph: there is no `needs`, no job outputs, and no shared artifacts between jobs
929 - a build matrix or per-step conditions (`strategy`, `if`, `continue-on-error`)
4ac42680
Keenan Nemetz
7d ago
930 - step-scoped secrets, expressions for building a secret name, or a value written
931 in the workflow file; secrets are job-scoped and the instance holds the values
390e3404
Keenan Nemetz
7d ago
932 - expressions or `${{ }}` contexts; the shell is the expression language and the
933 event file is the context. See [Context and expressions](#context-and-expressions).
c1929d2f
Keenan Nemetz
5d ago
934 - cache keys, or artifact downloads between jobs
e7aa0785
Keenan Nemetz
6d ago
935 - concurrency groups, or cancel-in-progress
1cde8b9e
Keenan Nemetz
8d ago
936 - workflow-level `env`, working directories, or shells; `env` is per job and every
937 step runs through `/bin/sh -c` in the checkout
938 - reusable workflows or composite actions (`uses:`); shell in an image is the
939 reuse mechanism
940 - environments, deployment approvals, per-job permissions, or OIDC
eb07ff56
Keenan Nemetz
8d ago
941 - event payloads larger than 64 KiB; a long body is clipped in the pinned event
1cde8b9e
Keenan Nemetz
8d ago
942
eb07ff56
Keenan Nemetz
8d ago
943 Combined results for a commit under review are still produced automatically by the
944 server; `on.change-request` is the event trigger, not the combined-result scheduler.
945 A local `vole workflow run` ignores triggers and `runs-on`, publishes no artifacts,
d7958024
Keenan Nemetz
7d ago
946 deploys no site, and has no event payload. It applies a job's `network:` request, so a
c1929d2f
Keenan Nemetz
5d ago
947 `bridge` job needs `--container-network bridge` here too, and it starts the job's
948 services on the same kind of private network a runner would. It reads the secrets its
949 jobs declare from the instance the checkout's remote names, and leaves the variables
950 unset — with a note — where it cannot.
1cde8b9e
Keenan Nemetz
8d ago
951
70b70264
Keenan Nemetz
15d ago
952 ## Validation and pinning
953
954 - The whole file is validated before a push is accepted.
955 - Unknown keys are errors; use the field names on this page.
956 - `version` must be `1`.
c1929d2f
Keenan Nemetz
5d ago
957 - The commit, workflow blob, image definition, display names, descriptions, steps,
958 environment, services, and artifact patterns are pinned when a run is queued. Later
959 edits do not change that run.
70b70264
Keenan Nemetz
15d ago
960 - `vole workflow check` and `vole workflow run` use the same validation as a push.
d7958024
Keenan Nemetz
7d ago
961 A local run executes in the job's image and ignores `on.push` and `runs-on`; it does
962 apply the job's `network:`, and refuses a `bridge` job when the run was started with
963 `--container-network none`.
70b70264
Keenan Nemetz
15d ago
964
965 ## Complete example
966
967 ```yaml
968 version: 1
1cde8b9e
Keenan Nemetz
8d ago
969 name: Verify and release
970 description: Test every matching push, then build a release from a version tag.
70b70264
Keenan Nemetz
15d ago
971 on:
972 push:
973 branches: [main, "release/*"]
974 tags: ["v*"]
975 jobs:
976 test:
1cde8b9e
Keenan Nemetz
8d ago
977 name: Unit tests
978 description: Runs the test suite against the pinned toolchain.
70b70264
Keenan Nemetz
15d ago
979 runs-on: [linux, arm64]
980 image: golang:1.27-bookworm@sha256:...
3b39a84c
Keenan Nemetz
14d ago
981 env:
982 GOCACHE: /cache/go-build
983 GOMODCACHE: /cache/go-mod
70b70264
Keenan Nemetz
15d ago
984 steps:
985 - name: unit tests
986 run: go test ./...
987 timeout: 20m
988 artifacts:
989 - path: coverage/*
990 release:
1cde8b9e
Keenan Nemetz
8d ago
991 name: Build release
70b70264
Keenan Nemetz
15d ago
992 runs-on: linux
993 image-file: release-toolchain
994 steps:
995 - name: build
996 run: make dist
997 artifacts:
998 - path: dist/*
999 release: true
1000 ```