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/server/ci.go +12−5
| @@ -100,8 +100,14 @@ func (s *Server) enqueue(ev runs.Event) { | |||
| 100 | 100 | } | |
| 101 | 101 | // What this push owes: one run per job of every workflow that applies to it. The | |
| 102 | 102 | // list is built before anything is queued, because a push that owes nothing must | |
| 103 | // not cancel the previous tip's builds. | ||
| 104 | var owing []runs.Run | ||
| 103 | // not cancel the previous tip's builds. A job's runs-on labels travel beside the | ||
| 104 | // run rather than inside it: they are lease policy, and Enqueue takes them as its | ||
| 105 | // own argument so a caller holding the job cannot forget them. | ||
| 106 | type owedRun struct { | ||
| 107 | run runs.Run | ||
| 108 | labels []string | ||
| 109 | } | ||
| 110 | var owing []owedRun | ||
| 105 | 111 | for _, wf := range wfs { | |
| 106 | 112 | switch { | |
| 107 | 113 | case branch != "" && !wf.Applies(branch): | |
| @@ -127,7 +133,7 @@ func (s *Server) enqueue(ev runs.Event) { | |||
| 127 | 133 | Created: time.Now().UTC(), | |
| 128 | 134 | } | |
| 129 | 135 | run.ID, _ = id.New(time.Now()) | |
| 130 | owing = append(owing, run) | ||
| 136 | owing = append(owing, owedRun{run: run, labels: job.Labels}) | ||
| 131 | 137 | } | |
| 132 | 138 | } | |
| 133 | 139 | if len(owing) == 0 { | |
| @@ -142,8 +148,9 @@ func (s *Server) enqueue(ev runs.Event) { | |||
| 142 | 148 | } else if n > 0 { | |
| 143 | 149 | s.Log.Info("ci: canceled superseded runs", "repo", ev.Repo, "ref", ev.Ref, "runs", n) | |
| 144 | 150 | } | |
| 145 | for _, run := range owing { | ||
| 146 | created, err := s.Runs.Enqueue(run) | ||
| 151 | for _, owed := range owing { | ||
| 152 | run := owed.run | ||
| 153 | created, err := s.Runs.Enqueue(run, owed.labels) | ||
| 147 | 154 | if err != nil { | |
| 148 | 155 | s.Log.Warn("ci: cannot queue a run", "repo", ev.Repo, "job", run.Job, "err", err) | |
| 149 | 156 | continue | |