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