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/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

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

Code

392 commit(s) on main

Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys keenan · 15m ago 4b8c9e1d
source preview history blame raw 37.5 KiB
1# Workflow reference
2
3This page lists every field Vole accepts in `.vole/workflows/*.yaml`, with its
4defaults and limits. For a task-oriented introduction, see [Workflows](../workflows.md).
5For `.vole/images` definitions, see [Image reference](image-reference.md).
6
7Unknown 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 |
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) |
24| Maximum `runs-on` labels per job | 8 |
25| Maximum step command size | 64 KiB |
26| Maximum secret value | 128 KiB (`NAME=value`, the kernel's environment limit) |
27| Maximum secret name | 64 bytes |
28| Maximum display name | 80 bytes |
29| Maximum description | 500 bytes |
30| Maximum path filters | 20 per list |
31| Maximum accounts per event filter | 10 per list |
32| Maximum mentions read from one body | 32 |
33| Maximum schedules | 5 |
34| Maximum manual inputs | 10 |
35| Maximum event payload | 64 KiB |
36| Default step timeout | 10 minutes, when the job has no budget |
37| Maximum step timeout | 4 hours |
38| Maximum job timeout | 4 hours |
39
40## Top-level fields
41
42| Field | Type | Required | Default | Meaning |
43|---|---|---|---|---|
44| `version` | integer | yes | — | Schema version. Must be `1`. |
45| `name` | string | no | the workflow file's base name | Display name on the run page. |
46| `description` | string | no | none | One-line summary of what the workflow is for. |
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. |
49| `on.push.tags` | list of strings | no | none | Tag patterns that trigger the workflow. |
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. |
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. |
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. |
65| `jobs` | mapping | yes | — | One or more jobs, keyed by job ID. |
66
67### `version`
68
69The only supported value is `1`:
70
71```yaml
72version: 1
73```
74
75### `name`
76
77A display name for the workflow. The file path stays the identity — it is what a
78run records and what `vole workflow run` selects — while `name` is what a reader
79sees on a run page:
80
81```yaml
82name: Verify main
83```
84
85When `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
88The name is trimmed and must be one line of text, at most 80 bytes. It is display
89metadata; two workflows may share a name.
90
91### `description`
92
93A one-line summary of what the workflow is for, shown under its name on each
94run page:
95
96```yaml
97description: Build, test and lint every push to main.
98```
99
100The description is trimmed and must be one line of text, at most 500 bytes. It is
101optional; an absent description shows nothing.
102
103### `on.push.branches`
104
105A list of branch patterns. The default depends on what else is present:
106
107- A workflow with no `on` trigger at all is an ordinary push ci and runs on every
108 branch.
109- `branches` present: only matching branches trigger it.
110- `tags` present without `branches`: the workflow runs only on matching tags.
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.
114
115Patterns use `path.Match` semantics with one special case: a lone `*` matches every
116branch, including names containing `/`. Other patterns keep the one-segment rule,
117so `release/*` does not match `release/1.0/hotfix`.
118
119Patterns may contain letters, digits, `*`, `?`, `[`, `]`, `!`, `.`, `_`, `/` and
120`-`. Empty patterns and patterns containing `..` are rejected.
121
122A leading `!` excludes what an earlier pattern included. The last matching pattern
123wins, so `["*", "!main"]` runs everywhere except `main`:
124
125```yaml
126on:
127 push:
128 branches: [main, "release/*"]
129```
130
131```yaml
132on:
133 push:
134 branches: ["*", "!main"]
135```
136
137An explicitly empty list means no branches trigger the workflow.
138
139### `on.push.tags`
140
141A 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
145on:
146 push:
147 tags: ["v*"]
148```
149
150Patterns follow the same rules as branch patterns, including `!` exclusion. A
151workflow with `tags` and no `branches` runs on tags only. A workflow with both runs
152on matching branches and matching tags.
153
154### `on.push.paths`
155
156A push runs only when at least one changed path matches one of these patterns.
157Patterns are relative to the repository root. `*` stays inside one path segment;
158`**` spans zero or more segments, so `docs/**` covers `docs/` and everything under
159it. At most 20 patterns may be declared.
160
161```yaml
162on:
163 push:
164 paths: ["src/**", "cmd/**", "go.mod"]
165```
166
167An empty pattern, an absolute path, or a pattern containing `..` is rejected. A
168push that changes no files does not match a workflow with a path filter.
169
170### `on.push.paths-ignore`
171
172Paths that do not trigger the workflow. An ignored path is removed before the
173allow list is consulted, so an ignored file cannot make a workflow run:
174
175```yaml
176on:
177 push:
178 paths-ignore: ["docs/**", "**/*.md"]
179```
180
181`paths` and `paths-ignore` may be combined. When every changed path is ignored,
182nothing runs. Both filters are evaluated against the range the push changed; a new
183branch or tag has no previous tree, so every file counts as changed.
184
185### `on.change-request`
186
187Runs when a change request changes. The run is checked out at the request's base
188branch tip, not at the proposed head: an event run must not be a way to execute an
189unapproved contributor commit. The proposed head and the changed files are in the
190event payload.
191
192```yaml
193on:
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
206The 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
211Runs when an issue changes, at the default branch tip.
212
213```yaml
214on:
215 issue:
216 types: [opened, closed]
217```
218
219Actions are `opened`, `edited`, `assigned`, `closed`, and `reopened`. The default is `[opened]`.
220An empty value means the default. `assigned` is queued when an edit adds an
221assignee. An issue opened with an assignee already set queues `opened`, and its
222`assignees_added` list makes that an assignment for `assigned-to`.
223
224### `on.comment`
225
226Runs when a comment on an issue or a change request is created, edited, or deleted.
227The payload names the comment and the issue or change request it belongs to.
228
229```yaml
230on:
231 comment:
232 types: [created]
233```
234
235Actions are `created`, `edited`, and `deleted`. The default is `[created]`. An
236empty value means the default.
237
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
242actions the trigger has to declare.
243
244```yaml
245on:
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
266The account filters (`mentioned`, `replied-to`, `assigned-to`, and `author.name`)
267take one account or a list of accounts, at most 10. Names follow the account-name
268rule: 2–32 characters, lowercase letters, digits, `-` and `_`. A leading `@` is
269accepted and removed. An explicitly empty list is rejected because a filter that
270can 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
276author: keenan
277author: [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
282an account or answers an account, so a workflow that should run for either can
283declare both. A comment is never treated as addressed to its own author, which
284keeps 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
288repository role of the account that caused the event. A `maintain` filter admits
289maintainers and admins and refuses contributors and readers. `none` is rejected
290because it would filter nothing. Use it when the rule is a capability rather than
291a list of names:
292
293```yaml
294on:
295 comment:
296 types: [created]
297 mentioned: [coding-agent]
298 author:
299 role: maintain
300```
301
302The role is resolved when the event is queued, from the repository's access rules
303and the instance-wide role attached to them. Changing the rules later does not
304change a run that is already queued.
305
306`author.role` is a policy gate on the recorded sender, not a substitute for
307authentication. Contributors reach the metadata branch through the browser, which
308records their session account, and writing that branch directly already requires
309maintain. An above-maintain role is therefore not protection against a malicious
310maintainer with direct push access.
311
312The mention syntax is `@name`. Mentions inside inline code or a fenced code block
313are ignored, as is the `@` in an email address or a URL. A body contributes at
314most 32 distinct mentions to the event.
315
316A filter is evaluated when the event is queued. `author` names the person whose
317write caused the event: the comment author, the issue author or editor, or the
318maintainer who assigned the issue.
319
320### `on.schedule`
321
322Cron schedules in UTC. A schedule queues the workflow at the default branch tip.
323The field syntax is five fields — minute, hour, day of month, month, day of week —
324with `*`, lists, ranges, and a step on `*` or a range. Names and `@shorthands` are
325not accepted.
326
327```yaml
328on:
329 schedule:
330 - cron: "0 3 * * 1-5"
331```
332
333At most 5 schedules may be declared. The run is queued once per matching minute;
334a server that was not running for a slot does not replay it later.
335
336### `on.workflow-dispatch`
337
338Marks a workflow as runnable by hand. The CLI has `vole workflow dispatch`.
339
340```yaml
341on:
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
363At 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.
365A manual run is available to maintainers and instance admins, and the selected
366branch must exist.
367
368### Event data available to steps
369
370Every 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
372trigger 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
380The JSON is pinned when the run is queued. It is data: read it with `jq` or a
381program, never by interpolating it into a shell command. Common fields are
382`event`, `action`, `repository`, `ref`, `branch`, `commit`, `sender`, and for a
383change 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
385under `inputs`. An issue or change request carries `mentions` in its entity. An
386issue 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
388and the payload marks that with `body_truncated`.
389
390These variables are set by the runner and cannot be replaced by a workflow's
391`env:`, the same rule as `CI` and `HOME`.
392
393### `jobs`
394
395A mapping of job IDs to job definitions. Job IDs must match:
396
397```text
398^[a-z][a-z0-9_-]{0,31}$
399```
400
401That is 1–32 characters: lowercase letters, digits, `_` and `-`, starting with a
402letter. At least one job is required.
403
404## Job fields
405
406| Field | Type | Required | Default | Meaning |
407|---|---|---|---|---|
408| `name` | string | no | the job ID | Display name on the run page. |
409| `description` | string | no | none | One-line summary of what the job does. |
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. |
413| `network` | `none` or `bridge` | no | the runner's setting | Whether the job's container has a network. |
414| `services` | mapping | no | none | Containers started beside the job, reached by name. |
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. |
416| `acts-as` | account name | no | none | A bot account the job acts as, with a credential for that run. |
417| `env` | mapping | no | none | Environment every step of the job runs with. |
418| `timeout` | duration string | no | none | Wall clock the job's steps share. |
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
423Exactly one of `image` and `image-file` must be present. Declaring both, or
424neither, is an error.
425
426### `name`
427
428A display name for the job, shown on the run page in place of the job ID:
429
430```yaml
431jobs:
432 test:
433 name: Unit tests
434```
435
436When `name` is absent or empty, the job is shown by its ID. The ID stays the key
437that a run, a lease and `vole workflow run --job` use.
438
439The name is trimmed and must be one line of text, at most 80 bytes.
440
441### `description`
442
443An optional one-line summary of what the job does:
444
445```yaml
446jobs:
447 test:
448 name: Unit tests
449 description: Runs the Go tests against the pinned toolchain.
450```
451
452The description is trimmed and must be one line of text, at most 500 bytes.
453
454### `image`
455
456An OCI image reference used exactly as written:
457
458```yaml
459jobs:
460 build:
461 image: golang:1.27-bookworm@sha256:...
462```
463
464The runner pulls and runs this image. Pin it by digest when reproducibility
465matters; a mutable tag can change between runs.
466
467References are limited to 255 bytes and must start with a letter or digit. They may
468contain letters, digits, `.`, `_`, `/`, `:`, `@`, `+`, `~` and `-`. A leading `-`,
469`..`, `://`, or a `file:` prefix is rejected. `file:` is reserved for
470`image-file:`.
471
472### `image-file`
473
474The name of a definition under `.vole/images/`:
475
476```yaml
477jobs:
478 build:
479 image-file: go
480```
481
482`image-file: go` resolves `.vole/images/go.yaml`. The `.yaml` or `.yml` extension
483is optional in the reference; `image-file: go.yaml` is also accepted.
484
485Names are 1–64 characters: lowercase letters, digits, `.`, `_` and `-`, starting
486with a letter or digit. A definition must live directly under `.vole/images/`, not
487in a subdirectory. Two files for the same name (`go.yaml` and `go.yml`) are
488rejected as ambiguous.
489
490The 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
495A single label or a list of labels:
496
497```yaml
498runs-on: linux
499runs-on: [linux, arm64]
500```
501
502A 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
5048 labels may be declared. Each label must match:
505
506```text
507^[a-z0-9][a-z0-9._-]{0,31}$
508```
509
510That is 1–32 characters: lowercase letters, digits, `.`, `_` and `-`, starting
511with a letter or digit.
512
513See [Runners § Start a runner](../runner.md#start-a-runner) for how a machine
514advertises labels.
515
516### `network`
517
518Whether the job's container can reach anything outside it:
519
520```yaml
521jobs:
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
530to reach out then fails to resolve a host, which is the answer a job that does not
531need 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
534Both names are the modes every supported runtime takes for `--network`, so the same
535workflow means the same thing on Docker, rootless Podman, and nerdctl. What the
536network is underneath differs — Docker's own bridge, Podman's userspace stack,
537nerdctl's CNI bridge — and that is the machine's business: the field says whether a
538job may reach out, not how the runner provides the route.
539
540The job decides, and the machine's own setting fills in only when the field is
541absent:
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
549A 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
551without the network it asked for. A job that asks for `none` gets none from either
552kind of runner, so the same workflow is isolated the same way everywhere.
553
554A `vole workflow run` applies the same rule: the job's request wins over the
555command line, and a `bridge` job is refused when the local run was started with
556`--container-network none`.
557
558### `services`
559
560Containers started beside the job, for the servers a test or a build talks to:
561
562```yaml
563jobs:
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
578A service is a container, not a job: it has no steps, no checkout, no secrets, and
579no host mount, and it runs the image's own command. The service's name is its
580hostname on a private network the runner creates for the attempt. The job and its
581services are the only members, so `db:5432` is the address a step uses and two
582services may listen on the same port.
583
584**A job with services must also declare `network: bridge`.** Without it the service
585could only be reached by giving the job a network the workflow did not ask for,
586which is the promise `network: none` exists to keep. A push that declares services
587with `none` or with no `network:` is refused. A bridge job is already only leased to
588a runner that advertises bridge, so no new runner capability is involved.
589
590The runner starts the services after the job image is resolved and before the first
591step. A service that is not running when the steps are about to start fails the job.
592There is no health check: a step that needs a server to accept connections waits for
593it, as `pg_isready` does in a postgres test. When the attempt fails, times out, or is
594canceled, 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
596the services, then the network. A runner killed mid-job leaves all three behind, and
597its 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
608The service image must run as the job's uid. A database image that expects to start
609as root is not supported by this field; use an image that can run unprivileged, or
610point its data directory at `/tmp` as the postgres example does. The resource flags
611on `vole runner` apply to each container, so a job with services can use more than
612one container's limit.
613
614### `env`
615
616Environment variables for every step of the job, on top of the image's own. Later
617values win, so a job can change something its image set:
618
619```yaml
620jobs:
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
638cannot be replaced: the sandbox depends on them, so the push that tries is refused
639rather 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
641a job at its credential, and a workflow that could rewrite them could redirect the
642push.
643
644This is how a job opts in to something the runner provides but does not decide for
645it. 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
650### `secrets`
651
652The repository secrets this job reads. Each entry is a name, never a value: the
653instance supplies the values when the job is leased, and the name is also the
654environment variable a step finds it in. One name may be written on its own:
655
656```yaml
657jobs:
658 deploy:
659 image: alpine:3.22
660 secrets: DEPLOY_KEY
661 steps:
662 - run: ./deploy.sh
663```
664
665or as a list:
666
667```yaml
668 secrets: [DEPLOY_KEY, REGISTRY_TOKEN]
669```
670
671Names are upper-case letters, digits and `_`, at most 64 bytes, starting with a
672letter. A name that repeats another entry is refused, and so is one the runner sets
673itself (`CI`, `VOLE_CI`, `HOME`, `TMPDIR`, `GIT_TERMINAL_PROMPT`, and the
674`VOLE_EVENT*` variables).
675
676A declared name the repository has no value for is not an error at push time and
677does not fail the job. The variable is left **unset** — a step can tell that apart
678from empty — and the runner writes one line to the log:
679
680```
681vole: DEPLOY_TOKEN is not set for this repository, so this job runs without it.
682```
683
684A step that requires one says so with the shell's own check:
685`: "${DEPLOY_TOKEN:?DEPLOY_TOKEN is not set}"`.
686
687A value is at most 128 KiB, the kernel's limit for one `NAME=value` string in a
688process environment, and it is not part of the workflow, of any container argument,
689or of any run record. A record names the version of each value the job ran with.
690
691The values reach the step through a file the runner writes into the run directory
692mounted at `/run/vole`, and the runner sources it before each step. There is no
693expressions layer: a value is a variable, and a step that builds a name at runtime
694reads the file itself.
695
696The runner replaces each delivered value with `***` as the log is written, so a step
697that prints one by accident does not put it in the log the instance stores. This is
698best 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
707### `acts-as`
708
709The bot account the job acts as:
710
711```yaml
712jobs:
713 triage:
714 image-file: agent
715 acts-as: coding-agent
716 steps:
717 - run: vole issue comment "$ISSUE" --reply-to "$COMMENT" -F reply.md
718```
719
720The account must be a bot account (see
721[Accounts and access § Bot accounts](../accounts-and-access.md#bot-accounts)), and
722the instance issues the run a credential for it when the job is leased: a key
723generated for that run, written into the job, and gone when the run ends. The job
724inherits the account's role, so nothing here grants more than that account may do.
725
726Two 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
738The credential is a file, not a variable, and the runner points git at it, so a step
739does 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
748These four are set by the runner and cannot be replaced by the workflow's `env:`,
749the same rule as `CI` and `HOME`. A job that names no `acts-as` gets none of them and no
750credential, which is what every workflow had before this field existed.
751
752### `timeout`
753
754A wall-clock budget for the job's steps, instead of a cap on each step separately:
755
756```yaml
757jobs:
758 test:
759 image: golang:1.27-bookworm
760 timeout: 2h
761 steps:
762 - run: go test ./...
763 - run: go vet ./...
764```
765
766The budget starts with the first step and is spent in order. A step that declares its
767own `timeout:` is still cut short when the budget ends first, and a step that declares
768none runs with whatever is left, so one long step is enough to say:
769
770```yaml
771jobs:
772 agent:
773 image-file: agent
774 timeout: 4h
775 steps:
776 - run: run-the-session
777```
778
779When the budget is spent before a step starts, that step does not run and the job
780fails, with one line in the log saying so. The field is optional: without it each step
781is bounded by its own timeout, which is how a workflow worked before the field
782existed.
783
784The image build is not part of the budget. It has its own cap, so a slow pull cannot
785eat 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
788refused at push time: the budget would end first, so the step could never use it.
789
790### `steps`
791
792A non-empty list of shell steps, at most 100. Steps run in order. A step that exits
793non-zero stops the job and becomes the job's exit code.
794
795Steps run inside the job's image, in the disposable checkout. They share the
796container 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`. |
804| `timeout` | duration string | no | the job's remaining budget, or `10m` | Maximum time for this step. |
805
806```yaml
807steps:
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
814trimmed; when omitted it is derived from the first line of `run`, shortened to 60
815characters.
816
817`timeout` uses Go duration syntax (`90s`, `10m`, `1h`). It must be positive and no
818more than 4 hours, and it may not be longer than the job's own `timeout` when that is
819declared. When a step times out, the job fails and its container is stopped.
820
821A step that declares no timeout of its own runs with the rest of the job's budget,
822or 10 minutes when the job has no budget. See the job's [`timeout`](#timeout), which is the budget the step runs inside.
823
824### `artifacts`
825
826A list of file patterns to publish when the steps succeed. At most 32 declarations
827are 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
835artifacts:
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
855for an annotated tag; a branch run or a lightweight tag cannot publish a release.
856Release artifacts are attached to the tag's release in addition to being run
857artifacts. See [Releases](../releases.md).
858
859### `pages`
860
861Publishes 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
868pages:
869 path: site/
870```
871
872`path` must be relative, at most 256 bytes, and must not climb out of the
873checkout with a `..` path segment or contain glob characters (`*`, `?`, `[`). A trailing `/` is accepted and trimmed. The directory
874is published after the steps pass, using the same rules as
875`vole pages deploy`.
876
877Only one job in a workflow may declare `pages`. The target branch is set by the
878instance (`pages_ref`), not by the workflow. The deploy is refused if the run is no
879longer the newest commit on its branch. See [Workflows § Publish a website](../workflows.md#publish-a-website).
880
881## Context and expressions
882
883A workflow file is not a template. There is no `${{ }}` and no substitution in a
884step's `run`: `env` values are literal, a step executes the shell it is given, and
885everything the run knows arrives as an environment variable, a declared secret, or
886the event file. A manual input is read from the event file like any other event
887data:
888
889```yaml
890on:
891 workflow-dispatch:
892 inputs:
893 environment:
894 type: choice
895 options: [staging, production]
896jobs:
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
907install the JSON reader the step uses.
908
909This 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
923## What a workflow cannot express
924
925The format deliberately supports push and event triggers with jobs of shell steps.
926It does not support:
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`)
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
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).
934- cache keys, or artifact downloads between jobs
935- concurrency groups, or cancel-in-progress
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
941- event payloads larger than 64 KiB; a long body is clipped in the pinned event
942
943Combined results for a commit under review are still produced automatically by the
944server; `on.change-request` is the event trigger, not the combined-result scheduler.
945A local `vole workflow run` ignores triggers and `runs-on`, publishes no artifacts,
946deploys no site, and has no event payload. It applies a job's `network:` request, so a
947`bridge` job needs `--container-network bridge` here too, and it starts the job's
948services on the same kind of private network a runner would. It reads the secrets its
949jobs declare from the instance the checkout's remote names, and leaves the variables
950unset — with a note — where it cannot.
951
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`.
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.
960- `vole workflow check` and `vole workflow run` use the same validation as a push.
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`.
964
965## Complete example
966
967```yaml
968version: 1
969name: Verify and release
970description: Test every matching push, then build a release from a version tag.
971on:
972 push:
973 branches: [main, "release/*"]
974 tags: ["v*"]
975jobs:
976 test:
977 name: Unit tests
978 description: Runs the test suite against the pinned toolchain.
979 runs-on: [linux, arm64]
980 image: golang:1.27-bookworm@sha256:...
981 env:
982 GOCACHE: /cache/go-build
983 GOMODCACHE: /cache/go-mod
984 steps:
985 - name: unit tests
986 run: go test ./...
987 timeout: 20m
988 artifacts:
989 - path: coverage/*
990 release:
991 name: Build release
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```
1001