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.
Phase 4: a job can name the machine it needs
6c03e461ee409f4bd053ee552edd87b6188a5eb2 · Keenan Nemetz · 16d ago · parent b238ac24
Phase 4: a job can name the machine it needs `runs-on: [linux, arm64]` on a job, `--label linux` on a runner, and the lease hands a job only to a runner carrying every label it asked for. Extra labels on the runner are not a mismatch — the rule is set inclusion, so a machine more specific than the job needs still qualifies — and a job with no runs-on matches every runner, which is what every job did before this existed. Both spellings are accepted (`runs-on: linux` and a list), because both are the obvious way to write it and a dialect already separate from GitHub's should not also fail on the spelling a person guessed. Labels are validated in pre-receive by the same rule the runner applies to --label at startup: a label no runner can carry leaves a job queued forever and looks exactly like a busy pool, which is the failure nobody debugs because there is no error. Labels are pinned at enqueue next to the steps and the artifact patterns. A workflow edited after a push must not change where an already-queued job is allowed to run. That also settles the derived-store question: run_labels is queue state, not history, because a finished run's requirement is recoverable from the pinned workflow blob, so `vole index rebuild` does not restore labels and nothing needs it to. A label is a claim by the runner about itself, unlike --repos, which the server enforces. A runner that lies about its architecture only takes work it will do badly; a runner that could name repositories it was not granted would be an access-control hole. /runners now shows what each connected runner takes, and says "any job" for an unlabelled one — the other half of "why has my build not started", since queue depth alone cannot say that no runner carrying the needed label is connected. Enqueue takes labels as a second argument rather than a field on Run: they are lease-time policy, not part of what a run is, and every reader of a Run (a gate, a page, a record rebuild) would be looking at a field only a lease ever consulted. Measured: TestRunsOnWaitsForARunnerThatCarriesTheLabel — a runner carrying only `linux` leaves a `[linux, fast]` job queued and the runner carrying both builds it. TestLabelsDecideWhichRunnerMayTakeAJob covers no requirement, exact match, one label missing, wrong labels and extra labels; TestLabelsArePinnedAtEnqueue, TestForgettingARepositoryDropsItsLabels, TestABadRunnerLabelIsRefusedBeforeConnecting, TestRunsOnLabelsAreParsedAndValidated and TestRunnersPageShowsWhatEachRunnerTakes cover the edges.
README.md +23−5
| @@ -312,6 +312,7 @@ on: | |||
| 312 | 312 | branches: [main] | |
| 313 | 313 | jobs: | |
| 314 | 314 | build: | |
| 315 | runs-on: [linux, arm64] # only a runner carrying both labels takes this job | ||
| 315 | 316 | steps: | |
| 316 | 317 | - name: test | |
| 317 | 318 | run: go test ./... | |
| @@ -343,12 +344,29 @@ file), and `vole index rebuild` brings the finished history back from the record | |||
| 343 | 344 | each repository's `vole` branch. Pushing again cancels the *queued* runs of the commit | |
| 344 | 345 | it replaced — never a running one — and one runner pool serves every repository: an | |
| 345 | 346 | idle runner parks on the lease instead of asking again, and the lease prefers the | |
| 346 | repository with the least work in flight. | ||
| 347 | repository with the least work in flight. `max_jobs_per_repo` bounds how much of the | ||
| 348 | pool one repository may occupy, when ordering is not enough. | ||
| 347 | 349 | ||
| 348 | `/runners` is the page for "why has my build not started": the connected runners, the job | ||
| 349 | each is building (or that it is waiting for one), and how much each repository has | ||
| 350 | queued and running. It follows the same visibility rule as everything else, so a runner | ||
| 351 | building a private repository is not shown. | ||
| 350 | `runs-on` names the machine a job needs, as one label or a list, and a job goes only to | ||
| 351 | a runner that carries *every* label it asks for (extra labels on the runner are fine). | ||
| 352 | A job with no `runs-on` runs anywhere, which is what every job did before targeting | ||
| 353 | existed. The labels are a claim by the runner about itself, not a permission: | ||
| 354 | |||
| 355 | ```sh | ||
| 356 | vole runner --remote ssh://git@host:2222 --label linux --label arm64 | ||
| 357 | vole runner --remote ssh://git@host:2222 --repos demo --repos docs # only these | ||
| 358 | ``` | ||
| 359 | |||
| 360 | `--repos` is the other direction and is *not* a claim: it narrows what the account's | ||
| 361 | runner allowlist already grants, and the server applies the intersection, so the worst a | ||
| 362 | runner can do is take less work than it was allowed. A filter that matches nothing is | ||
| 363 | refused at startup rather than presenting as a healthy idle runner. | ||
| 364 | |||
| 365 | `/runners` is the page for "why has my build not started": the connected runners and | ||
| 366 | what each takes, the job each is building (or that it is waiting for one), and how much | ||
| 367 | each repository has queued and running — including whether it is at its | ||
| 368 | `max_jobs_per_repo` cap. It follows the same visibility rule as everything else, so a | ||
| 369 | runner building a private repository is not shown. | ||
| 352 | 370 | ||
| 353 | 371 | ### Releases built by CI | |
| 354 | 372 | ||