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/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