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.
Bound what a CI job can consume
6bfc42949be4d40b3b74de8cc17c6d903cfe531e · Keenan Nemetz · 13d ago · parent f5456470
Bound what a CI job can consume A job runs untrusted build code, and everything it produces is either stored by the instance or kept on the runner. None of those channels had a bound of its own. The attempt log was a strings.Builder: the whole run in memory, then written to a temp file, then read twice by the LFS transfer (once to hash, once to send). It is now a spool file on disk from the first byte, capped at 32 MiB, uploaded once at the end as an ordinary LFS object. A capped log reports the whole write as taken so the producing process is not killed, and gets a line saying it was truncated. Step offsets are computed from the spool's length, which never exceeds the cap, so a truncated log still has valid ranges. Nothing is streamed to the server: a log's content-addressed id cannot be known before its last byte, so streaming would only buy a second protocol. A run may publish at most 256 artifact files totalling 4 GiB. The workflow limit is on declarations, and a declaration is a pattern that can match a directory tree, so the bound on what it expands to has to be where the tree is walked. Contributor caches are now namespaced per account, not one shared "contrib" namespace: one contributor's untrusted branch must not be able to seed another contributor's cache. A contributor keeps their cache across their own branches. Container volume paths are checked before the argument list is built: a colon is the field separator, and a NUL or newline would split the argument, so a path carrying one is refused rather than passed to the runtime. RunArgs is unchanged; the refusal is in Start, and the closed-set test now also pins --cap-add, --device, --pid/--ipc host and the podman socket. Image builds run through the configured runtime, so a rootless Podman runner builds rootless. The definition is pinned by digest already; the docs now say how the build gets its network and recommend pinning the base image in from: by digest for a reproducible build. Tests: the spool keeps what fits, truncates and says so, and removes its file; artifact count and byte caps (the byte cap uses a sparse file so it does not write 4 GiB); contributor caches are separated by account while one account's branches share; volume paths with a colon, NUL or newline are refused.
ci/agent/build.go +3−4
| @@ -10,10 +10,10 @@ package agent | |||
| 10 | 10 | import ( | |
| 11 | 11 | "context" | |
| 12 | 12 | "fmt" | |
| 13 | "io" | ||
| 13 | 14 | "os" | |
| 14 | 15 | "os/exec" | |
| 15 | 16 | "path/filepath" | |
| 16 | "strings" | ||
| 17 | 17 | "time" | |
| 18 | 18 | ||
| 19 | 19 | "github.com/rodent-software/vole/ci" | |
| @@ -24,7 +24,7 @@ const imageBuildTimeout = 30 * time.Minute | |||
| 24 | 24 | ||
| 25 | 25 | // resolveImage returns the reference to run. A registry image is used as written; | |
| 26 | 26 | // an image-file definition is built locally and cached under a digest tag. | |
| 27 | func (s *runnerSession) resolveImage(job *runnerJob, box runnerBox, log *strings.Builder, lease time.Duration) (string, error) { | ||
| 27 | func (s *runnerSession) resolveImage(job *runnerJob, box runnerBox, log io.Writer, lease time.Duration) (string, error) { | ||
| 28 | 28 | if job.ImageFile == "" { | |
| 29 | 29 | if err := ci.ValidImageRef(job.Image); err != nil { | |
| 30 | 30 | return "", fmt.Errorf("the job image: %w", err) | |
| @@ -66,8 +66,7 @@ func (s *runnerSession) resolveImage(job *runnerJob, box runnerBox, log *strings | |||
| 66 | 66 | ctx, cancel := context.WithTimeout(context.Background(), imageBuildTimeout) | |
| 67 | 67 | defer cancel() | |
| 68 | 68 | cmd := exec.CommandContext(ctx, box.Runtime, args...) | |
| 69 | code, out, lost := s.runCommand(ctx, cmd, job.ID, lease, nil) | ||
| 70 | log.WriteString(out) | ||
| 69 | code, lost := s.runCommand(ctx, cmd, job.ID, lease, nil, log) | ||
| 71 | 70 | if lost { | |
| 72 | 71 | return "", errLeaseLost | |
| 73 | 72 | } | |