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.
Document runners, and check the three files that describe one agree
e21f6c5e26cc49d5a08d337cb1c7b70d373a8cce · Keenan Nemetz · 3d ago · parent d0d4239f
Document runners, and check the three files that describe one agree A runner is a second decision after the forge, not part of its first boot: it executes repository code, it can read every secret a job declares, and the allowlist that lets it take work is per repository. The guide says what the deployment creates, how to set one up on a machine you already have, the one step only an admin can do, what to know before giving it a repository, and how to upgrade it. render.sh now renders either first-boot file, taking its placeholder list from the render line it uses, so the checker reads the same line instead of keeping a second copy. check-deploy.py renders both templates, requires each one's promised files with literal modes, and holds the runner's three descriptions together: the template writes the arguments, the unit reads them, and the package ships the example an operator copies. A name that drifts leaves a machine enabled and idle, which looks exactly like a runner that is working.
docs/deploy/runner.md +158
| @@ -0,0 +1,158 @@ | |||
| 1 | # CI runners | ||
| 2 | |||
| 3 | A runner is a machine that leases jobs from your instance over SSH, runs each job | ||
| 4 | in a container, and reports the result. Vole does not build on the forge's own | ||
| 5 | server: the machine that runs repository code is a machine you chose, with the OCI | ||
| 6 | runtime you installed on it. | ||
| 7 | |||
| 8 | The [deployment guides](README.md) can create one for you, or you can set one up | ||
| 9 | on a machine you already have. Either way there is one step only an admin can do, | ||
| 10 | and it is per repository. | ||
| 11 | |||
| 12 | ## Let the deployment create one | ||
| 13 | |||
| 14 | Each Terraform configuration takes `-var runner=true` and creates a second machine | ||
| 15 | next to the forge: rootless Podman, an unprivileged `ci-runner` account, a pinned | ||
| 16 | host key, and `vole-runner.service` enabled and waiting for work. | ||
| 17 | |||
| 18 | ```sh | ||
| 19 | cd deploy/terraform/hetzner | ||
| 20 | terraform apply \ | ||
| 21 | -var domain=forge.example.com \ | ||
| 22 | -var runner=true \ | ||
| 23 | -var "admin_ssh_public_key=$(cat ~/.ssh/id_ed25519.pub)" \ | ||
| 24 | -var vole_package_url=https://example.com/r/vole/releases/v0.2.0/artifacts/vole_0.2.0-1_amd64.deb | ||
| 25 | |||
| 26 | terraform output runner_setup | ||
| 27 | ``` | ||
| 28 | |||
| 29 | `runner_setup` prints the commands in the next section, filled in with the | ||
| 30 | addresses this apply created. | ||
| 31 | |||
| 32 | | Setting | Default | What it does | | ||
| 33 | |---|---|---| | ||
| 34 | | `runner` | `false` | Create the second machine | | ||
| 35 | | `runner_type` | same size as the forge | Server type or droplet size. Builds want memory; raise this before the forge's own | | ||
| 36 | | `runner_labels` | `["linux"]` | What this machine advertises. A job with `runs-on` only comes to a runner carrying every label it asks for | | ||
| 37 | | `runner_network` | `bridge` | Whether a job that asks for a network gets one. A job that fetches dependencies needs this; `none` means such a job waits for another runner instead | | ||
| 38 | | `runner_disk_gb` | `40` | AWS only. Container images and caches fill this, not the repositories | | ||
| 39 | |||
| 40 | The runner is not backed up. Its checkouts and caches are copies of what the | ||
| 41 | repositories already have, and everything it produces is stored by the instance. | ||
| 42 | |||
| 43 | ## On a machine you already have | ||
| 44 | |||
| 45 | A runner needs an OCI runtime — rootless Podman is preferred, because membership | ||
| 46 | in the Docker group is effectively root on the host — and it needs to reach the | ||
| 47 | instance's SSH port. | ||
| 48 | |||
| 49 | ```sh | ||
| 50 | # Render the runner's first-boot file, then paste it into the provider's user | ||
| 51 | # data field, or read it first and run the steps by hand. | ||
| 52 | cd deploy/cloud-init | ||
| 53 | FORGE_HOST=forge.example.com \ | ||
| 54 | VOLE_PACKAGE_URL=https://example.com/r/vole/releases/v0.2.0/artifacts/vole_0.2.0-1_amd64.deb \ | ||
| 55 | ./render.sh runner > runner-user-data.yaml | ||
| 56 | ``` | ||
| 57 | |||
| 58 | On a machine you configure yourself, the package carries the unit, so it is three | ||
| 59 | steps: install it, set up the account the runtime needs, and fill in the | ||
| 60 | arguments. | ||
| 61 | |||
| 62 | ```sh | ||
| 63 | sudo apt install ./vole_0.2.0-1_amd64.deb # dnf on Fedora and RHEL | ||
| 64 | sudo useradd --create-home --home-dir /var/lib/vole-runner --shell /usr/sbin/nologin ci-runner | ||
| 65 | sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 ci-runner | ||
| 66 | sudo apt install podman slirp4netns fuse-overlayfs uidmap | ||
| 67 | sudo install -d -o ci-runner -g ci-runner -m 0700 /var/lib/vole-runner/.ssh | ||
| 68 | sudo -u ci-runner ssh-keygen -q -t ed25519 -N '' -f /var/lib/vole-runner/.ssh/id_ed25519 | ||
| 69 | |||
| 70 | # Pin the instance's host key. A runner executes repository code, so a host that | ||
| 71 | # can answer in the instance's place could send it any job. | ||
| 72 | ssh-keyscan -p 2222 git@forge.example.com | sudo tee /etc/vole/runner_known_hosts | ||
| 73 | ``` | ||
| 74 | |||
| 75 | Then put the arguments in `/etc/vole/runner.env`. The package ships an example | ||
| 76 | with each option explained: | ||
| 77 | |||
| 78 | ```sh | ||
| 79 | sudo tee /etc/vole/runner.env >/dev/null <<'EOF' | ||
| 80 | VOLE_RUNNER_ARGS=--remote ssh://git@forge.example.com:2222 --container-runtime podman --container-network bridge --dir /var/lib/vole-runner --known-hosts /etc/vole/runner_known_hosts --label linux | ||
| 81 | EOF | ||
| 82 | sudo systemctl daemon-reload | ||
| 83 | sudo systemctl enable --now vole-runner | ||
| 84 | ``` | ||
| 85 | |||
| 86 | `systemctl status vole-runner` and `journalctl -u vole-runner` are where a runner | ||
| 87 | that is not taking work says why. | ||
| 88 | |||
| 89 | ## Register the runner, then allow it on a repository | ||
| 90 | |||
| 91 | The runner's key is not an admin key. It becomes a bot account, and the | ||
| 92 | allowlist decides what it may build: | ||
| 93 | |||
| 94 | ```sh | ||
| 95 | # The public key is on the runner; this copies it to the instance as a new bot. | ||
| 96 | ssh root@<runner-address> cat /var/lib/vole-runner/.ssh/id_ed25519.pub \ | ||
| 97 | | vole admin --remote ssh://git@forge.example.com:2222 \ | ||
| 98 | account create ci-runner-1 --bot --key - | ||
| 99 | |||
| 100 | # Per repository: let it take jobs, and give it read so it can clone. | ||
| 101 | vole admin --remote ssh://git@forge.example.com:2222 acl runners add demo ci-runner-1 | ||
| 102 | vole admin --remote ssh://git@forge.example.com:2222 acl set-role demo ci-runner-1 read | ||
| 103 | ``` | ||
| 104 | |||
| 105 | This is the part that cannot happen at first boot: repositories do not exist yet, | ||
| 106 | and the allowlist is per repository. Add each new repository to it as you create | ||
| 107 | it, or the runner will sit idle while its jobs wait in the queue. | ||
| 108 | |||
| 109 | A bot cannot sign in to the browser and cannot be an instance admin. That second | ||
| 110 | part matters: admins count as trusted reporters, so a bot that was an admin would | ||
| 111 | make every result count whatever the allowlist said. | ||
| 112 | |||
| 113 | ## Check that it works | ||
| 114 | |||
| 115 | - The Admin page's **Runners** tab (`/admin?tab=runners`) shows what is connected, | ||
| 116 | what each runner is building, and how much is queued. | ||
| 117 | - Push a commit on a branch with a workflow. The run page shows which runner took | ||
| 118 | it and whether the result counts. | ||
| 119 | - A run that executed on a runner outside the allowlist still exists and still | ||
| 120 | says so; it just does not count for the branch. | ||
| 121 | |||
| 122 | ## What to know before you give it a repository | ||
| 123 | |||
| 124 | **A runner can read every secret its jobs declare.** That is what running the job | ||
| 125 | means: the values arrive on the machine. If the machine is not yours, the secrets | ||
| 126 | are not either. This is also why an instance admin's machine should not be a | ||
| 127 | runner for a repository whose secrets it should not see. | ||
| 128 | |||
| 129 | **The allowlist is the control, not the container.** A job that escapes its | ||
| 130 | container finds a machine that can reach the instance and the internet. Keep the | ||
| 131 | runner on its own machine, run it as an unprivileged account, and give it no | ||
| 132 | credentials except the one key. | ||
| 133 | |||
| 134 | **Network is a per-job decision this machine permits.** A `bridge` runner still | ||
| 135 | gives a job that asks for none no network. What it cannot do is take the choice | ||
| 136 | away from a job, so a workflow you approve for a contributor branch is a workflow | ||
| 137 | that may reach the internet with whatever it can read. | ||
| 138 | |||
| 139 | **Disk fills where builds happen.** Container images, the `/cache` mount, and the | ||
| 140 | saved checkouts live under `--dir`. `--cache-size` (5 GiB) and `--git-cache-size` | ||
| 141 | (10 GiB) bound the last two; the images are the runtime's own storage, so size the | ||
| 142 | machine for them. | ||
| 143 | |||
| 144 | ## Upgrading | ||
| 145 | |||
| 146 | The runner is the same package as the instance, and the two talk over a versioned | ||
| 147 | protocol. Upgrade the instance first, then the runner machines: | ||
| 148 | |||
| 149 | ```sh | ||
| 150 | sudo apt install ./vole-<new-version>.deb | ||
| 151 | sudo systemctl restart vole-runner | ||
| 152 | ``` | ||
| 153 | |||
| 154 | ## Next | ||
| 155 | |||
| 156 | - [Runners reference](../runner.md) — every option, the container defaults, caching, | ||
| 157 | and how a lease is reported | ||
| 158 | - [Workflows](../workflows.md) — what a job asks a runner for, including `runs-on` | ||