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.
Bound and reclaim the runner's saved checkouts
f594f1a16f6f8a8a3ee2867a44249b72a2c76c96 · Keenan Nemetz · 5d ago · parent de84c32b
Bound and reclaim the runner's saved checkouts The runner already downloaded each repository once into --dir and cloned every job from that copy, but nothing kept the copies bounded: they grew with every tag, every branch, and every commit a job pinned by id, and a force-push left the old history reachable through the remote-tracking reflogs. A long-lived runner filled its disk and then could not check anything out at all. Repack a copy that is most of the cap, dropping the objects no branch or tag can reach and the temporary refs a killed runner leaves behind; then delete the least recently used copy when the cap is still reached. --git-cache-size sets the cap (10G by default, 0 keeps every copy). The repository this runner is about to build is never the one deleted, and a copy another process holds is passed over. Reading and writing a saved checkout now takes a per-repository lock, so a repack or a deletion in one runner process cannot run underneath another's clone, or underneath the site and release pushes that are written from the same cache. The first clone skips the working-tree checkout, which nothing ever read.
ci/agent/agent.go +35
| @@ -252,6 +252,12 @@ type Options struct { | |||
| 252 | 252 | NoCache bool | |
| 253 | 253 | // CacheMaxBytes caps that cache. Zero means the default; see ci/cache. | |
| 254 | 254 | CacheMaxBytes int64 | |
| 255 | |||
| 256 | // GitCacheMaxBytes caps the saved checkouts of the repositories this runner has | ||
| 257 | // built. A job clones from the saved checkout instead of downloading the history | ||
| 258 | // again, so the cap trades disk for network. Negative means the default, and zero | ||
| 259 | // keeps every checkout; see DefaultGitCacheBytes and gitcache.go. | ||
| 260 | GitCacheMaxBytes int64 | ||
| 255 | 261 | } | |
| 256 | 262 | ||
| 257 | 263 | // Run leases and executes CI jobs until the connection fails, the context ends, | |
| @@ -282,6 +288,14 @@ func Run(ctx context.Context, opts Options) error { | |||
| 282 | 288 | } | |
| 283 | 289 | cleanupJobTrees(cache) | |
| 284 | 290 | cleanupStaleContainers(box) | |
| 291 | // Bound the saved checkouts before work starts, the same way the job cache is | ||
| 292 | // bound below: a runner whose disk filled up while it was not watching should | ||
| 293 | // not start by failing a job. | ||
| 294 | gitCacheMax := opts.GitCacheMaxBytes | ||
| 295 | if gitCacheMax < 0 { | ||
| 296 | gitCacheMax = DefaultGitCacheBytes | ||
| 297 | } | ||
| 298 | maintainTrustedCaches(cache, gitCacheMax, "") | ||
| 285 | 299 | ||
| 286 | 300 | // The directory is mounted whether or not it is reused: an image that sets | |
| 287 | 301 | // GOCACHE=/cache/go-build must work under --no-cache too, and a non-root job | |
| @@ -396,8 +410,13 @@ func Run(ctx context.Context, opts Options) error { | |||
| 396 | 410 | } | |
| 397 | 411 | sess.run(opts.Remote, cache, rep.Run, lease, box) | |
| 398 | 412 | if opts.Once { | |
| 413 | // This process is done; the next one bounds the cache at startup instead, | ||
| 414 | // before it leases anything. | ||
| 399 | 415 | return nil | |
| 400 | 416 | } | |
| 417 | // Tidy between jobs, never during one: the checkout this runner just used is | ||
| 418 | // the one the next job is most likely to want, so it is passed over as active. | ||
| 419 | maintainTrustedCaches(cache, gitCacheMax, rep.Run.Repo) | ||
| 401 | 420 | } | |
| 402 | 421 | } | |
| 403 | 422 | ||
| @@ -950,6 +969,14 @@ func publishReleases(remote string, co checkout, job *runnerJob, rel []runnerArt | |||
| 950 | 969 | if err != nil { | |
| 951 | 970 | return err | |
| 952 | 971 | } | |
| 972 | // Opening the branch fetches into the trusted cache and the write adds objects to | ||
| 973 | // it. Hold the cache's lock so a repack or a deletion in another runner process | ||
| 974 | // cannot move the object store while this reads and writes it. | ||
| 975 | unlock, err := co.lock() | ||
| 976 | if err != nil { | ||
| 977 | return err | ||
| 978 | } | ||
| 979 | defer unlock() | ||
| 953 | 980 | c, err := client.OpenBranch(co.GitDir, url, strings.TrimPrefix(metadata.Ref, "refs/heads/"), "vole-runner") | |
| 954 | 981 | if err != nil { | |
| 955 | 982 | return err | |
| @@ -1048,6 +1075,14 @@ func publishSite(remote string, co checkout, job *runnerJob, log io.Writer) erro | |||
| 1048 | 1075 | if err != nil { | |
| 1049 | 1076 | return err | |
| 1050 | 1077 | } | |
| 1078 | // The snapshot is committed and pushed from the trusted cache, which this runner | ||
| 1079 | // shares with any other process given the same --dir. Take its lock, as the | ||
| 1080 | // release push and the maintenance pass do. | ||
| 1081 | unlock, err := co.lock() | ||
| 1082 | if err != nil { | ||
| 1083 | return err | ||
| 1084 | } | ||
| 1085 | defer unlock() | ||
| 1051 | 1086 | tip, err := client.LSRemote(co.GitDir, url, "refs/heads/"+job.Branch) | |
| 1052 | 1087 | if err != nil { | |
| 1053 | 1088 | return fmt.Errorf("check %s before deploying the site: %w", job.Branch, err) | |