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