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.
Phase 7 slice 6: a job declares its outputs, the runner publishes them
213f4d6dc6707cc9199f97acdacd617740bfb84d · Keenan Nemetz · 17d ago · parent 4c0f2aef
Phase 7 slice 6: a job declares its outputs, the runner publishes them
Workflow artifacts are a declaration, not a CLI call in a step. A workflow is
validated at the push that introduces it, so a bad pattern fails the push; a
shell command calling vole would fail at run time, need the binary on a runner's
PATH, and get idempotency wrong under retries.
- workflow: artifacts: [{path: dist/*}] on a job, validated at push — relative,
no '..', no '**', no trailing slash, a glob that parses. Patterns are pinned
into the queued run beside the pinned steps
- runner: expands the patterns in its checkout after the steps pass (one segment
at a time, .git excluded), uploads through the same SSH object transfer git-lfs
uses, and reports the list with its finish. A pattern matching nothing fails the
job: a silent empty artifact is the failure found a week later
- server: the artifact list rides the finish message, so a run's outputs and its
result are one fact; nothing is recorded unless the lease is still held, and
every oid must be in the store at the stated size, or the finish is refused
- storage: run_artifacts rows point at objects in the existing store. A build
output is machine-written state, so it does not touch the metadata branch; the
queue is its only index, and gc marks it from there the way it marks logs
- web: a run page (status, job, sha, log, artifacts) and
/r/<repo>/runs/<id>/artifacts/<name> served from the store, ETag = oid, always
an attachment, nosniff, Range. The CI table now links the run rather than only
its log
Measured through the binary on a real instance: a two-file dist/* declaration ran
on a real runner, both files uploaded over SSH, the run page served them with a
matching sha256, and vole gc --dry-run reported 3 objects, 3 reachable roots (the
log and both artifacts), removing nothing.
README.md +58−3
| @@ -45,8 +45,9 @@ Working today: | |||
| 45 | 45 | page with its patch, and a change request with the diff that would land | |
| 46 | 46 | - CI: workflows in `.vole/workflows/*.yaml`, validated by the push that introduces | |
| 47 | 47 | them. A push queues jobs; `vole runner` leases one over SSH with the same key, runs | |
| 48 | the shell steps, heartbeats and reports the log. Protected branches can require | ||
| 49 | green checks as well as approvals | ||
| 48 | the shell steps, heartbeats and reports the log. A job can declare `artifacts:` | ||
| 49 | (path patterns), which the runner uploads and the run page serves. Protected | ||
| 50 | branches can require green checks as well as approvals | ||
| 50 | 51 | - LFS: large files stay out of the git object database and live in one | |
| 51 | 52 | content-addressed store per instance. Uploads ride the SSH key | |
| 52 | 53 | (`git-lfs-transfer`); downloads come through the HTTP clone URL, anonymously for a | |
| @@ -67,7 +68,8 @@ Working today: | |||
| 67 | 68 | - An instance ed25519 key generated at first boot (used for metadata signing in | |
| 68 | 69 | Phase 7) | |
| 69 | 70 | ||
| 70 | Not yet: workflow artifact uploads, LFS locking. | ||
| 71 | Not yet: LFS locking, and retention for run artifacts (they live as long as the run | ||
| 72 | does). | ||
| 71 | 73 | Signature verification is deferred, | |
| 72 | 74 | not planned: the SSH handshake already establishes who pushed, so signing approvals | |
| 73 | 75 | would restate it for a verifier this deployment does not have (see the roadmap). | |
| @@ -195,6 +197,59 @@ admin can, and that is audited). The names in `meta.json` — `author` on an iss | |||
| 195 | 197 | request — are display fields and carry no authority; the approval path is the only | |
| 196 | 198 | name that decides who may land a commit. | |
| 197 | 199 | ||
| 200 | ## CI | ||
| 201 | |||
| 202 | Workflows live in `.vole/workflows/*.yaml`, are validated by the push that introduces | ||
| 203 | them, and run on a runner you start anywhere: | ||
| 204 | |||
| 205 | ```yaml | ||
| 206 | version: 1 | ||
| 207 | on: | ||
| 208 | push: | ||
| 209 | branches: [main] | ||
| 210 | jobs: | ||
| 211 | build: | ||
| 212 | steps: | ||
| 213 | - name: test | ||
| 214 | run: go test ./... | ||
| 215 | - name: build | ||
| 216 | run: make dist | ||
| 217 | artifacts: | ||
| 218 | - path: dist/* | ||
| 219 | ``` | ||
| 220 | |||
| 221 | ```sh | ||
| 222 | vole runner --remote ssh://git@host:2222 # long-poll; --once runs one job | ||
| 223 | ``` | ||
| 224 | |||
| 225 | A push that touches a workflow queues one run per job, pinned to the commit *and* to | ||
| 226 | the workflow file's blob, so editing the workflow afterwards cannot change what a | ||
| 227 | queued run executes. The runner leases a job over the same SSH key you push with, | ||
| 228 | checks out the commit, runs the steps with per-step timeouts, heartbeats while it | ||
| 229 | works, and reports the exit code and log. A runner that disappears has its job | ||
| 230 | requeued; after five attempts the job fails instead of cycling forever. Protected | ||
| 231 | branches can require green checks as well as approvals. | ||
| 232 | |||
| 233 | `artifacts:` declares the files a run publishes. The patterns are expanded in the | ||
| 234 | checkout after the steps pass, uploaded over SSH into the same content-addressed | ||
| 235 | store that holds LFS objects and CI logs, and listed on the run page with a download | ||
| 236 | each: | ||
| 237 | |||
| 238 | ```sh | ||
| 239 | curl -OJ http://host/r/demo/runs/01M5.../artifacts/tool-linux | ||
| 240 | sha256sum tool-linux | ||
| 241 | ``` | ||
| 242 | |||
| 243 | A pattern that matches nothing **fails the job** rather than publishing nothing — a | ||
| 244 | build that stopped producing the file is exactly what the declaration is there to | ||
| 245 | catch. Two matches with the same base name are refused too, so nothing is published | ||
| 246 | under a name nobody chose. The bytes are kept while the run is in the queue: `vole gc` | ||
| 247 | marks them from the queue, since nothing in git names a build output. | ||
| 248 | |||
| 249 | An artifact of a run is not a release artifact. It belongs to the run that built it, | ||
| 250 | and publishing it to a release is a separate, deliberate step (`vole release attach`), | ||
| 251 | so "latest build" and "the bytes v1.0.0 ships" are never the same link. | ||
| 252 | |||
| 198 | 253 | ## Large files | |
| 199 | 254 | ||
| 200 | 255 | Track a pattern with git-lfs as usual; the only thing that changes is where the bytes | |