Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

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