Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

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