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.
Require an image for every CI job
70b702649b875d841e294f5a085ca784d43e5bc4 · Keenan Nemetz · 15d ago · parent 7da9111e
Require an image for every CI job Runners now require an OCI runtime, and each workflow job must name either a registry image or an image-file definition under .vole/images. Definitions are validated at push time, pinned with the run, built locally by the runner, and cached by definition digest. Records and the run page carry the image and definition digest. Split the CI guide into workflows and runners, and add reference pages for workflow and image definition fields.
docs/workflows.md +247
| @@ -0,0 +1,247 @@ | |||
| 1 | # Workflows | ||
| 2 | |||
| 3 | A workflow describes what CI runs: which branches and tags trigger it, which | ||
| 4 | steps each job executes, and what it publishes. Machines and execution belong to | ||
| 5 | [runners](runner.md); this page is about the project side. | ||
| 6 | |||
| 7 | For every field, limit, and default, see the [Workflow reference](reference/workflow-reference.md). | ||
| 8 | |||
| 9 | ## Add a workflow | ||
| 10 | |||
| 11 | Workflows live in `.vole/workflows/*.yaml`. When you push a new workflow, Vole | ||
| 12 | checks it before accepting the push, so a broken workflow is rejected early. | ||
| 13 | |||
| 14 | ```yaml | ||
| 15 | version: 1 | ||
| 16 | on: | ||
| 17 | push: | ||
| 18 | branches: [main] | ||
| 19 | jobs: | ||
| 20 | build: | ||
| 21 | runs-on: [linux, arm64] # only a runner with both labels takes this job | ||
| 22 | image: golang:1.27-bookworm | ||
| 23 | steps: | ||
| 24 | - name: test | ||
| 25 | run: go test ./... | ||
| 26 | - name: build | ||
| 27 | run: make dist | ||
| 28 | artifacts: | ||
| 29 | - path: dist/* | ||
| 30 | pages: | ||
| 31 | path: site/ # publish this directory as the repository's site | ||
| 32 | ``` | ||
| 33 | |||
| 34 | Every job must name the environment it runs in, with exactly one of `image:` or | ||
| 35 | `image-file:`. A workflow that names neither fails the push that introduced it. | ||
| 36 | See [Images](#images) for the two forms. | ||
| 37 | |||
| 38 | Triggers work like this: | ||
| 39 | |||
| 40 | - `on.push.branches` limits a workflow to certain branches. | ||
| 41 | - `on.push.tags` limits it to tag pushes. | ||
| 42 | - A workflow with neither runs on every branch. | ||
| 43 | - A workflow that names `tags:` runs only on tags. | ||
| 44 | |||
| 45 | `runs-on` asks for a runner with every label in the list. A job with no `runs-on` | ||
| 46 | can run anywhere. See [Runners § Start a runner](runner.md#start-a-runner) for what the | ||
| 47 | labels mean on the machine. | ||
| 48 | |||
| 49 | When a push starts a job, Vole remembers the exact commit, the exact workflow | ||
| 50 | file, and the exact image definition. Editing any of them later does not change a | ||
| 51 | job that is already queued. | ||
| 52 | |||
| 53 | ## Images | ||
| 54 | |||
| 55 | A job can name a registry image directly: | ||
| 56 | |||
| 57 | ```yaml | ||
| 58 | jobs: | ||
| 59 | build: | ||
| 60 | image: golang:1.27-bookworm@sha256:... | ||
| 61 | ``` | ||
| 62 | |||
| 63 | Or keep a reusable definition under `.vole/images/` and reference it by name: | ||
| 64 | |||
| 65 | ```yaml | ||
| 66 | # .vole/images/go.yaml | ||
| 67 | version: 1 | ||
| 68 | from: golang:1.27-bookworm | ||
| 69 | env: | ||
| 70 | CGO_ENABLED: "0" | ||
| 71 | run: | ||
| 72 | - apt-get update && apt-get install -y --no-install-recommends jq | ||
| 73 | ``` | ||
| 74 | |||
| 75 | ```yaml | ||
| 76 | jobs: | ||
| 77 | build: | ||
| 78 | image-file: go | ||
| 79 | steps: | ||
| 80 | - run: go test ./... | ||
| 81 | ``` | ||
| 82 | |||
| 83 | `image-file: go` resolves `.vole/images/go.yaml`. For the complete definition | ||
| 84 | schema, see the [Image reference](reference/image-reference.md). A definition has: | ||
| 85 | |||
| 86 | | Field | Meaning | | ||
| 87 | |---|---| | ||
| 88 | | `version` | Required; currently `1` | | ||
| 89 | | `from` | Required base image reference | | ||
| 90 | | `env` | Optional environment variables for the image | | ||
| 91 | | `run` | Optional build commands; each becomes a `RUN` | | ||
| 92 | |||
| 93 | Definitions are validated when the workflow is pushed, including definitions | ||
| 94 | that are not referenced, and they are pinned with the run. The runner builds the | ||
| 95 | image locally and tags it by the definition digest, so later jobs with the same | ||
| 96 | definition reuse the build. See [Runners § Images](runner.md#images-and-image-builds) | ||
| 97 | for the execution, caching, and security details. | ||
| 98 | |||
| 99 | An image definition is code: its build runs on the runner through the OCI | ||
| 100 | builder, so approving a contributor tip that changes a definition also approves | ||
| 101 | that build. Prefer rootless Podman and pin the base image by digest. | ||
| 102 | |||
| 103 | The image is the dependency manifest. It should contain the toolchains and system | ||
| 104 | packages the builds need, plus `/bin/sh` and a POSIX `sleep` (Vole runs shell steps | ||
| 105 | and keeps the container alive between them). Pin the base image by digest; a | ||
| 106 | mutable tag changes what a build means. | ||
| 107 | |||
| 108 | ## Contributor branches | ||
| 109 | |||
| 110 | A push to `refs/heads/contrib/<name>/...` queues no work by itself. Those | ||
| 111 | branches contain code from outside the project's maintainer group, and running a | ||
| 112 | workflow executes it on a runner. CI is held until a maintainer or admin opens the | ||
| 113 | repository page and chooses **Allow CI for this tip** next to the contributor | ||
| 114 | branch. | ||
| 115 | |||
| 116 | The approval is keyed by the exact commit, not by the branch. A new commit or a | ||
| 117 | force-push is a new decision, so the contributor cannot push once and inherit | ||
| 118 | approval forever. The approval is stored in the CI queue database, so a | ||
| 119 | reconciliation or a temporary queue outage does not silently run an unapproved | ||
| 120 | tip. | ||
| 121 | |||
| 122 | Allowing CI queues every workflow that applies to the branch. Workflows with no | ||
| 123 | `on.push.branches` filter run on every branch, including names with slashes such | ||
| 124 | as contributor branches. | ||
| 125 | |||
| 126 | ## Check and run a workflow locally | ||
| 127 | |||
| 128 | You can check workflows before pushing: | ||
| 129 | |||
| 130 | ```sh | ||
| 131 | vole workflow check | ||
| 132 | vole workflow list | ||
| 133 | ``` | ||
| 134 | |||
| 135 | You can also run the steps without a server or a runner. From the repository root: | ||
| 136 | |||
| 137 | ```sh | ||
| 138 | vole workflow run # every job in every workflow | ||
| 139 | vole workflow run build # every job in build.yaml | ||
| 140 | vole workflow run ci -j test # one job in one workflow | ||
| 141 | vole workflow run -j test # the job named test in every workflow | ||
| 142 | ``` | ||
| 143 | |||
| 144 | A workflow can be named with or without its extension (`ci` or `ci.yaml`). Steps | ||
| 145 | run in the working tree, in the order the workflow declares them. The command | ||
| 146 | stops at the first failure and exits with that step's status, so it can wrap a | ||
| 147 | script. It validates the workflow first, with the same checker a push uses. | ||
| 148 | |||
| 149 | A local run is a check of the commands, not a rehearsal of the push. It ignores | ||
| 150 | `on.push` filters, `runs-on`, and the job image, and it does not publish artifacts | ||
| 151 | or deploy a site. It sets `CI=true`, `VOLE_CI=1`, and `VOLE_CI_LOCAL=1`, gives | ||
| 152 | each step its declared timeout, and reads no standard input. Jobs share your | ||
| 153 | working tree, so a later job sees files an earlier one wrote; a pushed run starts | ||
| 154 | each job from a clean checkout in its image. | ||
| 155 | |||
| 156 | ## Artifacts | ||
| 157 | |||
| 158 | Use `artifacts:` to list files that a build should publish. Vole expands the | ||
| 159 | patterns in the checkout after the steps pass, then uploads the files to the same | ||
| 160 | storage used by LFS and CI logs. | ||
| 161 | |||
| 162 | ```yaml | ||
| 163 | artifacts: | ||
| 164 | - path: dist/* | ||
| 165 | ``` | ||
| 166 | |||
| 167 | Artifacts appear on the run page with a download link: | ||
| 168 | |||
| 169 | ```sh | ||
| 170 | curl -OJ http://host/r/demo/runs/01M5.../artifacts/tool-linux | ||
| 171 | sha256sum tool-linux | ||
| 172 | ``` | ||
| 173 | |||
| 174 | A pattern that matches nothing fails the job. A build that stopped producing the | ||
| 175 | declared file is exactly what the declaration is meant to catch. If two matches | ||
| 176 | have the same base name, Vole refuses them so nothing is published under an | ||
| 177 | unexpected name. | ||
| 178 | |||
| 179 | A run artifact belongs to its run. It is not a release artifact. Moving one into a | ||
| 180 | release is a separate step (`vole release attach`). Run artifacts are kept for as | ||
| 181 | long as the run exists. | ||
| 182 | |||
| 183 | ## Build a release | ||
| 184 | |||
| 185 | A tag push can build a release. Mark the artifacts `release: true`: | ||
| 186 | |||
| 187 | ```yaml | ||
| 188 | version: 1 | ||
| 189 | on: | ||
| 190 | push: | ||
| 191 | tags: ['v*'] | ||
| 192 | jobs: | ||
| 193 | release: | ||
| 194 | image: golang:1.27-bookworm | ||
| 195 | steps: | ||
| 196 | - name: build | ||
| 197 | run: make dist | ||
| 198 | artifacts: | ||
| 199 | - path: dist/* | ||
| 200 | release: true | ||
| 201 | ``` | ||
| 202 | |||
| 203 | ```sh | ||
| 204 | git tag -a v1.0.0 -m "first release" # needs notes and a tagger | ||
| 205 | git push origin refs/tags/v1.0.0 # the runner builds and publishes | ||
| 206 | ``` | ||
| 207 | |||
| 208 | The record on the `vole` branch names the build (`release: v1.0.0 artifacts from | ||
| 209 | run 01M5...`), so `git log vole` shows which run shipped the files. A lightweight | ||
| 210 | tag is refused: it has no release notes and no tagger. A tag run never counts as a | ||
| 211 | branch's required check, even if the tag points at that branch's tip. | ||
| 212 | |||
| 213 | See [Releases](releases.md) for the release page and artifact rules. | ||
| 214 | |||
| 215 | ## Publish a website | ||
| 216 | |||
| 217 | `pages:` publishes a directory as the repository's website, using the same rules | ||
| 218 | as `vole pages deploy`: the branch contains exactly those files, so a page removed | ||
| 219 | from the build stops being served. | ||
| 220 | |||
| 221 | ```yaml | ||
| 222 | pages: | ||
| 223 | path: site/ | ||
| 224 | ``` | ||
| 225 | |||
| 226 | The commit names the build (`site: main from run 01M5... (6beaa53)`), so | ||
| 227 | `git log pages` shows what put that tree there. | ||
| 228 | |||
| 229 | Vole refuses the deploy if the run is no longer the newest commit on its branch. | ||
| 230 | An older build finishing late must not silently roll the site back. The run log | ||
| 231 | explains why: | ||
| 232 | |||
| 233 | ```text | ||
| 234 | this run built f1f415ec but main is at 6beaa530 now; the site was not deployed | ||
| 235 | ``` | ||
| 236 | |||
| 237 | Only one job per workflow can publish the site. The branch name is set by the | ||
| 238 | instance (`pages_ref`), not by the workflow. | ||
| 239 | |||
| 240 | ## What happens to a queued workflow | ||
| 241 | |||
| 242 | Pushing again cancels queued runs for the commit it replaced. It never cancels a | ||
| 243 | run that is already in progress. Finished runs become records on the repository's | ||
| 244 | `vole` branch. | ||
| 245 | |||
| 246 | For what happens when a runner disappears, how work is requeued, and how finished | ||
| 247 | history is rebuilt, see [Runners § Scheduling and the queue](runner.md#scheduling-and-the-queue). | ||