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.
internal/server/ci.go +12
| @@ -89,6 +89,7 @@ func (s *Server) enqueue(ev runs.Event) { | |||
| 89 | 89 | WorkflowSHA: wf.BlobSHA, | |
| 90 | 90 | Job: job.ID, | |
| 91 | 91 | Steps: steps(job), | |
| 92 | Artifacts: artifactPaths(job), | ||
| 92 | 93 | Created: time.Now().UTC(), | |
| 93 | 94 | } | |
| 94 | 95 | run.ID, _ = id.New(time.Now()) | |
| @@ -114,6 +115,17 @@ func steps(job workflow.Job) []runs.Step { | |||
| 114 | 115 | return out | |
| 115 | 116 | } | |
| 116 | 117 | ||
| 118 | // artifactPaths pins the declared patterns into the queued run, next to the pinned | ||
| 119 | // steps: a workflow edited after the push must not change what an already-queued | ||
| 120 | // run publishes. | ||
| 121 | func artifactPaths(job workflow.Job) []string { | ||
| 122 | out := make([]string, 0, len(job.Artifacts)) | ||
| 123 | for _, a := range job.Artifacts { | ||
| 124 | out = append(out, a.Path) | ||
| 125 | } | ||
| 126 | return out | ||
| 127 | } | ||
| 128 | |||
| 117 | 129 | // watchLeases hands jobs from dead runners back to the queue. | |
| 118 | 130 | func (s *Server) watchLeases(ctx context.Context) { | |
| 119 | 131 | t := time.NewTicker(5 * time.Second) | |