Skip to content

v1.2.0

Choose a tag to compare

@github-actions github-actions released this 31 Aug 07:33
· 14 commits to main since this release
v1.2.0
2b6f8f3

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:009fa48c7e28681b9778d8c0b719c53f96e3624d2bef126ca0ea06b6d527e849

The 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
    chronological vN. 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 --json and allow attempts list to continue through the full queue.
  • status --json is now v2. 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
    beyond memory.max and 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 let serve
    start at all; once running, the only account of it was a line in the
    controller's log. runpool status --json now carries egress_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 ending USER runner cannot 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:
    still capsule_incompatible, so runpool attempts reads 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; and govulncheck, 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.enabled was 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-runners

Attached: standalone linux/amd64 and linux/arm64 binaries with
checksums.txt, an SBOM for each image, and the qualification
record this release was authorized against.