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.
internal/web/runners.go +10−1
| @@ -16,6 +16,7 @@ package web | |||
| 16 | 16 | import ( | |
| 17 | 17 | "net/http" | |
| 18 | 18 | "sort" | |
| 19 | "strings" | ||
| 19 | 20 | "time" | |
| 20 | 21 | ) | |
| 21 | 22 | ||
| @@ -31,6 +32,11 @@ type runnerView struct { | |||
| 31 | 32 | Job string | |
| 32 | 33 | Seen time.Time | |
| 33 | 34 | Idle bool | |
| 35 | // Labels are what the runner says it is, joined for display. They matter here | ||
| 36 | // because a job waiting for a machine that is not connected is invisible from the | ||
| 37 | // queue side: the depth says seven waiting, and only this says that nobody | ||
| 38 | // carrying the label the job asks for has ever connected. | ||
| 39 | Labels string | ||
| 34 | 40 | } | |
| 35 | 41 | ||
| 36 | 42 | type queueView struct { | |
| @@ -59,7 +65,10 @@ func (w *Web) RunnersPage(rw http.ResponseWriter, r *http.Request) { | |||
| 59 | 65 | return | |
| 60 | 66 | } | |
| 61 | 67 | for _, s := range rows { | |
| 62 | v := runnerView{Account: s.Account, Repo: s.Repo, RunID: s.RunID, Seen: s.LastSeen, Idle: s.RunID == ""} | ||
| 68 | v := runnerView{ | ||
| 69 | Account: s.Account, Repo: s.Repo, RunID: s.RunID, Seen: s.LastSeen, | ||
| 70 | Idle: s.RunID == "", Labels: strings.Join(s.Labels, ", "), | ||
| 71 | } | ||
| 63 | 72 | if w.Ident != nil { | |
| 64 | 73 | if name := w.Ident.Name(s.Account); name != "" { | |
| 65 | 74 | v.Account = name | |