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.
| 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 | |
| 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 | |
| 69 | The only supported value is `1`: |
| 70 | |
| 71 | ```yaml |
| 72 | version: 1 |
| 73 | ``` |
| 74 | |
| 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 |
| 79 | sees on a run page: |
| 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 | |
| 93 | A one-line summary of what the workflow is for, shown under its name on each |
| 94 | run page: |
| 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 | |
| 103 | ### `on.push.branches` |
| 104 | |
| 105 | A 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 | |
| 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 | |
| 122 | A leading `!` excludes what an earlier pattern included. The last matching pattern |
| 123 | wins, so `["*", "!main"]` runs everywhere except `main`: |
| 124 | |
| 125 | ```yaml |
| 126 | on: |
| 127 | push: |
| 128 | branches: [main, "release/*"] |
| 129 | ``` |
| 130 | |
| 131 | ```yaml |
| 132 | on: |
| 133 | push: |
| 134 | branches: ["*", "!main"] |
| 135 | ``` |
| 136 | |
| 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 | |
| 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 | |
| 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`. |
| 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 | |
| 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 | |
| 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 | |
| 338 | Marks a workflow as runnable by hand. The CLI has `vole workflow dispatch`. |
| 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 |
| 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`. |
| 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`. |
| 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 | |---|---|---|---|---| |
| 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 | |
| 423 | Exactly one of `image` and `image-file` must be present. Declaring both, or |
| 424 | neither, is an error. |
| 425 | |
| 426 | ### `name` |
| 427 | |
| 428 | A display name for the job, shown on the run page in place of the job ID: |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| 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 |
| 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. |
| 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 | |
| 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 | |
| 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 | |
| 707 | ### `acts-as` |
| 708 | |
| 709 | The bot account the job acts as: |
| 710 | |
| 711 | ```yaml |
| 712 | jobs: |
| 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 | |
| 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:`, |
| 749 | the same rule as `CI` and `HOME`. A job that names no `acts-as` gets none of them and no |
| 750 | credential, which is what every workflow had before this field existed. |
| 751 | |
| 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 | |
| 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`. | |
| 804 | | `timeout` | duration string | no | the job's remaining budget, or `10m` | Maximum time for this step. | |
| 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 |
| 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. |
| 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 | |
| 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 | |
| 923 | ## What a workflow cannot express |
| 924 | |
| 925 | The format deliberately supports push and event triggers with jobs of shell steps. |
| 926 | It 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 | |
| 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, |
| 946 | deploys 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 |
| 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. |
| 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 |
| 968 | version: 1 |
| 969 | name: Verify and release |
| 970 | description: Test every matching push, then build a release from a version tag. |
| 971 | on: |
| 972 | push: |
| 973 | branches: [main, "release/*"] |
| 974 | tags: ["v*"] |
| 975 | jobs: |
| 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 |