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/reference/workflow-reference.md +38−1
| @@ -97,6 +97,7 @@ letter. At least one job is required. | |||
| 97 | 97 | | `image` | string | one of `image` / `image-file` | — | Registry image the job runs in. | | |
| 98 | 98 | | `image-file` | string | one of `image` / `image-file` | — | Name of a definition under `.vole/images/`. | | |
| 99 | 99 | | `runs-on` | string or list of strings | no | any runner | Runner labels the job requires. | | |
| 100 | | `env` | mapping | no | none | Environment every step of the job runs with. | | ||
| 100 | 101 | | `steps` | list | yes | — | Commands the job runs. At least one is required. | | |
| 101 | 102 | | `artifacts` | list | no | none | Files the job publishes. | | |
| 102 | 103 | | `pages` | mapping | no | none | Directory published as the repository's website. | | |
| @@ -166,6 +167,39 @@ with a letter or digit. | |||
| 166 | 167 | See [Runners § Start a runner](../runner.md#start-a-runner) for how a machine | |
| 167 | 168 | advertises labels. | |
| 168 | 169 | ||
| 170 | ### `env` | ||
| 171 | |||
| 172 | Environment variables for every step of the job, on top of the image's own. Later | ||
| 173 | values win, so a job can change something its image set: | ||
| 174 | |||
| 175 | ```yaml | ||
| 176 | jobs: | ||
| 177 | test: | ||
| 178 | image: golang:1.27-bookworm | ||
| 179 | env: | ||
| 180 | GOCACHE: /cache/go-build | ||
| 181 | GOFLAGS: "-mod=readonly" | ||
| 182 | steps: | ||
| 183 | - run: go test ./... | ||
| 184 | ``` | ||
| 185 | |||
| 186 | | Constraint | Value | | ||
| 187 | |---|---| | ||
| 188 | | Maximum entries | 64 | | ||
| 189 | | Key syntax | `^[A-Za-z_][A-Za-z0-9_]*$` | | ||
| 190 | | Maximum value size | 8 KiB | | ||
| 191 | | Line endings | Values must be one line; `CR`/`LF` are rejected | | ||
| 192 | |||
| 193 | `CI`, `VOLE_CI`, `GIT_TERMINAL_PROMPT`, `HOME` and `TMPDIR` are set by the runner and | ||
| 194 | cannot be replaced: the sandbox depends on them, so the push that tries is refused | ||
| 195 | rather than silently disagreeing with it. | ||
| 196 | |||
| 197 | This is how a job opts in to something the runner provides but does not decide for | ||
| 198 | it. The job cache is the one that matters today: the runner mounts a directory at | ||
| 199 | `/cache` and sets no variable for it, so pointing `GOCACHE` (or `CARGO_HOME`, or | ||
| 200 | `npm_config_cache`) there is what makes the cache shared. See | ||
| 201 | [Runners § Caching](../runner.md#caching). | ||
| 202 | |||
| 169 | 203 | ### `steps` | |
| 170 | 204 | ||
| 171 | 205 | A non-empty list of shell steps, at most 100. Steps run in order. A step that exits | |
| @@ -261,7 +295,7 @@ longer the newest commit on its branch. See [Workflows § Publish a website](../ | |||
| 261 | 295 | - The commit, workflow blob, and image definition are pinned when a run is queued. | |
| 262 | 296 | Later edits do not change that run. | |
| 263 | 297 | - `vole workflow check` and `vole workflow run` use the same validation as a push. | |
| 264 | A local run ignores `on.push`, `runs-on`, and the image. | ||
| 298 | A local run executes in the job's image and ignores `on.push` and `runs-on`. | ||
| 265 | 299 | ||
| 266 | 300 | ## Complete example | |
| 267 | 301 | ||
| @@ -275,6 +309,9 @@ jobs: | |||
| 275 | 309 | test: | |
| 276 | 310 | runs-on: [linux, arm64] | |
| 277 | 311 | image: golang:1.27-bookworm@sha256:... | |
| 312 | env: | ||
| 313 | GOCACHE: /cache/go-build | ||
| 314 | GOMODCACHE: /cache/go-mod | ||
| 278 | 315 | steps: | |
| 279 | 316 | - name: unit tests | |
| 280 | 317 | run: go test ./... | |