Why
A finished attempt’s log was written to the runner’s temp file and uploaded once, at the end, as an LFS object. That made a running job’s log invisible, made the log look like a shared content-addressed artifact when it belongs to exactly one attempt, and put the bytes in the wrong store. Now that CI history is out of Git, logs can be the instance’s own files.
What changed
- The runner streams the log over the same
vole-runnerSSH channel that holds the lease, in 128 KiB chunks. The chunk carries its attempt and byte offset; the server refuses a gap or an oversized log. - The server appends each chunk to
data/logs/<repo>/<run>/<attempt>.logand updates the attempt’s size while the job runs. The run page can open the file before the job finishes. finishcarries the final log size. The server checks the file is exactly that long, syncs it, and stores the result. A failed or partial stream is reported as no log: the runner drops the step ranges and the server removes the partial file.- The run page serves the file directly, with Range support and the existing per-step byte slices. The status page no longer classifies logs as LFS usage, and GC no longer treats them as LFS roots.
data/logs/is created and hardened with the rest of the instance; backup and restore docs include it.
Tests
ci/logs: append, read, size, remove, offset gap, cap, path validation.ci/runs: per-attempt log sizes and lease rules.serverintegration: a real runner streams a log, the file is underdata/logs, the served log matches the file, the stored attempt names the size, and step slices work.- Existing runner, SSH, web, and status tests updated.
Checks
go test -count=1 ./...go test -race ./ci/logs ./ci/runs ./ci/agent ./server/ssh ./server/webgo vet ./...vole workflow checkvole workflow run verify
Stacked change
This is based on ci-records-in-db (#83), not main, because it uses the database-only record path. Retarget it to main after #83 lands.
Deployment
No compatibility code ships. An existing local instance needs a one-time migration before this binary starts: copy each old run_logs.oid from data/lfs/repos/<repo>/<oid> to data/logs/<repo>/<run>/<attempt>.log, then rebuild run_logs with the new (run_id, attempt, size, created, updated) shape. Fresh instances need nothing.