Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

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 }