Repository navigation
v1.2.0
Changes since v1.1 strengthen durable delivery, admission, qualification and
large-backlog operation. The status document advances to v2, the state
schema advances through forward-only migrations, and release evidence now
proves the exact cases, platform and candidate images it represents. The
runbook covers upgrade, rollback and the conditions that
need an operator.
Deploy this release
Point the Compose deployment at this controller digest and recreate
the container:
RUNPOOL_IMAGE=ghcr.io/rhobuild/runpool@sha256:009fa48c7e28681b9778d8c0b719c53f96e3624d2bef126ca0ea06b6d527e849The controller carries the capsule digest below stamped into it and
refuses any other, so there is one image to set. Read the
deployment guide
before a first installation, and
the runbook
before moving an existing one.
What changed
Reliability and scheduling
- Disk admission starts closed until a fresh measurement succeeds. Missing,
stale or failed pressure measurements can no longer advertise provider
capacity. Worsening pressure closes admission before persistence; recovery
opens it only after the improved verdict commits. - Discovery credit follows the poll that owns it. Immutable allocation
plans carry a generation and pool holder; an empty result can rotate only
its own current holder, and capacity transfers revoke the previous holder
before granting the next one. - The discovery credit no longer waits on a busy holder. A binding
holding the credit that found work kept it until it happened to drain and
poll empty, which nothing bounds — and every other silent binding in the
tier announced zero to the broker for the duration. The credit now moves
to the next silent binding the moment its holder gains demand or work,
and the thaw after a disk-pressure hold re-checks the pointer, since
bindings keep gaining work while held. - Delivery fingerprints use an unambiguous canonical encoding. New rows use
length-prefixed SHA-256 input. Historical rows retain the delimiter encoder
named on their row, so exact redelivery remains idempotent after upgrade. - Durable internal formats have semantic names. Delivery keys, configured
binding keys and fingerprint encodings identify their contents instead of a
chronologicalvN. The migration preserves row ids and existing digest
bytes.
Scale and operations
- Allocation publishes one immutable plan per mutation. Batched
water-filling replaces credit-by-credit scans, and concurrent readers share
the same generation. - Scheduling reads bounded ready batches. A large durable backlog is not
materialized merely to admit the few attempts current capacity can serve. - Operational attempt lists are paginated. Stable cursors and totals bound
status --jsonand allowattempts listto continue through the full queue. status --jsonis nowv2. The binding identity field is
configured_binding_key, matching its source in operator configuration;
persistence column names remain private.
Qualification
- Every qualification boundary emits structured evidence. The assembler
rejects missing or skipped cases, mismatched commits, platforms and image
digests, and evidence from the wrong suite. - Kernel resource claims are exercised independently under pressure. One
live capsule contract reaches the gateway PID ceiling, reaps the pressure
processes and proves the relay recovers. A fresh capsule observes swap use
beyondmemory.maxand attributes an inner workload's OOM to the aggregate
capsule cgroup, so PID exhaustion cannot invalidate the memory evidence. - Platform evidence is emitted by a typed collector. Release qualification
reads Docker through its API and serializes host facts with Go's JSON encoder;
shell remains responsible only for workflow orchestration. - Controller crash adoption is a real
SIGKILL. The end-to-end workload
kills the active controller, starts a successor on the same state and proves
adoption, completion, cache isolation and ownership-safe cleanup. A portable
hosted leg keeps the regression executable; the exact reference-host leg is
the release authority.
Isolation and egress
- An egress sandbox that closed itself says so. Rediscovery runs every
five minutes, and a pass it cannot complete closes every gateway on the
host to all egress — the right answer to a policy that cannot be shown to
be current, and also every running job losing its network at once, for as
long as discovery keeps failing. The same failure refuses to letserve
start at all; once running, the only account of it was a line in the
controller's log.runpool status --jsonnow carriesegress_sandbox
with the last pass's time and, when it failed, why.
Operations
- A release page says what changed. The body carried the qualified commit
and the two image digests and nothing else, so the page every reader reaches
a version from answered what exactly this is and never answered what is in
it. It now opens with the changelog entry for the tag — the same bytes the
gate checked, carried from the step that checked them rather than read a
second time — and follows with the digest to deploy, the grouped detail, and
the provenance and the command that verifies it. - A capsule that died before it could explain itself now carries what it
said. An operator's own capsule image is built by deriving from the
published one, and the published one stays root deliberately: the
supervisor is PID 1, boots the inner Docker daemon, and drops the runner
to uid 1001 itself. A derived image endingUSER runnercannot write the
root-owned control surface the launcher mounts for it — so it cannot write
the abort that would have said so either, and every attempt on that tier
was held with an exit code and the words "the capsule image and this
controller are not a pair", which sends an operator to re-check a digest
that was correct. The reason was in the container's log the whole time,
and the refusal now quotes it — in the controller's log, where the
refusal is reported. What the held attempt itself records is unchanged:
stillcapsule_incompatible, sorunpool attemptsreads as before. - A step that fetches over the network is tried again when it fails.
Four release cycles were lost to services answering badly and nothing
asking twice — a digest verification that could not reach Docker Hub, a
base image fetched through a 502, a module proxy that reset mid-download,
and a push accepted layer by layer and then called an unknown blob — each
costing a manual re-run, one of them after the tag was already cut. Every
build, push, pull and index assembly goes through the retry, and so does
the module download each job now does once, so the builds and the tests
that follow it work from a warm cache. Three reaches are still asked
once, and naming them is the point: a step that is an action rather than
a command — the SBOM scan and the attestations — which is not a command
line to wrap;docker login, whose token arrives on a pipe the first
attempt consumes; andgovulncheck, which fetches its vulnerability
index on every run and would, if retried, print two spurious attempts
over a real finding. - A binding that cannot persist what its provider hands it no longer
reports as healthy. The poll that carries a message records contact when
it succeeds, and a delivery that could not be persisted was left for
redelivery — which arrives through another poll, which records contact
again. A binding wedged that way refreshed its own health forever while
nothing it was offered ever became an attempt, so the work was not queued
either: it read as a binding in perfect health from every angle an
operator has. The failure is now recorded, as the acquisition failure
beside it already was. - The absence of a metrics endpoint is a decision, and the runbook now
says what to watch instead.observability.metrics.enabledwas refused
with the word "yet", which is a promise nobody had a date for. There is no
endpoint and there will not be one at this scope:runpool status --json
is the machine-readable account, versioned for exactly this, and deciding
when a person has to look belongs to the host. The runbook's new "Watching
an instance" names the ten conditions that need one and gives a command
that prints a line per condition and nothing when there is nothing to look
at. The field is still accepted — configuration parsing is strict and the
shipped example writes it, so removing it would fail the startup of every
deployment that copied that example — and removing it would be a
major-version break, which classifies the change rather than schedules it. See
the decision record.
Provenance
| Release-qualified commit | 2b6f8f314a8f4a725acf91a152a38e5fc8b2360f |
| Controller | ghcr.io/rhobuild/runpool@sha256:009fa48c7e28681b9778d8c0b719c53f96e3624d2bef126ca0ea06b6d527e849 |
| Capsule | ghcr.io/rhobuild/runpool/capsule@sha256:e21bb40c9a4990ddeb49ff5bdc14c5a05b2966fe04ee547646429a771e53129b |
Publication runs only after qualification passed against these exact
digests, so an attestation signed here says more than that GitHub
built it. Verify a digest before it reaches a deployment:
gh attestation verify oci://ghcr.io/rhobuild/runpool@sha256:009fa48c7e28681b9778d8c0b719c53f96e3624d2bef126ca0ea06b6d527e849 \
--repo rhobuild/runpool \
--signer-workflow rhobuild/runpool/.github/workflows/release.yml \
--source-ref refs/tags/v1.2.0 \
--deny-self-hosted-runnersAttached: standalone linux/amd64 and linux/arm64 binaries with
checksums.txt, an SBOM for each image, and the qualification
record this release was authorized against.