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.
Bound and reclaim the runner's saved checkouts
f594f1a16f6f8a8a3ee2867a44249b72a2c76c96 · Keenan Nemetz · 5d ago · parent de84c32b
Bound and reclaim the runner's saved checkouts The runner already downloaded each repository once into --dir and cloned every job from that copy, but nothing kept the copies bounded: they grew with every tag, every branch, and every commit a job pinned by id, and a force-push left the old history reachable through the remote-tracking reflogs. A long-lived runner filled its disk and then could not check anything out at all. Repack a copy that is most of the cap, dropping the objects no branch or tag can reach and the temporary refs a killed runner leaves behind; then delete the least recently used copy when the cap is still reached. --git-cache-size sets the cap (10G by default, 0 keeps every copy). The repository this runner is about to build is never the one deleted, and a copy another process holds is passed over. Reading and writing a saved checkout now takes a per-repository lock, so a repack or a deletion in one runner process cannot run underneath another's clone, or underneath the site and release pushes that are written from the same cache. The first clone skips the working-tree checkout, which nothing ever read.
docs/runner.md +28−3
| @@ -132,7 +132,9 @@ instance or kept on the runner, so each of those channels has a bound: | |||
| 132 | 132 | evicting whole namespaces. Each repository gets its own namespace, and each | |
| 133 | 133 | contributor account gets its own inside a repository, so one contributor's | |
| 134 | 134 | branch cannot seed the cache another contributor's branch builds from. A | |
| 135 | protected branch never shares a cache with a contributor branch. | ||
| 135 | protected branch never shares a cache with a contributor branch. The saved | ||
| 136 | checkouts jobs clone from are bounded separately; see | ||
| 137 | [Saved checkouts](#saved-checkouts). | ||
| 136 | 138 | - **Container.** `--container-memory`, `--container-cpus` and `--container-pids` | |
| 137 | 139 | pass the runtime's own limits. `--container-network` is what this machine can | |
| 138 | 140 | offer — `none` (the default) or `bridge` — and a job's own `network:` decides what | |
| @@ -165,8 +167,9 @@ that build. Prefer rootless Podman, and pin the base image by digest. | |||
| 165 | 167 | One container is created per job and removed afterward. The workflow's steps run | |
| 166 | 168 | inside it, but the runner process stays on the host: the SSH key, the trusted Git | |
| 167 | 169 | cache, and the finish channel are never exposed to the job. Each job gets a fresh | |
| 168 | clone, so a job cannot leave a Git hook or a modified checkout behind for the next | ||
| 169 | one. | ||
| 170 | clone of the runner's saved copy of the repository (see | ||
| 171 | [Saved checkouts](#saved-checkouts)), so a job cannot leave a Git hook or a modified | ||
| 172 | checkout behind for the next one. | ||
| 170 | 173 | ||
| 171 | 174 | Container defaults are intentionally closed: | |
| 172 | 175 | ||
| @@ -331,6 +334,28 @@ the runner trims at startup and after each job. `--no-cache` keeps nothing | |||
| 331 | 334 | between jobs: the directory is still mounted — an image that points at `/cache` | |
| 332 | 335 | has to work either way — but it is emptied after every job. | |
| 333 | 336 | ||
| 337 | ## Saved checkouts | ||
| 338 | |||
| 339 | The first job for a repository downloads it once, and the runner keeps that copy | ||
| 340 | under `--dir`. A later job clones from the copy and fetches only the commits it does | ||
| 341 | not have — usually none at all — so history crosses the network once instead of once | ||
| 342 | per run. The copy is never mounted into a job: the workflow gets a clone with its own | ||
| 343 | `.git`, which is why a hook it writes still dies with the job. | ||
| 344 | |||
| 345 | Two things keep the copies from filling the disk: | ||
| 346 | |||
| 347 | - **Repacking.** A copy that is most of the cap is repacked. That drops history no | ||
| 348 | branch or tag can reach any more: commits from a force-push or a deleted branch, | ||
| 349 | and a commit a job once pinned by id. Nothing a job can still ask for is touched, | ||
| 350 | so the next job for the same revision is still a local copy with no download. | ||
| 351 | - **Deleting.** When the cap is still reached, the copy nobody has built from in the | ||
| 352 | longest time is deleted, whole. The next job for that repository downloads it | ||
| 353 | again, which is why it is the last resort rather than the first. | ||
| 354 | |||
| 355 | `--git-cache-size` caps all saved checkouts together (10G by default; `0` keeps | ||
| 356 | every one). A copy another runner process is using is passed over, and the | ||
| 357 | repository this runner is about to build is never the one deleted. | ||
| 358 | |||
| 334 | 359 | ## Who can run jobs and report results | |
| 335 | 360 | ||
| 336 | 361 | A job runs repository code, so Vole uses a runner allowlist for both taking a job | |