vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software: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.
internal/web/templates.go +34−2
| @@ -102,7 +102,7 @@ select{background:var(--card);color:var(--fg);border:1px solid var(--line);borde | |||
| 102 | 102 | <tr><th>Job</th><th>Commit</th><th>Status</th><th>Attempts</th><th>Age</th></tr> | |
| 103 | 103 | {{range .Runs}} | |
| 104 | 104 | <tr> | |
| 105 | <td>{{.Job}} <span class="muted">{{.Workflow}}</span></td> | ||
| 105 | <td><a href="/r/{{$.Name}}/runs/{{.ID}}">{{.Job}}</a> <span class="muted">{{.Workflow}}</span></td> | ||
| 106 | 106 | <td class="sha">{{short .SHA}}</td> | |
| 107 | 107 | <td class="muted">{{.Status}}{{if eq .Status "failed"}} (exit {{.ExitCode}}){{end}}</td> | |
| 108 | 108 | <td class="muted">{{.Attempts}}</td> | |
| @@ -409,7 +409,7 @@ commit to land, plus <code>approvals/<user>.json</code> from each approver | |||
| 409 | 409 | <tr><th>Job</th><th>Status</th></tr> | |
| 410 | 410 | {{range .Checks}} | |
| 411 | 411 | <tr> | |
| 412 | <td>{{.Job}} <span class="muted">{{.Workflow}}</span></td> | ||
| 412 | <td><a href="/r/{{$.Name}}/runs/{{.ID}}">{{.Job}}</a> <span class="muted">{{.Workflow}}</span></td> | ||
| 413 | 413 | <td class="muted">{{.Status}}{{if eq .Status "failed"}} (exit {{.Exit}}){{end}}</td> | |
| 414 | 414 | </tr> | |
| 415 | 415 | {{end}} | |
| @@ -512,4 +512,36 @@ release a description; the tarball below works either way.</p> | |||
| 512 | 512 | {{if .Capped}}<p class="muted">Showing the first {{.CappedAt}}; the rest are in the history.</p>{{end}} | |
| 513 | 513 | {{template "footer" .}} | |
| 514 | 514 | {{end}} | |
| 515 | |||
| 516 | {{define "run"}} | ||
| 517 | {{template "header" .}} | ||
| 518 | <h1><a href="/r/{{.Name}}">{{.Name}}</a> · run {{.Run.Job}}</h1> | ||
| 519 | <p class="muted"> | ||
| 520 | {{.Run.Workflow}} · {{.Run.Branch}} at <a class="sha" href="/r/{{.Name}}/commit/{{.Run.SHA}}">{{short .Run.SHA}}</a> | ||
| 521 | · {{.Run.Status}}{{if eq .Run.Status "failed"}} (exit {{.Run.ExitCode}}){{end}} | ||
| 522 | · attempt {{.Run.Attempts}} | ||
| 523 | · {{since .Run.Created.Unix}} | ||
| 524 | </p> | ||
| 525 | {{if .HasLog}}<p><a href="/r/{{.Name}}/runs/{{.Run.ID}}/log">log</a></p>{{end}} | ||
| 526 | |||
| 527 | <h2>Artifacts</h2> | ||
| 528 | {{if .Artifacts}} | ||
| 529 | <table> | ||
| 530 | <tr><th>File</th><th>Size</th><th>SHA-256</th><th></th></tr> | ||
| 531 | {{range .Artifacts}} | ||
| 532 | <tr> | ||
| 533 | <td><a href="/r/{{$.Name}}/runs/{{$.Run.ID}}/artifacts/{{.Name}}">{{.Name}}</a></td> | ||
| 534 | <td class="muted">{{bytes .Size}}</td> | ||
| 535 | <td class="muted"><code>{{short .OID}}</code></td> | ||
| 536 | <td><a href="/r/{{$.Name}}/runs/{{$.Run.ID}}/artifacts/{{.Name}}">download</a></td> | ||
| 537 | </tr> | ||
| 538 | {{end}} | ||
| 539 | </table> | ||
| 540 | <p class="muted">Built from {{short .Run.SHA}} by this run. The bytes live once in the | ||
| 541 | instance's content-addressed store, and are swept only when nothing names them.</p> | ||
| 542 | {{else}} | ||
| 543 | <p class="muted">No artifacts published by this run.</p> | ||
| 544 | {{end}} | ||
| 545 | {{template "footer" .}} | ||
| 546 | {{end}} | ||
| 515 | 547 | ` | |