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