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.
Ship the runner's unit and let a deployment create a runner machine
d0d4239f3f7236197c87a752d7965de238430f98 · Keenan Nemetz · 3d ago · parent dbae2e01
Ship the runner's unit and let a deployment create a runner machine The package installs one binary that is the forge, the runner, and the CLI, so it now carries a second unit. It is not enabled, and it refuses to start with a message until an operator fills in /etc/vole/runner.env: who runs builds on a machine is that machine owner's decision. The unit runs as a dedicated account, because rootless podman needs a home directory and a range of subordinate ids, and because a job should not find the forge's own account on the same disk. The arguments live in that one file rather than in the unit, split at spaces by systemd, so a single unit serves every machine. Each Terraform configuration takes runner=true and creates that machine beside the forge: its own firewall, no backups (its checkouts and caches are copies), the same key for a shell, and Cloud-init that installs the package, creates the account and its subuids, generates the runner's key, pins the instance's host key once, and enables the unit. The runner_setup output prints the three commands that let it take work, filled in with the addresses this apply created. The first-boot file cannot register the runner: the allowlist is per repository, and at first boot there are no repositories. That step stays with an instance admin, and the docs say so.
deploy/terraform/hetzner/variables.tf +35
| @@ -106,3 +106,38 @@ variable "cloud_init_file" { | |||
| 106 | 106 | type = string | |
| 107 | 107 | default = "" | |
| 108 | 108 | } | |
| 109 | |||
| 110 | variable "runner" { | ||
| 111 | description = "Create a second machine that runs CI jobs. See docs/deploy/runner.md: an admin has to register its key and allow it on a repository before it receives any work." | ||
| 112 | type = bool | ||
| 113 | default = false | ||
| 114 | } | ||
| 115 | |||
| 116 | variable "runner_type" { | ||
| 117 | description = "Server type for the runner. Builds want memory, so this is worth raising before the instance's own type." | ||
| 118 | type = string | ||
| 119 | default = "cx22" | ||
| 120 | } | ||
| 121 | |||
| 122 | variable "runner_labels" { | ||
| 123 | description = "Labels this runner advertises, which a job's runs-on asks for." | ||
| 124 | type = list(string) | ||
| 125 | default = ["linux"] | ||
| 126 | } | ||
| 127 | |||
| 128 | variable "runner_network" { | ||
| 129 | description = "none gives a job no network even if it asks; bridge gives one to a job that asks. Jobs that fetch dependencies need bridge." | ||
| 130 | type = string | ||
| 131 | default = "bridge" | ||
| 132 | |||
| 133 | validation { | ||
| 134 | condition = contains(["none", "bridge"], var.runner_network) | ||
| 135 | error_message = "runner_network must be none or bridge." | ||
| 136 | } | ||
| 137 | } | ||
| 138 | |||
| 139 | variable "runner_cloud_init_file" { | ||
| 140 | description = "Override the runner's user data file. Empty uses deploy/cloud-init/runner.yaml from this repository." | ||
| 141 | type = string | ||
| 142 | default = "" | ||
| 143 | } | ||