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.
Phase 4: a job can name the machine it needs
6c03e461ee409f4bd053ee552edd87b6188a5eb2 · Keenan Nemetz · 17d 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.
internal/sshserver/repos_filter_test.go +2−2
| @@ -81,7 +81,7 @@ func TestARequestCannotLeaseBeyondTheAllowlist(t *testing.T) { | |||
| 81 | 81 | SHA: strings.Repeat("ab", 20), Workflow: ".vole/workflows/ci.yaml", | |
| 82 | 82 | WorkflowSHA: strings.Repeat("cd", 20), Job: "forbidden", | |
| 83 | 83 | Steps: []runs.Step{{Name: "test", Run: "true"}}, Created: time.Now().UTC(), | |
| 84 | }); err != nil { | ||
| 84 | }, nil); err != nil { | ||
| 85 | 85 | t.Fatal(err) | |
| 86 | 86 | } | |
| 87 | 87 | ||
| @@ -141,7 +141,7 @@ func TestARepoFilterTakesOnlyWhatItNames(t *testing.T) { | |||
| 141 | 141 | SHA: strings.Repeat("ab", 20), Workflow: ".vole/workflows/ci.yaml", | |
| 142 | 142 | WorkflowSHA: strings.Repeat("cd", 20), Job: "job-" + name, | |
| 143 | 143 | Steps: []runs.Step{{Name: "test", Run: "true"}}, Created: time.Now().UTC(), | |
| 144 | }); err != nil { | ||
| 144 | }, nil); err != nil { | ||
| 145 | 145 | t.Fatal(err) | |
| 146 | 146 | } | |
| 147 | 147 | } | |