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.
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
7d 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
7d 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
7d 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
7d 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
7d 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
6d 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
6d 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
6d 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
6d 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
6d 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
6d 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
6d 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 | ``` |