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.
Add packaging, a CI workflow, and containerised local runs
3b39a84c913cabe15bfa6a60dd176ff2dd28d241 · Keenan Nemetz · 14d ago · parent 30a72490
Add packaging, a CI workflow, and containerised local runs A v* tag now builds a .deb and a .rpm for amd64 and arm64 with nfpm and attaches them, with checksums, to the release. The package ships the systemd unit, a config at /etc/vole/vole.toml, and a postinstall that creates the service account and prepares the instance that config names; `vole admin init --config` is what makes that possible, and the binary's version is stamped from the tag instead of the source tree. scripts/ci.sh becomes .vole/workflows/ci.yaml, so `vole workflow run ci` executes exactly what a push runs, and the Markdown link check moves to scripts/check-links.py. Local runs now always execute in the job image and share ci/container with the runner; --no-container is gone, because a run in a different environment proves nothing about CI. The runner mounts a per-repository namespace at /cache and sets no variable for it: a job opts in through its image's env or the new job-level env, which is pinned with the queue and delivered with the lease. Whole namespaces are evicted least-recently-written first so a module cache is never half-deleted, and contributor branches get their own namespace so an untrusted tip cannot seed the cache a protected branch builds into. Running the suite in that container found four bugs worth naming: /tmp was mounted noexec, which broke `go test` and `go run`; the read-only root is gone, because images make toolchain directories world-writable deliberately and the mount set is what protects the host; OpenSSH needs a passwd entry for the job's uid; and a lost metadata push reported as "cannot lock ref" was not classified as a race, so concurrent CLI writes gave up on the first collision. That retry now backs off exponentially with jitter.
docs/workflows.md +51−10
| @@ -105,6 +105,26 @@ packages the builds need, plus `/bin/sh` and a POSIX `sleep` (Vole runs shell st | |||
| 105 | 105 | and keeps the container alive between them). Pin the base image by digest; a | |
| 106 | 106 | mutable tag changes what a build means. | |
| 107 | 107 | ||
| 108 | ## Environment | ||
| 109 | |||
| 110 | `env` on a job applies to every step, after the image's own, so the job wins: | ||
| 111 | |||
| 112 | ```yaml | ||
| 113 | jobs: | ||
| 114 | test: | ||
| 115 | image: golang:1.27-bookworm | ||
| 116 | env: | ||
| 117 | GOCACHE: /cache/go-build | ||
| 118 | steps: | ||
| 119 | - run: go test ./... | ||
| 120 | ``` | ||
| 121 | |||
| 122 | The runner sets `CI=true`, `VOLE_CI=1`, `HOME=/tmp`, `TMPDIR=/tmp`, and | ||
| 123 | `GIT_TERMINAL_PROMPT=0`; a workflow that tries to replace one of those is refused at | ||
| 124 | push time. Everything else is the workflow's. This is how a job opts in to the | ||
| 125 | cache the runner mounts at `/cache`, and how a job using a registry image gets it | ||
| 126 | without an image definition. | ||
| 127 | |||
| 108 | 128 | ## Contributor branches | |
| 109 | 129 | ||
| 110 | 130 | A push to `refs/heads/contrib/<name>/...` queues no work by itself. Those | |
| @@ -140,17 +160,38 @@ vole workflow run ci -j test # one job in one workflow | |||
| 140 | 160 | vole workflow run -j test # the job named test in every workflow | |
| 141 | 161 | ``` | |
| 142 | 162 | ||
| 143 | A workflow can be named with or without its extension (`ci` or `ci.yaml`). Steps | ||
| 144 | run in the working tree, in the order the workflow declares them. The command | ||
| 163 | A workflow can be named with or without its extension (`ci` or `ci.yaml`). The | ||
| 164 | command validates the workflow first, with the same checker a push uses, then | ||
| 145 | 165 | stops at the first failure and exits with that step's status, so it can wrap a | |
| 146 | script. It validates the workflow first, with the same checker a push uses. | ||
| 147 | |||
| 148 | A local run is a check of the commands, not a rehearsal of the push. It ignores | ||
| 149 | `on.push` filters, `runs-on`, and the job image, and it does not publish artifacts | ||
| 150 | or deploy a site. It sets `CI=true`, `VOLE_CI=1`, and `VOLE_CI_LOCAL=1`, gives | ||
| 151 | each step its declared timeout, and reads no standard input. Jobs share your | ||
| 152 | working tree, so a later job sees files an earlier one wrote; a pushed run starts | ||
| 153 | each job from a clean checkout in its image. | ||
| 166 | script. | ||
| 167 | |||
| 168 | The steps run in the image the job names, not on your machine. That is the point: | ||
| 169 | the answer here is the answer CI gives, so a toolchain difference between "works | ||
| 170 | for me" and a red run does not exist. An `image-file:` definition is built and | ||
| 171 | cached by digest exactly as a runner would build it, and the working tree is bound | ||
| 172 | into the container, so the steps see your uncommitted edits and leave their output | ||
| 173 | where you would expect. | ||
| 174 | |||
| 175 | The run needs a container runtime — Docker, rootless Podman, or nerdctl — and | ||
| 176 | `--container-runtime` selects one (Docker by default). There is no mode that skips | ||
| 177 | the container: a step that runs in a different environment proves nothing about | ||
| 178 | CI. The default network is `bridge`, because a job that cannot fetch a dependency | ||
| 179 | cannot run here either; `--container-network none` matches what a runner does by | ||
| 180 | default. The container runs as your uid and gid; its filesystem is writable but | ||
| 181 | thrown away when the run ends, and `/tmp` is a tmpfs. | ||
| 182 | |||
| 183 | A local run mounts the same job cache as a runner, under the user cache directory | ||
| 184 | (`$XDG_CACHE_HOME/vole/workflows`, usually `~/.cache/vole/workflows`), so a | ||
| 185 | repeated run is as fast here as it is on a runner. As on a runner, the run sets no | ||
| 186 | variables: the image's `env:` (or the step) decides whether `/cache` is used. See | ||
| 187 | [Runners § Caching](runner.md#caching). `--cache-size` caps it (5G by default) and | ||
| 188 | `--no-cache` empties it after every job. | ||
| 189 | |||
| 190 | A local run still ignores `on.push` filters and `runs-on`, publishes no artifacts, | ||
| 191 | and deploys no site. It sets `CI=true` and `VOLE_CI=1`, gives each step its | ||
| 192 | declared timeout, and reads no standard input. Jobs run in declaration order and | ||
| 193 | share the working tree, so a later job sees files an earlier one wrote; a pushed | ||
| 194 | run starts each job from a clean checkout. | ||
| 154 | 195 | ||
| 155 | 196 | ## Artifacts | |
| 156 | 197 | ||