Builders and build recipes

What a platform recipe of the Stado-native release pipeline declares, what the builder reads and measures, and how placement and compiled dependencies follow from it.

Platform output coordinates are canonical lowercase identifiers. Each recipe names the verified fleet runner_platform (darwin-arm64 or linux-amd64), quality argv in order, one build argv, staged source-to-archive paths, and optional secret_env references of the form ENV=item#field. Builders receive no repository coordinate or repository token. They materialize only the exact source URI and declared immutable inputs. Inputs are digest-checked and either extracted under WISENT_INPUTS_DIR or mounted there as their original archive, as declared by each input's extract field. Builders write output through the queue's existing canonical job-output collection.

A recipe may also declare min_free_gb, the free space its build needs on the work volume. The worker reads the volume holding the checkout through the host's own df -Pk and refuses before the first quality gate when the space is not there, naming the requirement, the reading and the path. A recipe that declares nothing is not measured. The refusal is the whole point of the field: a build that runs out of disk otherwise fails twenty minutes in, with the requirement readable only as No space left on device inside a long job log. stado space reclaim <host> --apply makes the room; the refusal says how much is missing.

A recipe field is read by the worker installed on the builder, not by the coordinator that queues the job, and the manifest refuses fields it does not know. A product therefore declares a new field only after a release carrying its reader is installed on every builder of that product's platforms: a manifest that declares a field before its reader is installed makes every builder's worker refuse the job at the manifest with unknown field. The reader ships first; the declaration follows it.

The worker reads only the product and the recipe of the platform it builds. deliveries, promotion, inputs and runtime are the control host's to read, with the binary the operator submits with, so a change of shape in one of those sections — a delivery target that becomes the product's destination declaration — does not stop the build that carries its reader on a builder still running the previous release.

The builder also measures what its build actually wrote. When the build step ends, before the scratch tree is removed, the worker records the tree's bytes and the free bytes left on its volume in scratch.json beside the receipt, and a failed build's receipt and exit sentence carry the same two numbers (the build tree held 14.8 GiB and its volume had 0.0 GiB free (ran out of disk)). The record is its own output leaf, never a receipt field, so a reader older than it is not asked to parse it.

Placement reads that evidence. Before a build job is queued, the coordinator looks up the newest scratch.json for the same product and platform among that product's own builds — runs/build/<product>/<build>/platforms/<platform>/output/, or a retry's attempts/<id>/output/ — with one listing of that product's build tree, and judges every live builder's capacity publication against it: a host whose headroom — what may still be written before its volume reaches the 80% disk-full threshold, computed from the published free_disk_gb and disk_used_percent — is less than that scratch is refused with release_scratch_short (<free> GiB free, <headroom> GiB before the 80% disk-full threshold; the last <platform> build of <product> wrote <size> GiB on <builder>; stado space report shows what holds the volume), and nothing is queued. A build that failed for lack of disk is a floor, reported as wrote at least … before running out of disk on <builder>. A build that took the volume past the threshold would have the janitor delete its own cache under it. Among the builders that may take the job, one that can start now goes before one that can only wait (busy CPU, low RAM headroom, an exclusive job, or cleanup_in_progress, the janitor's turn on a volume past the disk-full threshold, after which the next claim follows); then the one compiling fewer builds of the same product goes first — the same listing counts, per builder, this product's platform requests from the last two hours with no receipt after them, because builds of one product on one host share its Cargo build directory and wait on its lock; then the one publishing the most free disk; name order decides only ties. A product no measuring builder has built yet is placed without a scratch verdict — silence is not a requirement — and its first build leaves the record the next one is judged by.

The keys in stage are relative to WISENT_OUTPUT_DIR, not the source checkout. The worker normally binds that output directory to source/.wisent-output for archive staging. A build script that writes into that output directory stages from it directly: Stado's own deploy/release/build_stado.sh copies the compiled binary to stado-rs/target/release/stado in the release tree under WISENT_OUTPUT_DIR, which is exactly the stage key. Putting .wisent-output/ in that key repeats the directory prefix and packaging fails even after compilation succeeds. Stage values name paths inside the release archive.

Compiled dependencies belong to the builder, not to the job. Every build job is handed CARGO_TARGET_DIR at <work root>/build-cache/<product>/<platform>/cargo-target, so the next release of the same product recompiles only what its commit changed; only the job's own scratch is deleted per run. A recipe step that overrides that variable with a --target-dir inside the output tree throws the cache away and recompiles the whole dependency graph every release, which makes a build many times slower than a warm build of the same commit. stado release status prints what each platform's build cost, from the job's own clock, so that difference is visible without a stopwatch.

Source: this website