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 run record names the machine, not the toolchain

379dad1e7417b9153a4913402cfc97bb51ce1bb4 · Keenan Nemetz · 16d ago · parent 72797dcd

Phase 4: a run record names the machine, not the toolchain

A finished attempt now says which operating system and architecture ran it,
in record.md beside the exit code and the signer, so the two fields ride the
signature that is the attestation. A result a clone carries says what kind
of machine produced it, not only which job it was — which is what turns
"one macOS runner in a Linux pool" into a question somebody can answer
after the fact, and the first half of the targeting work that remains.

The roadmap also asked for the runner's Go version and that was dropped
deliberately: runtime.Version() names the toolchain the *runner binary* was
compiled with, which is not the Go that builds anything in a job. A step
running `go build` uses whatever is on PATH in the checkout, usually a
different install, so in a signed record the field would be a confident
answer to a question nobody asked. OS and architecture survive the same
objection because they describe the machine, which is the fact this was for.

The keys are optional and an absent machine writes nothing at all rather
than an empty line, because a record is never rewritten and history that
predates a field has to stay readable. runs.db mirrors the pair the way it
mirrors the log and the artifacts — only while the runner still holds the
lease, so an overtaken runner leaves no claim behind — and `vole index
rebuild` restores it from the branch.

Measured: TestRunRecordIsPortable carries the machine through the record,
the row, the rebuild and the page; TestRecordWithoutMachineStillReads and
TestMachineIsRecordedOnlyWhileTheLeaseIsHeld pin the two edges.

docs/roadmap.md +25−2

@@ -492,10 +492,32 @@ naming either would confirm the repository exists.
492 492 `TestRunnersPageShowsTheRunnerHoldingAJobAndTheQueueDepth`,
493 493 `TestRunnersPageKeepsAPrivateRepositoryOut`.)
494 494
495 ✅ **Slice 9 — the record names the machine.** A finished attempt says which operating
496 system and architecture ran it, in `record.md` beside the exit code and the signer, so
497 the two fields ride the same signature as everything else: a result a clone carries now
498 says what kind of machine produced it, not only which job it was. This is what makes
499 "one macOS runner in a Linux pool" a question a reader can answer after the fact, and it
500 is the first half of the targeting work below. Records written before the field existed
501 still read — the keys are optional and an absent machine writes nothing at all rather
502 than an empty line — because a record is never rewritten and history has to stay
503 readable. `runs.db` mirrors the pair the way it mirrors the log and the artifacts: only
504 while the runner still holds the lease, so an overtaken runner leaves no claim behind,
505 and `vole index rebuild` restores it from the record.
506 (Measured: `TestRunRecordIsPortable` — the machine survives the record on the branch,
507 the row, the rebuild and the page; `TestRecordWithoutMachineStillReads` and
508 `TestMachineIsRecordedOnlyWhileTheLeaseIsHeld` pin the two edges.)
509
510 **The Go version was dropped from this slice, deliberately.** The roadmap asked for "the
511 runner's Go version" beside the OS and architecture, and it does not earn its place:
512 `runtime.Version()` names the toolchain the *runner binary* was compiled with, which is
513 not the Go that builds anything in a job — a step running `go build` uses whatever is on
514 `PATH` in the checkout, usually a different install. A number that looks authoritative,
515 sits inside a signed record and is routinely wrong about the thing a reader would assume
516 it describes is worse than no number. OS and architecture survive the same objection
517 because they describe the machine, which is the fact this slice was actually for.
518
495 519 Remaining:
496 520
497 - Toolchain environment in the record (the runner's Go version, OS, architecture) — the
498 record names the job, not the machine
499 521 - A narrowing filter on the runner (`--repos`, labels) and job-level targeting
500 522 (`runs-on`): one instance-wide pool is right for identical machines and wrong for one
501 523 macOS runner in a Linux pool. The filter can only narrow what the allowlist grants,
@@ -1003,6 +1025,7 @@ Filled in as phases land. Append; don't rewrite.
1003 1025 | 2026-10-23 | An idle lease parks on an in-process wake-up channel, and eligibility is cached for one poll window | The "long poll" was the client's own timer — the queue answered at once and the runner slept — and every ask re-derived the account's repositories by opening each one's rules, which is O(repos) per idle runner every two seconds. The wait is a hint and never a durability mechanism (the lease is still the database's decision), so a missed wake-up costs one poll interval; in-process is enough because one `vole serve` owns the queue, and a second process would have to ask the database anyway |
1004 1026 | 2026-10-23 | A new tip cancels the queued runs of the same ref, and the lease serves the least busy repository first | Pushing twice in a minute built the first commit twice while `canceled` existed with no writer. The cancellation covers queued rows only: interrupting a running build is a different promise from not starting one. Fairness is ordering rather than a per-repository cap, because a cap is a number somebody has to tune and ordering needs none — a burst still runs, it just cannot take every runner |
1005 1027 | 2026-10-23 | Runner presence is a queue row per channel, filtered by the clone rule | A runner was an SSH channel invisible to both the web and the queue, so "is my build waiting because nobody is listening" had no answer anywhere. Presence is derived rather than stored: written by connect, lease, heartbeat and finish, dropped when the channel closes, aged out after ten silent minutes. The web is anonymous, so a session appears only while the repository it is building is public — naming a private repository would confirm it exists |
1028 | 2026-10-23 | A record names the machine (OS, architecture), not the toolchain | The roadmap asked for the runner's Go version too, and it is the one field here that would mislead: `runtime.Version()` reports what compiled the *runner binary*, not what a job builds with, so in a signed record it would be a confident answer to a question nobody asked. OS and architecture describe the machine, which is the fact worth keeping — it is what turns "one macOS runner in a Linux pool" into something a finished run can answer. Both are the binary's build target rather than a live probe: a `uname` subprocess would add a failure path on Windows to sharpen a fact the binary already knows, and the only case it would correct is emulation. The keys are optional so records written before them still read, since a record is never rewritten |
1006 1029
1007 1030 ## Open questions
1008 1031