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/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.

Require an image for every CI job

70b702649b875d841e294f5a085ca784d43e5bc4 · Keenan Nemetz · 15d 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.

docs/runner.md +142

@@ -0,0 +1,142 @@
1 # Runners
2
3 A runner is a machine that leases jobs over SSH, executes their steps in
4 containers, and reports the result. Workflows themselves are described in
5 [Workflows](workflows.md); this page is about the machines that run them.
6
7 ## Start a runner
8
9 A runner needs an OCI runtime: Docker, rootless Podman, or nerdctl. Rootless
10 Podman is preferred, because membership in the Docker group is effectively root on
11 the host. Start a runner anywhere it can reach the Vole server and that runtime:
12
13 ```sh
14 vole runner --remote ssh://git@host:2222 --container-runtime podman
15 vole runner --remote ssh://git@host:2222 --container-runtime podman --once
16 ```
17
18 You can describe the machine and limit which repositories it serves:
19
20 ```sh
21 vole runner --remote ssh://git@host:2222 --label linux --label arm64
22 vole runner --remote ssh://git@host:2222 --repos demo --repos docs
23 ```
24
25 - `--label` says what the machine is. A job is sent only to a runner that has
26 every label in its `runs-on`. A job with no `runs-on` can run anywhere.
27 - `--repos` narrows what the account is already allowed to run. It cannot grant
28 access to a repository.
29 - `--dir` chooses where checkouts and caches are kept.
30 - `--once` leases one job and exits, which is useful for a runner that is
31 scheduled per job.
32
33 ## Images and image builds
34
35 Every job names its image in the workflow. A registry reference is used as
36 written; an `image-file` definition is built locally first. The definition fields
37 are described in the [Image reference](reference/image-reference.md).
38
39 For a custom definition, the runner receives the pinned definition text and its
40 digest from the queue, so it cannot build a definition that changed after the job
41 was queued. It renders the definition to a Dockerfile, builds it with the
42 configured OCI runtime, and tags the result by the definition digest. A later job
43 with the same definition reuses that tag.
44
45 The image build uses the runtime's own default network. `--container-network`
46 applies to the job container, not to the image build.
47
48 An image definition is code. Its build runs on the runner through the OCI
49 builder, so approving a contributor tip that changes a definition also approves
50 that build. Prefer rootless Podman, and pin the base image by digest.
51
52 ## How a job runs
53
54 One container is created per job and removed afterward. The workflow's steps run
55 inside it, but the runner process stays on the host: the SSH key, the trusted Git
56 cache, and the record push are never exposed to the job. Each job gets a fresh
57 clone, so a job cannot leave a Git hook or a modified checkout behind for the next
58 one.
59
60 Container defaults are intentionally closed:
61
62 - no network (`--container-network none`); add `bridge` only when a job needs it;
63 - read-only root filesystem, with `/tmp` as a writable tmpfs;
64 - no capabilities, `no-new-privileges`, and no host mounts except the job tree;
65 - no Docker socket, SSH agent, or host environment;
66 - the steps run as the runner user's `uid:gid` by default.
67
68 Use `--container-runtime` to choose `docker`, `podman`, or `nerdctl`. The runner
69 user must be able to talk to that runtime; rootless Podman is preferred. Do not
70 mount the host's Docker socket into a job; that would undo the isolation.
71
72 `--container-pull` controls whether an image is pulled (`missing`, `always`, or
73 `never`). `--container-user`, `--container-userns`, `--container-memory`,
74 `--container-cpus`, and `--container-pids` override the user, namespace mode, and
75 resource limits. Rootless Podman uses `keep-id` by default so the numeric user can
76 write the bind-mounted job tree; for rootless Docker, choose a user that matches
77 the runtime's mapping (container root is the runner user) or use a runtime with
78 `keep-id`.
79
80 The run record names the image, the definition digest when there is one, and the
81 runtime, so the run page says what actually executed.
82
83 ## Who can run jobs and report results
84
85 A job runs repository code, so Vole uses a runner allowlist for both taking a job
86 and reporting its result:
87
88 ```sh
89 vole admin acl runners demo ci-runner-1 # replace the list
90 vole admin acl runners demo --add ci-runner-2 # add one account
91 vole admin acl runners demo --remove ci-runner-1 # remove one account
92 vole admin acl runners demo --clear # back to admins only
93 ```
94
95 Admins always count. For everyone else, a maintainer running CI on their laptop does
96 not turn a required check green. The result is still recorded, and the run page
97 says whether it counts.
98
99 When a job finishes, the runner uploads its log, writes a record under
100 `runs/<run id>/<attempt>/`, and pushes the record together with artifact pointers
101 and any release pointers. Vole checks that the account is allowed, that the record
102 names that account, and that it describes the job the account was given. The run
103 page names the account and links to the commit that holds the record.
104
105 The runner did the work, so the runner reports the result. The SSH key behind the
106 push identifies the runner, and `data/audit.log` records the decision.
107
108 ## Logs
109
110 The runner uploads the log before reporting the exit code. Vole does not stream
111 logs: a log assembled by the server would not be the runner's own record.
112
113 ```text
114 /r/<repo>/runs/<id>/log # latest attempt
115 /r/<repo>/runs/<id>/log?attempt=N # a particular retry
116 ```
117
118 If a runner is killed, its attempt has no log.
119
120 ## Scheduling and the queue
121
122 All runners share one pool. An idle runner waits for work instead of asking
123 repeatedly, and Vole gives the next job to the repository with the least work in
124 progress. Set `max_jobs_per_repo` when you need a hard limit per repository.
125
126 The `/runners` page shows who is connected, what each runner is building, and how
127 much work is queued.
128
129 How the queue behaves:
130
131 - Finished runs are stored as records on the `vole` branch. `data/runs.db` only
132 stores work that has not happened yet.
133 - A push normally reaches the queue in under a second. Every 30 seconds Vole also
134 compares each branch and tag with the last tip it queued, so a missed
135 notification still produces a run.
136 - Pushing again cancels queued runs for the commit it replaced. It never cancels a
137 run that is already in progress.
138 - If a runner disappears, its job is requeued. After five attempts the job fails
139 instead of cycling forever.
140 - Deleting `data/runs.db` loses attempts that were in flight. Current branch and
141 tag tips are queued again from Git, and `vole admin index rebuild` restores
142 finished history from the records on each repository's `vole` branch.