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/web.go +13
| @@ -44,6 +44,7 @@ func New(repos *repo.Store, ix *index.Store, ident *identity.Store, rs *runs.Sto | |||
| 44 | 44 | "since": humanSince, | |
| 45 | 45 | "short": shortSHA, | |
| 46 | 46 | "esc": url.PathEscape, | |
| 47 | "bytes": humanBytes, | ||
| 47 | 48 | }).Parse(templates) | |
| 48 | 49 | if err != nil { | |
| 49 | 50 | return nil, err | |
| @@ -401,6 +402,18 @@ func shortSHA(s string) string { | |||
| 401 | 402 | return s | |
| 402 | 403 | } | |
| 403 | 404 | ||
| 405 | // humanBytes renders a size for a table cell. A reader comparing two artifacts | ||
| 406 | // wants "1.4 MiB", not 1468006. | ||
| 407 | func humanBytes(n int64) string { | ||
| 408 | switch { | ||
| 409 | case n >= 1<<20: | ||
| 410 | return fmt.Sprintf("%.1f MiB", float64(n)/(1<<20)) | ||
| 411 | case n >= 1<<10: | ||
| 412 | return fmt.Sprintf("%.1f KiB", float64(n)/(1<<10)) | ||
| 413 | } | ||
| 414 | return fmt.Sprintf("%d B", n) | ||
| 415 | } | ||
| 416 | |||
| 404 | 417 | func humanSince(t int64) string { | |
| 405 | 418 | if t == 0 { | |
| 406 | 419 | return "" | |