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.
|
70b70264
Keenan Nemetz
15d ago
|
1 | # Workflows |
| 2 | ||
|
eb07ff56
Keenan Nemetz
8d ago
|
3 | A workflow describes what CI runs: which pushes, change requests, issues, |
| 4 | comments, schedules, or manual runs trigger it, which steps each job executes, and | |
| 5 | what it publishes. Machines and execution belong to [runners](runner.md); this page | |
| 6 | is about the repository side. | |
|
70b70264
Keenan Nemetz
15d ago
|
7 | |
|
1cde8b9e
Keenan Nemetz
8d ago
|
8 | For every field, limit, and default, see the [Workflow reference](reference/workflow-reference.md), |
| 9 | including [what a workflow cannot express](reference/workflow-reference.md#what-a-workflow-cannot-express). | |
|
70b70264
Keenan Nemetz
15d ago
|
10 | |
| 11 | ## Add a workflow | |
| 12 | ||
| 13 | Workflows live in `.vole/workflows/*.yaml`. When you push a new workflow, Vole | |
| 14 | checks it before accepting the push, so a broken workflow is rejected early. | |
| 15 | ||
| 16 | ```yaml | |
| 17 | version: 1 | |
|
1cde8b9e
Keenan Nemetz
8d ago
|
18 | name: Verify main |
| 19 | description: Build, test and lint every push to main. | |
|
70b70264
Keenan Nemetz
15d ago
|
20 | on: |
| 21 | push: | |
| 22 | branches: [main] | |
| 23 | jobs: | |
| 24 | build: | |
|
1cde8b9e
Keenan Nemetz
8d ago
|
25 | name: Build and test |
|
70b70264
Keenan Nemetz
15d ago
|
26 | runs-on: [linux, arm64] # only a runner with both labels takes this job |
| 27 | image: golang:1.27-bookworm | |
| 28 | steps: | |
| 29 | - name: test | |
| 30 | run: go test ./... | |
| 31 | - name: build | |
| 32 | run: make dist | |
| 33 | artifacts: | |
| 34 | - path: dist/* | |
| 35 | pages: | |
| 36 | path: site/ # publish this directory as the repository's site | |
| 37 | ``` | |
| 38 | ||
|
1cde8b9e
Keenan Nemetz
8d ago
|
39 | `name` and `description` say what the workflow is for; the file path stays its |
|
57dc79af
Keenan Nemetz
6d ago
|
40 | identity. When `name` is absent, a run shows the file's base name (`verify.yaml` |
| 41 | becomes `verify`). A job's `name` is shown in place of its ID on the run page. | |
| 42 | These fields are display metadata: they do not change what runs, and two | |
| 43 | workflows may share a name. | |
|
1cde8b9e
Keenan Nemetz
8d ago
|
44 | |
|
70b70264
Keenan Nemetz
15d ago
|
45 | Every job must name the environment it runs in, with exactly one of `image:` or |
| 46 | `image-file:`. A workflow that names neither fails the push that introduced it. | |
| 47 | See [Images](#images) for the two forms. | |
| 48 | ||
| 49 | Triggers work like this: | |
| 50 | ||
|
eb07ff56
Keenan Nemetz
8d ago
|
51 | - `on.push.branches` limits a workflow to certain branches; a leading `!` excludes |
| 52 | what an earlier pattern included. | |
|
70b70264
Keenan Nemetz
15d ago
|
53 | - `on.push.tags` limits it to tag pushes. |
|
eb07ff56
Keenan Nemetz
8d ago
|
54 | - `on.push.paths` and `on.push.paths-ignore` filter a push by the files it changes. |
| 55 | - A workflow with no `on` trigger at all runs on every branch. | |
|
70b70264
Keenan Nemetz
15d ago
|
56 | - A workflow that names `tags:` runs only on tags. |
|
eb07ff56
Keenan Nemetz
8d ago
|
57 | - A workflow that declares only event triggers does not also run on every push. |
|
70b70264
Keenan Nemetz
15d ago
|
58 | |
|
1cde8b9e
Keenan Nemetz
8d ago
|
59 | A push queues each job in the workflow as its own run. Jobs are independent and |
| 60 | unordered in CI: one job cannot wait for another, read its files, or consume its | |
| 61 | result. The order in the file matters only to a local `vole workflow run`, which | |
| 62 | executes the selected jobs in that order. | |
| 63 | ||
|
70b70264
Keenan Nemetz
15d ago
|
64 | `runs-on` asks for a runner with every label in the list. A job with no `runs-on` |
| 65 | can run anywhere. See [Runners § Start a runner](runner.md#start-a-runner) for what the | |
| 66 | labels mean on the machine. | |
| 67 | ||
|
e7aa0785
Keenan Nemetz
6d ago
|
68 | A job can budget its whole run instead of each step separately. `timeout:` on a job |
| 69 | is the clock its steps share, so one long step is one number to write: | |
| 70 | ||
| 71 | ```yaml | |
| 72 | jobs: | |
| 73 | test: | |
| 74 | image: golang:1.27-bookworm | |
| 75 | timeout: 2h | |
| 76 | steps: | |
| 77 | - run: go test ./... | |
| 78 | ``` | |
| 79 | ||
| 80 | A step with no timeout of its own runs with what is left of the budget, and a budget | |
| 81 | that runs out stops the job at the next step. Without the field, each step keeps its | |
| 82 | own cap and the job has no total. See | |
| 83 | [Workflow reference § timeout](reference/workflow-reference.md#timeout), which | |
| 84 | also says why the image build is not part of the budget. | |
| 85 | ||
|
d7958024
Keenan Nemetz
6d ago
|
86 | A job's network is closed by default: the container gets whatever the runner can |
| 87 | offer, and a runner with no network is the normal case. `network: bridge` asks for | |
| 88 | one, which a job that downloads dependencies needs, and only a runner that can | |
| 89 | provide it takes the job. `network: none` withholds it, so a job that should not | |
| 90 | reach out fails to resolve a host instead of quietly succeeding, even on a machine | |
| 91 | that could have given it a network. Declaring the field makes the job behave the | |
| 92 | same way everywhere instead of depending on which runner picked it up. See | |
| 93 | [Workflow reference § network](reference/workflow-reference.md#network). | |
| 94 | ||
|
70b70264
Keenan Nemetz
15d ago
|
95 | When a push starts a job, Vole remembers the exact commit, the exact workflow |
| 96 | file, and the exact image definition. Editing any of them later does not change a | |
| 97 | job that is already queued. | |
| 98 | ||
|
eb07ff56
Keenan Nemetz
8d ago
|
99 | ## Triggers beyond a push |
| 100 | ||
| 101 | A workflow can react to a change request, an issue, a comment, a schedule, or a | |
| 102 | maintainer asking for a run: | |
| 103 | ||
| 104 | ```yaml | |
| 105 | version: 1 | |
| 106 | on: | |
| 107 | change-request: | |
| 108 | types: [opened, updated, ready] | |
| 109 | issue: | |
| 110 | types: [opened, closed] | |
| 111 | comment: | |
| 112 | types: [created] | |
| 113 | schedule: | |
| 114 | - cron: "0 3 * * 1-5" | |
| 115 | workflow-dispatch: | |
| 116 | inputs: | |
| 117 | environment: | |
| 118 | type: choice | |
| 119 | default: staging | |
| 120 | options: [staging, production] | |
| 121 | jobs: | |
| 122 | report: | |
| 123 | image: alpine:3.22 | |
| 124 | steps: | |
| 125 | - run: jq . "$VOLE_EVENT_PATH" | |
| 126 | ``` | |
| 127 | ||
| 128 | A change-request run checks out the request's base branch tip, not the proposed | |
| 129 | head. The head and the changed files are in the event payload, so a workflow can | |
| 130 | report on a change without executing an unapproved contributor commit. | |
| 131 | ||
| 132 | An issue, comment, or schedule run uses the default branch tip. A manual run uses | |
| 133 | the branch chosen in the browser or with `--ref`. | |
| 134 | ||
| 135 | The event is a JSON file. The runner sets `VOLE_EVENT`, `VOLE_EVENT_ACTION`, and | |
| 136 | `VOLE_EVENT_PATH` (`/run/vole/event.json`) for every non-push trigger. Read it as | |
| 137 | data; do not interpolate it into a shell command. See | |
| 138 | [Workflow reference § Event data available to steps](reference/workflow-reference.md#event-data-available-to-steps) | |
| 139 | for the fields. | |
| 140 | ||
|
390e3404
Keenan Nemetz
6d ago
|
141 | A workflow is not a template. There is no `${{ }}` and no substitution inside |
| 142 | `run`: use `env` or a declared secret for a configuration value, and read the | |
| 143 | event file when a step needs something the event carries. The event is text | |
| 144 | someone else wrote, so it stays data instead of becoming part of a command. See | |
| 145 | [Workflow reference § Context and expressions](reference/workflow-reference.md#context-and-expressions) | |
| 146 | for the reasoning and an input example. | |
| 147 | ||
|
727df386
Keenan Nemetz
7d ago
|
148 | ## Filtering an event |
| 149 | ||
| 150 | A trigger can narrow an event to the cases a job cares about. This is how a coding | |
| 151 | agent can wait to be asked, instead of running on every comment: | |
| 152 | ||
| 153 | ```yaml | |
| 154 | version: 1 | |
| 155 | name: Coding agent | |
| 156 | on: | |
| 157 | issue: | |
| 158 | types: [opened, assigned] | |
| 159 | assigned-to: [coding-agent] | |
|
6e1fca51
Keenan Nemetz
6d ago
|
160 | author: |
| 161 | role: maintain | |
|
727df386
Keenan Nemetz
7d ago
|
162 | comment: |
| 163 | types: [created] | |
| 164 | mentioned: [coding-agent] | |
| 165 | replied-to: [coding-agent] | |
| 166 | author: | |
| 167 | role: maintain | |
|
6e1fca51
Keenan Nemetz
6d ago
|
168 | jobs: |
| 169 | agent: | |
| 170 | image-file: agent | |
| 171 | network: bridge | |
| 172 | timeout: 2h | |
|
ac541b26
Keenan Nemetz
6d ago
|
173 | acts-as: coding-agent |
|
6e1fca51
Keenan Nemetz
6d ago
|
174 | steps: |
| 175 | - run: run-the-session | |
|
727df386
Keenan Nemetz
7d ago
|
176 | ``` |
| 177 | ||
| 178 | `mentioned` matches an `@coding-agent` in the issue, comment, or change body. | |
| 179 | `replied-to` matches a comment that answers one written by `coding-agent`. | |
| 180 | `assigned-to` matches an issue that was assigned to it, including one opened with | |
| 181 | that assignment already set. `author.role: maintain` is the ACL filter: it admits | |
| 182 | maintainers and admins, not contributors or readers. Use `author.name` instead when the | |
| 183 | rule is a list of people rather than a capability. | |
| 184 | ||
| 185 | A comment is not treated as addressed to its own author, so the agent's reply does | |
| 186 | not start it again. Mentions inside code samples are ignored. See | |
| 187 | [Workflow reference § Event filters](reference/workflow-reference.md#event-filters) | |
| 188 | for the exact rules. | |
| 189 | ||
|
29a795b9
Keenan Nemetz
6d ago
|
190 | The job acts as an account, so the account it uses should be a bot: it has no |
|
ac541b26
Keenan Nemetz
6d ago
|
191 | browser sign-in and is shown as a bot wherever its name appears. `acts-as:` names it, and |
|
6e1fca51
Keenan Nemetz
6d ago
|
192 | the instance gives the job a credential for that account when it is leased — one |
| 193 | run's key, written into the job, gone when the run ends — so a comment or a change it | |
| 194 | writes is attributable and needs no long-lived key in a repository secret. The | |
| 195 | account's role still decides what it may do. See | |
| 196 | [Accounts and access § Bot accounts](accounts-and-access.md#bot-accounts) and | |
|
ac541b26
Keenan Nemetz
6d ago
|
197 | [Workflow reference § acts-as](reference/workflow-reference.md#acts-as). |
|
6e1fca51
Keenan Nemetz
6d ago
|
198 | |
| 199 | `network: bridge` is there because the session calls a model; `timeout` because a | |
| 200 | session can run longer than the ten-minute step default. A job that runs unattended | |
| 201 | should have both. | |
|
29a795b9
Keenan Nemetz
6d ago
|
202 | |
|
727df386
Keenan Nemetz
7d ago
|
203 | A comment body is still untrusted text. If the job can write — comment, push a |
|
f24c4142
Keenan Nemetz
6d ago
|
204 | branch, or open a change — use `author.role: maintain` or `author.name` to limit who |
| 205 | can start it. Assignment is not a substitute for that filter: whoever opened an | |
| 206 | issue may assign it, so `assigned-to` alone lets a contributor whose issue it is | |
| 207 | start the job. Combine the two when only a maintainer should be able to ask. Read | |
| 208 | the agent's instructions from the repository, never from the event body. | |
|
727df386
Keenan Nemetz
7d ago
|
209 | |
|
eb07ff56
Keenan Nemetz
8d ago
|
210 | ## Manual runs |
| 211 | ||
|
57dc79af
Keenan Nemetz
6d ago
|
212 | A workflow with `on.workflow-dispatch` can be started by hand. Maintainers and |
| 213 | instance admins can start one from a clone: | |
|
eb07ff56
Keenan Nemetz
8d ago
|
214 | |
| 215 | ```sh | |
| 216 | vole workflow dispatch --workflow .vole/workflows/deploy.yaml \ | |
| 217 | --ref main --input environment=production | |
| 218 | ``` | |
| 219 | ||
| 220 | The selected branch must exist, and each input is resolved against the types and | |
| 221 | defaults the workflow declares before anything is queued. | |
| 222 | ||
|
e5478085
Keenan Nemetz
11d ago
|
223 | ## Combined results |
| 224 | ||
| 225 | A change request that cannot fast-forward needs a merge commit before it can land. | |
| 226 | Vole builds that merge and runs the jobs that apply to the base branch against it, | |
| 227 | so the tree that lands is the tree that was tested. The run is queued automatically | |
| 228 | when the head is pushed, and again when the base moves for a request that is under | |
| 229 | review or waiting to merge. It uses the workflow files at the merge, so a workflow | |
| 230 | change on the base applies to the landing, and it does not deploy the repository's | |
| 231 | site. | |
| 232 | ||
| 233 | A combined result derived from a contributor branch is untrusted code. It runs only | |
| 234 | after a maintainer allowed that exact head, and it uses the contributor's cache | |
| 235 | namespace rather than the one a protected branch builds into. | |
| 236 | ||
|
70b70264
Keenan Nemetz
15d ago
|
237 | ## Images |
| 238 | ||
| 239 | A job can name a registry image directly: | |
| 240 | ||
| 241 | ```yaml | |
| 242 | jobs: | |
| 243 | build: | |
| 244 | image: golang:1.27-bookworm@sha256:... | |
| 245 | ``` | |
| 246 | ||
| 247 | Or keep a reusable definition under `.vole/images/` and reference it by name: | |
| 248 | ||
| 249 | ```yaml | |
| 250 | # .vole/images/go.yaml | |
| 251 | version: 1 | |
| 252 | from: golang:1.27-bookworm | |
| 253 | env: | |
| 254 | CGO_ENABLED: "0" | |
| 255 | run: | |
| 256 | - apt-get update && apt-get install -y --no-install-recommends jq | |
| 257 | ``` | |
| 258 | ||
| 259 | ```yaml | |
| 260 | jobs: | |
| 261 | build: | |
| 262 | image-file: go | |
| 263 | steps: | |
| 264 | - run: go test ./... | |
| 265 | ``` | |
| 266 | ||
| 267 | `image-file: go` resolves `.vole/images/go.yaml`. For the complete definition | |
| 268 | schema, see the [Image reference](reference/image-reference.md). A definition has: | |
| 269 | ||
| 270 | | Field | Meaning | | |
| 271 | |---|---| | |
| 272 | | `version` | Required; currently `1` | | |
| 273 | | `from` | Required base image reference | | |
| 274 | | `env` | Optional environment variables for the image | | |
| 275 | | `run` | Optional build commands; each becomes a `RUN` | | |
| 276 | ||
| 277 | Definitions are validated when the workflow is pushed, including definitions | |
| 278 | that are not referenced, and they are pinned with the run. The runner builds the | |
| 279 | image locally and tags it by the definition digest, so later jobs with the same | |
| 280 | definition reuse the build. See [Runners § Images](runner.md#images-and-image-builds) | |
| 281 | for the execution, caching, and security details. | |
| 282 | ||
| 283 | An image definition is code: its build runs on the runner through the OCI | |
| 284 | builder, so approving a contributor tip that changes a definition also approves | |
| 285 | that build. Prefer rootless Podman and pin the base image by digest. | |
| 286 | ||
| 287 | The image is the dependency manifest. It should contain the toolchains and system | |
| 288 | packages the builds need, plus `/bin/sh` and a POSIX `sleep` (Vole runs shell steps | |
| 289 | and keeps the container alive between them). Pin the base image by digest; a | |
| 290 | mutable tag changes what a build means. | |
| 291 | ||
|
3b39a84c
Keenan Nemetz
14d ago
|
292 | ## Environment |
| 293 | ||
| 294 | `env` on a job applies to every step, after the image's own, so the job wins: | |
| 295 | ||
| 296 | ```yaml | |
| 297 | jobs: | |
| 298 | test: | |
| 299 | image: golang:1.27-bookworm | |
| 300 | env: | |
| 301 | GOCACHE: /cache/go-build | |
| 302 | steps: | |
| 303 | - run: go test ./... | |
| 304 | ``` | |
| 305 | ||
| 306 | The runner sets `CI=true`, `VOLE_CI=1`, `HOME=/tmp`, `TMPDIR=/tmp`, and | |
| 307 | `GIT_TERMINAL_PROMPT=0`; a workflow that tries to replace one of those is refused at | |
| 308 | push time. Everything else is the workflow's. This is how a job opts in to the | |
| 309 | cache the runner mounts at `/cache`, and how a job using a registry image gets it | |
| 310 | without an image definition. | |
| 311 | ||
|
c1929d2f
Keenan Nemetz
5d ago
|
312 | ## Services |
| 313 | ||
| 314 | A job can start a database, a queue, or another server beside it and reach it by | |
| 315 | name. The job has to ask for a network too, because the service is reached over it: | |
| 316 | ||
| 317 | ```yaml | |
| 318 | jobs: | |
| 319 | test: | |
| 320 | image: golang:1.27-bookworm | |
| 321 | network: bridge | |
| 322 | services: | |
| 323 | db: | |
| 324 | image: postgres:17 | |
| 325 | env: | |
| 326 | POSTGRES_PASSWORD: test | |
| 327 | PGDATA: /tmp/pgdata | |
| 328 | steps: | |
| 329 | - run: | | |
| 330 | DATABASE_URL=postgres://postgres:test@db:5432/postgres go test ./... | |
| 331 | ``` | |
| 332 | ||
| 333 | The service's name is its hostname on a private network the runner creates for the | |
| 334 | attempt. The job and its services are the only members, so `db:5432` is the address | |
| 335 | a step uses. The service is a container, not a job: it has no steps, no checkout, | |
| 336 | no secrets, and no files from the runner. At most three may run beside one job. | |
| 337 | ||
| 338 | The runner starts them after the image is ready and before the first step. A service | |
| 339 | that exits before the steps start fails the job. There is no health check, so a step | |
| 340 | that needs a server to accept connections waits for it, as a postgres test waits with | |
| 341 | `pg_isready`. When the job fails, times out, or is canceled, the end of each | |
| 342 | service's output is added to the log under `=== service db ===`; a passing run drops | |
| 343 | it. The job, the services, and the network are removed together, and a killed | |
| 344 | runner's leftovers are cleaned up by its next start. | |
| 345 | ||
| 346 | A service image must run as the job's uid, and `/tmp` is writable. The postgres | |
| 347 | example points `PGDATA` at `/tmp` for that reason. A database image that insists on | |
| 348 | starting as root does not work here; use one that can run unprivileged or publish a | |
| 349 | custom image. The runner's memory, CPU, and PID flags apply to each container, so a | |
| 350 | job with services can use more than one container's share. | |
| 351 | ||
|
4ac42680
Keenan Nemetz
7d ago
|
352 | ## Secrets |
| 353 | ||
| 354 | A repository can keep the values its jobs need out of its code: a deploy key, a | |
| 355 | registry token, a staging password. They are stored encrypted on the instance and | |
| 356 | set by the people who maintain the repository: | |
| 357 | ||
| 358 | ```sh | |
| 359 | vole secret set demo DEPLOY_TOKEN # the value is read from stdin | |
| 360 | vole secret list demo | |
| 361 | ``` | |
| 362 | ||
| 363 | A job asks for one by name, and the name is also the variable a step reads: | |
| 364 | ||
| 365 | ```yaml | |
| 366 | jobs: | |
| 367 | deploy: | |
| 368 | image: alpine:3.22 | |
| 369 | secrets: [DEPLOY_TOKEN] | |
| 370 | steps: | |
| 371 | - run: ./deploy.sh # reads $DEPLOY_TOKEN | |
| 372 | ``` | |
| 373 | ||
| 374 | Declaring the name is what makes it present. A job that does not list a secret | |
| 375 | does not receive it, so a repository's values are never ambient in its steps. | |
| 376 | ||
| 377 | A name the repository has no value for does not fail the job: the variable is left | |
| 378 | **unset**, not empty, and the log says which one it went without. | |
| 379 | ||
| 380 | ``` | |
| 381 | vole: DEPLOY_TOKEN is not set for this repository, so this job runs without it. | |
| 382 | ``` | |
| 383 | ||
| 384 | That is what makes an optional secret optional. When one is required, the step says | |
| 385 | so, and the reader gets the message its author wrote: | |
| 386 | ||
| 387 | ```yaml | |
| 388 | steps: | |
| 389 | - run: | | |
| 390 | : "${DEPLOY_TOKEN:?DEPLOY_TOKEN is not set}" | |
| 391 | ./deploy.sh | |
| 392 | ``` | |
| 393 | ||
| 394 | A value is never copied into the workflow, into a container argument, or into a run | |
| 395 | record: the record names the version of each value the job ran with, so a result | |
| 396 | says which one it used. Values reach a step only as environment variables, and one | |
| 397 | is at most 128 KiB, which is as much as one environment variable can carry. | |
| 398 | ||
|
e82bbe7d
Keenan Nemetz
7d ago
|
399 | A value is also replaced with `***` in the log as it is written, including one a |
| 400 | step prints by accident. That is best effort: a re-encoded value is not caught, a | |
| 401 | value under four bytes is left alone, and an artifact or a site file is published | |
| 402 | exactly as the step wrote it. See | |
| 403 | [Workflow reference § secrets](reference/workflow-reference.md#secrets). | |
| 404 | ||
|
4ac42680
Keenan Nemetz
7d ago
|
405 | Anybody who can run a runner for a repository can read the values its jobs declare, |
| 406 | because that is the machine they arrive on. Secrets belong to the accounts that | |
|
e189a23e
Keenan Nemetz
22h ago
|
407 | approve work and allow CI, and the Secrets tab of the settings page that lists |
| 408 | them never shows one. | |
|
4ac42680
Keenan Nemetz
7d ago
|
409 | See [Accounts and access](accounts-and-access.md#roles). |
| 410 | ||
|
70b70264
Keenan Nemetz
15d ago
|
411 | ## Contributor branches |
| 412 | ||
| 413 | A push to `refs/heads/contrib/<name>/...` queues no work by itself. Those | |
|
ab326d3a
Keenan Nemetz
14d ago
|
414 | branches contain code from outside the repository's maintainer group, and running |
| 415 | a workflow executes it on a runner. CI is held until a maintainer or admin opens the | |
|
f6f4b1db
Keenan Nemetz
15d ago
|
416 | change request for the branch and chooses **Allow CI** there. |
|
70b70264
Keenan Nemetz
15d ago
|
417 | |
| 418 | The approval is keyed by the exact commit, not by the branch. A new commit or a | |
| 419 | force-push is a new decision, so the contributor cannot push once and inherit | |
| 420 | approval forever. The approval is stored in the CI queue database, so a | |
| 421 | reconciliation or a temporary queue outage does not silently run an unapproved | |
| 422 | tip. | |
| 423 | ||
| 424 | Allowing CI queues every workflow that applies to the branch. Workflows with no | |
| 425 | `on.push.branches` filter run on every branch, including names with slashes such | |
| 426 | as contributor branches. | |
| 427 | ||
| 428 | ## Check and run a workflow locally | |
| 429 | ||
| 430 | You can check workflows before pushing: | |
| 431 | ||
| 432 | ```sh | |
| 433 | vole workflow check | |
| 434 | vole workflow list | |
| 435 | ``` | |
| 436 | ||
| 437 | You can also run the steps without a server or a runner. From the repository root: | |
| 438 | ||
| 439 | ```sh | |
| 440 | vole workflow run # every job in every workflow | |
| 441 | vole workflow run build # every job in build.yaml | |
|
91e6a33a
Keenan Nemetz
14d ago
|
442 | vole workflow run build -j test # one job in one workflow |
|
70b70264
Keenan Nemetz
15d ago
|
443 | vole workflow run -j test # the job named test in every workflow |
| 444 | ``` | |
| 445 | ||
|
91e6a33a
Keenan Nemetz
14d ago
|
446 | A workflow can be named with or without its extension (`verify` or `verify.yaml`). The |
|
3b39a84c
Keenan Nemetz
14d ago
|
447 | command validates the workflow first, with the same checker a push uses, then |
|
70b70264
Keenan Nemetz
15d ago
|
448 | stops at the first failure and exits with that step's status, so it can wrap a |
|
3b39a84c
Keenan Nemetz
14d ago
|
449 | script. |
| 450 | ||
| 451 | The steps run in the image the job names, not on your machine. That is the point: | |
| 452 | the answer here is the answer CI gives, so a toolchain difference between "works | |
| 453 | for me" and a red run does not exist. An `image-file:` definition is built and | |
| 454 | cached by digest exactly as a runner would build it, and the working tree is bound | |
| 455 | into the container, so the steps see your uncommitted edits and leave their output | |
| 456 | where you would expect. | |
| 457 | ||
| 458 | The run needs a container runtime — Docker, rootless Podman, or nerdctl — and | |
| 459 | `--container-runtime` selects one (Docker by default). There is no mode that skips | |
| 460 | the container: a step that runs in a different environment proves nothing about | |
| 461 | CI. The default network is `bridge`, because a job that cannot fetch a dependency | |
| 462 | cannot run here either; `--container-network none` matches what a runner does by | |
| 463 | default. The container runs as your uid and gid; its filesystem is writable but | |
| 464 | thrown away when the run ends, and `/tmp` is a tmpfs. | |
| 465 | ||
| 466 | A local run mounts the same job cache as a runner, under the user cache directory | |
| 467 | (`$XDG_CACHE_HOME/vole/workflows`, usually `~/.cache/vole/workflows`), so a | |
| 468 | repeated run is as fast here as it is on a runner. As on a runner, the run sets no | |
| 469 | variables: the image's `env:` (or the step) decides whether `/cache` is used. See | |
| 470 | [Runners § Caching](runner.md#caching). `--cache-size` caps it (5G by default) and | |
| 471 | `--no-cache` empties it after every job. | |
| 472 | ||
|
eb07ff56
Keenan Nemetz
8d ago
|
473 | A local run still ignores triggers and `runs-on`, publishes no artifacts, |
|
d7958024
Keenan Nemetz
6d ago
|
474 | and deploys no site. It does apply the job's `network:` request, so a `bridge` job |
|
c1929d2f
Keenan Nemetz
5d ago
|
475 | needs `--container-network bridge` here too (the default), and it starts the job's |
| 476 | services on the same kind of private network a runner would. It spends the job's | |
|
e7aa0785
Keenan Nemetz
6d ago
|
477 | `timeout:` the way a runner does. It sets `CI=true` and `VOLE_CI=1`, gives each step |
| 478 | whatever the job's budget and its own timeout leave it, and reads no standard input. | |
| 479 | It reads the secrets its jobs declare from the instance the checkout's remote names, | |
| 480 | with your own credential, so a job that deploys behaves the same here as it does on | |
| 481 | a runner. Where it cannot — no remote, or no value stored — the variable is unset | |
| 482 | and the note says which one. Jobs run in declaration order and share the working | |
| 483 | tree, so a later job sees files an earlier one wrote; a pushed run queues each job | |
| 484 | separately, starts it from a clean checkout, and has no ordering between jobs. A | |
| 485 | local run is not an event, so it sets no `VOLE_EVENT*` variables and has no event | |
| 486 | payload. | |
|
70b70264
Keenan Nemetz
15d ago
|
487 | |
| 488 | ## Artifacts | |
| 489 | ||
| 490 | Use `artifacts:` to list files that a build should publish. Vole expands the | |
| 491 | patterns in the checkout after the steps pass, then uploads the files to the same | |
|
5ece70a5
Keenan Nemetz
10d ago
|
492 | storage used by LFS and CI artifacts. |
|
70b70264
Keenan Nemetz
15d ago
|
493 | |
| 494 | ```yaml | |
| 495 | artifacts: | |
| 496 | - path: dist/* | |
| 497 | ``` | |
| 498 | ||
| 499 | Artifacts appear on the run page with a download link: | |
| 500 | ||
| 501 | ```sh | |
|
b7396a11
Keenan Nemetz
2h ago
|
502 | curl -OJ http://host/demo/runs/01M5.../artifacts/tool-linux |
|
70b70264
Keenan Nemetz
15d ago
|
503 | sha256sum tool-linux |
| 504 | ``` | |
| 505 | ||
| 506 | A pattern that matches nothing fails the job. A build that stopped producing the | |
| 507 | declared file is exactly what the declaration is meant to catch. If two matches | |
| 508 | have the same base name, Vole refuses them so nothing is published under an | |
| 509 | unexpected name. | |
| 510 | ||
| 511 | A run artifact belongs to its run. It is not a release artifact. Moving one into a | |
| 512 | release is a separate step (`vole release attach`). Run artifacts are kept for as | |
| 513 | long as the run exists. | |
| 514 | ||
| 515 | ## Build a release | |
| 516 | ||
| 517 | A tag push can build a release. Mark the artifacts `release: true`: | |
| 518 | ||
| 519 | ```yaml | |
| 520 | version: 1 | |
| 521 | on: | |
| 522 | push: | |
| 523 | tags: ['v*'] | |
| 524 | jobs: | |
| 525 | release: | |
| 526 | image: golang:1.27-bookworm | |
| 527 | steps: | |
| 528 | - name: build | |
| 529 | run: make dist | |
| 530 | artifacts: | |
| 531 | - path: dist/* | |
| 532 | release: true | |
| 533 | ``` | |
| 534 | ||
| 535 | ```sh | |
| 536 | git tag -a v1.0.0 -m "first release" # needs notes and a tagger | |
| 537 | git push origin refs/tags/v1.0.0 # the runner builds and publishes | |
| 538 | ``` | |
| 539 | ||
|
b312e166
Keenan Nemetz
10d ago
|
540 | The release commit on the `vole` branch names the run that produced the files |
| 541 | (`release: attach ... from run 01M5...`), so `git log vole` still shows which build | |
| 542 | shipped them. The run's full record is in `data/runs.db`, not on the branch. A | |
| 543 | lightweight tag is refused: it has no release notes and no tagger. A tag run never | |
| 544 | counts as a branch's required check, even if the tag points at that branch's tip. | |
|
70b70264
Keenan Nemetz
15d ago
|
545 | |
| 546 | See [Releases](releases.md) for the release page and artifact rules. | |
| 547 | ||
| 548 | ## Publish a website | |
| 549 | ||
| 550 | `pages:` publishes a directory as the repository's website, using the same rules | |
| 551 | as `vole pages deploy`: the branch contains exactly those files, so a page removed | |
| 552 | from the build stops being served. | |
| 553 | ||
| 554 | ```yaml | |
| 555 | pages: | |
| 556 | path: site/ | |
| 557 | ``` | |
| 558 | ||
| 559 | The commit names the build (`site: main from run 01M5... (6beaa53)`), so | |
| 560 | `git log pages` shows what put that tree there. | |
| 561 | ||
| 562 | Vole refuses the deploy if the run is no longer the newest commit on its branch. | |
| 563 | An older build finishing late must not silently roll the site back. The run log | |
| 564 | explains why: | |
| 565 | ||
| 566 | ```text | |
| 567 | this run built f1f415ec but main is at 6beaa530 now; the site was not deployed | |
| 568 | ``` | |
| 569 | ||
| 570 | Only one job per workflow can publish the site. The branch name is set by the | |
| 571 | instance (`pages_ref`), not by the workflow. | |
| 572 | ||
| 573 | ## What happens to a queued workflow | |
| 574 | ||
|
0c14f078
Keenan Nemetz
6d ago
|
575 | Pushing again cancels queued runs for the commit it replaced. It never stops a run |
|
deb024d4
Keenan Nemetz
6d ago
|
576 | that is already in progress; a maintainer can stop that one from its page or with |
| 577 | `vole run cancel <id>`, and the runner stops at its next heartbeat. The attempt that | |
|
1d06b008
Keenan Nemetz
5d ago
|
578 | comes back is kept as canceled. |
|
70b70264
Keenan Nemetz
15d ago
|
579 | |
|
1d06b008
Keenan Nemetz
5d ago
|
580 | A run can have more than one attempt. Each time a runner starts the job it records a |
| 581 | new attempt, and a runner that disappears hands the job back for another one. After | |
|
70c9ad4e
Keenan Nemetz
1h ago
|
582 | five attempts the run fails instead of cycling. |
| 583 | ||
| 584 | An attempt that never reaches the job's first step ends as **could not run**, not | |
| 585 | **failed**: the image could not be built, the container would not start, or the | |
| 586 | network the job asked for could not be made. The run page shows the runner's own | |
| 587 | reason, and the check says the commit is not what failed, so a required check waits | |
| 588 | instead of going red. It is a rerun away from finishing. A step that exits non-zero | |
| 589 | is still a failure, and still reds the check, because that one is the code's. A finished run can be queued again | |
|
1d06b008
Keenan Nemetz
5d ago
|
590 | with **Rerun** on its page or `vole run rerun <id>`; that is another attempt of the |
| 591 | same pinned workflow, image, and secrets, not a new run. A queued or running run | |
| 592 | cannot be rerun. The run page lists each attempt's log, and the log URL takes | |
| 593 | `?attempt=N`. | |
| 594 | ||
|
70c9ad4e
Keenan Nemetz
1h ago
|
595 | A run's status is `queued`, `running`, `success`, `failed`, `canceled`, or `error`, |
| 596 | which the page calls **could not run**. Only the last two are not a verdict about the | |
| 597 | commit in the same way: a canceled run was stopped on purpose, and an `error` run | |
| 598 | never started. | |
| 599 | ||
|
1d06b008
Keenan Nemetz
5d ago
|
600 | Finished runs are stored in `data/runs.db`, not on the repository's `vole` branch. |
|
70b70264
Keenan Nemetz
15d ago
|
601 | For what happens when a runner disappears, how work is requeued, and how finished |
| 602 | history is rebuilt, see [Runners § Scheduling and the queue](runner.md#scheduling-and-the-queue). |