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.

Require an image for every CI job

70b702649b875d841e294f5a085ca784d43e5bc4 · Keenan Nemetz · 16d ago · parent 7da9111e

Require an image for every CI job

Runners now require an OCI runtime, and each workflow job must name either a registry image or an image-file definition under .vole/images. Definitions are validated at push time, pinned with the run, built locally by the runner, and cached by definition digest. Records and the run page carry the image and definition digest.

Split the CI guide into workflows and runners, and add reference pages for workflow and image definition fields.

ci/agent/build.go +96

@@ -0,0 +1,96 @@
1 package agent
2
3 // Building a custom image from a .vole/images definition.
4 //
5 // The job carries the pinned definition text and its digest, so the runner does
6 // not need to read the repository again and cannot build a definition that changed
7 // after the job was queued. The image is tagged by that digest, which makes a
8 // second job with the same definition reuse the first build.
9
10 import (
11 "context"
12 "fmt"
13 "os"
14 "os/exec"
15 "path/filepath"
16 "strings"
17 "time"
18
19 "github.com/rodent-software/vole/ci"
20 )
21
22 const imageBuildTimeout = 30 * time.Minute
23
24 // resolveImage returns the reference to run. A registry image is used as written;
25 // 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 if job.ImageFile == "" {
28 if err := ci.ValidImageRef(job.Image); err != nil {
29 return "", fmt.Errorf("the job image: %w", err)
30 }
31 return job.Image, nil
32 }
33 if job.ImageDefinition == "" {
34 return "", fmt.Errorf("image-file %q has no pinned definition", job.ImageFile)
35 }
36 if job.ImageSHA == "" {
37 return "", fmt.Errorf("image-file %q has no definition digest", job.ImageFile)
38 }
39 tag := imageTag(job)
40 if cfg.pull != "always" && imageExists(cfg.runtime, tag) {
41 fmt.Fprintf(log, "image-file %s: using cached %s\n", job.ImageFile, tag)
42 return tag, nil
43 }
44
45 dockerfile, err := ci.RenderImage(job.ImageDefinition)
46 if err != nil {
47 return "", fmt.Errorf("image-file %q: %w", job.ImageFile, err)
48 }
49 dir, err := os.MkdirTemp("", "vole-image-*")
50 if err != nil {
51 return "", err
52 }
53 defer os.RemoveAll(dir)
54 if err := os.WriteFile(filepath.Join(dir, "Dockerfile"), []byte(dockerfile), 0o600); err != nil {
55 return "", err
56 }
57
58 args := []string{"build", "-t", tag, "-f", filepath.Join(dir, "Dockerfile"), dir}
59 switch {
60 case cfg.pull == "always" && cfg.runtime == "podman":
61 args = append(args, "--pull=always")
62 case cfg.pull == "always":
63 args = append(args, "--pull")
64 }
65 ctx, cancel := context.WithTimeout(context.Background(), imageBuildTimeout)
66 defer cancel()
67 cmd := exec.CommandContext(ctx, cfg.runtime, args...)
68 code, out, lost := s.runCommand(ctx, cmd, job.ID, lease, nil)
69 log.WriteString(out)
70 if lost {
71 return "", errLeaseLost
72 }
73 if ctx.Err() == context.DeadlineExceeded {
74 return "", fmt.Errorf("building image-file %q timed out after %s", job.ImageFile, imageBuildTimeout)
75 }
76 if code != 0 {
77 return "", fmt.Errorf("building image-file %q exited %d", job.ImageFile, code)
78 }
79 if !imageExists(cfg.runtime, tag) {
80 return "", fmt.Errorf("building image-file %q produced no image tagged %s", job.ImageFile, tag)
81 }
82 fmt.Fprintf(log, "\nimage-file %s: built %s\n", job.ImageFile, tag)
83 return tag, nil
84 }
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 }