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.
Add packaging, a CI workflow, and containerised local runs
3b39a84c913cabe15bfa6a60dd176ff2dd28d241 · Keenan Nemetz · 15d ago · parent 30a72490
Add packaging, a CI workflow, and containerised local runs A v* tag now builds a .deb and a .rpm for amd64 and arm64 with nfpm and attaches them, with checksums, to the release. The package ships the systemd unit, a config at /etc/vole/vole.toml, and a postinstall that creates the service account and prepares the instance that config names; `vole admin init --config` is what makes that possible, and the binary's version is stamped from the tag instead of the source tree. scripts/ci.sh becomes .vole/workflows/ci.yaml, so `vole workflow run ci` executes exactly what a push runs, and the Markdown link check moves to scripts/check-links.py. Local runs now always execute in the job image and share ci/container with the runner; --no-container is gone, because a run in a different environment proves nothing about CI. The runner mounts a per-repository namespace at /cache and sets no variable for it: a job opts in through its image's env or the new job-level env, which is pinned with the queue and delivered with the lease. Whole namespaces are evicted least-recently-written first so a module cache is never half-deleted, and contributor branches get their own namespace so an untrusted tip cannot seed the cache a protected branch builds into. Running the suite in that container found four bugs worth naming: /tmp was mounted noexec, which broke `go test` and `go run`; the read-only root is gone, because images make toolchain directories world-writable deliberately and the mount set is what protects the host; OpenSSH needs a passwd entry for the job's uid; and a lost metadata push reported as "cannot lock ref" was not classified as a race, so concurrent CLI writes gave up on the first collision. That retry now backs off exponentially with jitter.
ci/agent/build.go +8−19
| @@ -17,13 +17,14 @@ import ( | |||
| 17 | 17 | "time" | |
| 18 | 18 | ||
| 19 | 19 | "github.com/rodent-software/vole/ci" | |
| 20 | "github.com/rodent-software/vole/ci/container" | ||
| 20 | 21 | ) | |
| 21 | 22 | ||
| 22 | 23 | const imageBuildTimeout = 30 * time.Minute | |
| 23 | 24 | ||
| 24 | 25 | // resolveImage returns the reference to run. A registry image is used as written; | |
| 25 | 26 | // an image-file definition is built locally and cached under a digest tag. | |
| 26 | func (s *runnerSession) resolveImage(job *runnerJob, cfg containerConfig, log *strings.Builder, lease time.Duration) (string, error) { | ||
| 27 | func (s *runnerSession) resolveImage(job *runnerJob, box runnerBox, log *strings.Builder, lease time.Duration) (string, error) { | ||
| 27 | 28 | if job.ImageFile == "" { | |
| 28 | 29 | if err := ci.ValidImageRef(job.Image); err != nil { | |
| 29 | 30 | return "", fmt.Errorf("the job image: %w", err) | |
| @@ -36,8 +37,8 @@ func (s *runnerSession) resolveImage(job *runnerJob, cfg containerConfig, log *s | |||
| 36 | 37 | if job.ImageSHA == "" { | |
| 37 | 38 | return "", fmt.Errorf("image-file %q has no definition digest", job.ImageFile) | |
| 38 | 39 | } | |
| 39 | tag := imageTag(job) | ||
| 40 | if cfg.pull != "always" && imageExists(cfg.runtime, tag) { | ||
| 40 | tag := container.ImageTag(job.ImageFile, job.ImageSHA) | ||
| 41 | if box.Pull != "always" && container.Exists(box.Runtime, tag) { | ||
| 41 | 42 | fmt.Fprintf(log, "image-file %s: using cached %s\n", job.ImageFile, tag) | |
| 42 | 43 | return tag, nil | |
| 43 | 44 | } | |
| @@ -57,14 +58,14 @@ func (s *runnerSession) resolveImage(job *runnerJob, cfg containerConfig, log *s | |||
| 57 | 58 | ||
| 58 | 59 | args := []string{"build", "-t", tag, "-f", filepath.Join(dir, "Dockerfile"), dir} | |
| 59 | 60 | switch { | |
| 60 | case cfg.pull == "always" && cfg.runtime == "podman": | ||
| 61 | case box.Pull == "always" && box.Runtime == "podman": | ||
| 61 | 62 | args = append(args, "--pull=always") | |
| 62 | case cfg.pull == "always": | ||
| 63 | case box.Pull == "always": | ||
| 63 | 64 | args = append(args, "--pull") | |
| 64 | 65 | } | |
| 65 | 66 | ctx, cancel := context.WithTimeout(context.Background(), imageBuildTimeout) | |
| 66 | 67 | defer cancel() | |
| 67 | cmd := exec.CommandContext(ctx, cfg.runtime, args...) | ||
| 68 | cmd := exec.CommandContext(ctx, box.Runtime, args...) | ||
| 68 | 69 | code, out, lost := s.runCommand(ctx, cmd, job.ID, lease, nil) | |
| 69 | 70 | log.WriteString(out) | |
| 70 | 71 | if lost { | |
| @@ -76,21 +77,9 @@ func (s *runnerSession) resolveImage(job *runnerJob, cfg containerConfig, log *s | |||
| 76 | 77 | if code != 0 { | |
| 77 | 78 | return "", fmt.Errorf("building image-file %q exited %d", job.ImageFile, code) | |
| 78 | 79 | } | |
| 79 | if !imageExists(cfg.runtime, tag) { | ||
| 80 | if !container.Exists(box.Runtime, tag) { | ||
| 80 | 81 | return "", fmt.Errorf("building image-file %q produced no image tagged %s", job.ImageFile, tag) | |
| 81 | 82 | } | |
| 82 | 83 | fmt.Fprintf(log, "\nimage-file %s: built %s\n", job.ImageFile, tag) | |
| 83 | 84 | return tag, nil | |
| 84 | 85 | } | |
| 85 | |||
| 86 | func imageTag(job *runnerJob) string { | ||
| 87 | sha := job.ImageSHA | ||
| 88 | if len(sha) > 16 { | ||
| 89 | sha = sha[:16] | ||
| 90 | } | ||
| 91 | return "vole/image-file:" + job.ImageFile + "-" + sha | ||
| 92 | } | ||
| 93 | |||
| 94 | func imageExists(runtime, ref string) bool { | ||
| 95 | return exec.Command(runtime, "image", "inspect", ref).Run() == nil | ||
| 96 | } | ||