On this page
Native builds
Stado stores native build recipes in the canonical registry. A recipe names a public HTTPS Git repository, a branch, one POSIX shell command, the artifact paths produced by that command, and one or more worker platforms. The coordinator watches the branch and enqueues one job per platform when its head changes. A worker can claim only the job for its own platform.
Builds and releases are separate. A build uploads the declared artifacts and may record the exact semantic-version tag found on the commit. It does not sign an artifact, change release_control, or promote a release. stado release submit owns qualification, signing through Skarbiec, publication, delivery, and installation.
When Stado itself is delivered, the installer records the installed version
beside the managed binary. Each managed queue agent finishes its active jobs,
detects that its loaded version differs, and exits through its declared
KeepAlive or Restart=on-failure policy; the supervisor then starts the
installed release without unloading the unit or interrupting a job.
Before executing an artifact, every delivery verifies both the newest submitted
source in the product catalog and the exact published platform coordinate in
its still-delivering run. This fences an older queued delivery without assuming
the product has a separate release-control rollout policy.
The Stado delivery job starts its worker from the digest-pinned candidate
archive and that worker uses itself for install-local; a broken older
installed worker therefore cannot prevent the release that repairs it.
A non-Stado archive contains that product rather than another Stado binary, so
its delivery runs the installed Stado worker against the digest-pinned archive.
Repeating stado release submit resumes the same release run. An initial
platform submission keeps its original stable job identity and output URI.
Before replacing a failed platform attempt, the coordinator reads the original
job: queued, running, completed, and uploaded jobs keep that identity and their
existing output. A publication or storage-read failure does not start another
build. A failed job also keeps its identity when its durable build receipt says
passed: the bootstrap's upload can fail after compilation and qualification
succeed, while the native finalizer subsequently preserves the output.
If canonical output is missing, resumption reads the retained receipt from the
recorded builder through Stado. It verifies the receipt's run, job, product,
version, platform, builder, source and manifest bindings, then asks that builder's
installed Stado to upload its retained command log, archive and receipt to both
the attempt and canonical output locations. The receipt is uploaded last;
publication checks the storage write receipts, reads the canonical receipt back,
and verifies the actual archive's SHA-256 before using it.
A mismatch returns release job returned mixed or invalid output; a missing
archive returns release job omitted archive. Neither replaces a passed build.
The original job keeps its recorded terminal state and cause; recovery does not
rewrite that history.
The worker also checks for its own exact completed output before executing a replayed request. It reuses a passed receipt only when the immutable request, including its inputs and secret references, matches and the retained archive still has the recorded size and SHA-256. That replay returns the existing build instead of running the compiler again.
The stored platform request fixes its builder and input archives. Resumption
checks that request against the release's source and manifest, preserves its
exact bytes, and uses the already-staged inputs instead of fetching them again.
An existing queue plan retains its recorded consumer and does not require a
new live-builder selection. If no queue plan exists yet, the saved builder
must still pass normal admission; another machine is not silently selected.
An altered request returns immutable queue object differs: PATH without
overwriting the saved request or creating a replacement build.
stado release status stado --json shows the recorded release runs, including
each platform's builder, job ID, state, and failure. A recorded platform failure
can describe a publisher read error rather than a failed worker; resumption
checks the job and its build receipt before deciding whether another attempt
is needed. A published platform can retain a failed job whose passing build
receipt and archive were recovered; the qualification receipt records the
successful build and the job record preserves the failed upload.
When a required delivery fails, the submit command names the delivery and its
queue job, followed by that job's recorded failure and last command output.
The same detail is retained in the run's delivery record. Use
stado machine status JOB_ID and stado machine logs JOB_ID to inspect that
exact job; no new build or delivery is needed to retrieve its failure.
A cancelled unfinished build, or a failed build without a passed receipt,
receives a retry identity derived from the release run, platform, and prior
terminal job ID, with its own output URI. A cancelled replay is different when
its retained passing receipt was completed before that replay started: resumption
can publish the earlier completed build without undoing the later cancellation.
A passing receipt from the cancelled attempt itself does not override cancellation.
A crash before or after the replacement platform record is saved reuses the
same attempt; only another recorded terminal job failure can chain to another
retry. An unreadable job or receipt returns the storage or decoding error.
A missing job returns
release job JOB_ID was not found in recorded states; refusing a replacement without terminal failure;
neither condition creates a replacement build.
Local queue jobs execute from the agent owner's
~/.stado/work/jobs/wc-<job-id> tree, not a temporary directory. Admission
resolves the physical home, then creates or opens .stado, work, and jobs
one component at a time as owner-only directories with
O_DIRECTORY|O_NOFOLLOW; component symlinks are refused. The serialized
old-agent bridge repeats that refusal and changes permissions or
creates the next child only from a held cwd whose physical path it just
verified.
A release submission remains able to repair an older agent: its deterministic
bootstrap moves that agent's already-materialized job into the persistent root
before the worker starts and preserves a compatibility symlink for the old
agent's log and artifact upload. It also pins TMPDIR, TMP, and TEMP
beneath that persistent tree so a pre-migration worker cannot put its release
scratch back in OS temp.
After a successful worker, the bridge uses the checked storage writer to upload
and read back the command log and archive at both canonical status and exact
attempt URIs, then publishes the attempt's receipt.json last as the completion
marker. Each writer receipt must match the quiescent local source's SHA-256 and
byte count. Its JSON and lifecycle-watch responses use random 0600 files under
the validated work tmp directory, are removed after parsing, and leave only
their proof lines in the already-open command log. A failed write or read-back
turns the workload into a failure instead of allowing a successful terminal
record.
For a worker that already failed, the bridge still proves the command log at
both destinations and, when an owned regular receipt.json exists, publishes
that failed receipt to canonical then attempt storage. Evidence errors are
appended to the safe log without replacing the worker's original nonzero exit
code; the legacy link remains for the old agent's canonical finalization.
The bootstrap prints both physical workdir and effective temporary root before
invoking the worker, so an operator can prove the retry left /tmp from the
canonical job log. Queue-workdir reclamation runs only through the janitor under
its exclusive admission lock; every pass reserves a bounded scan share for
terminal old-agent links independently of canonical workdir enumeration.
Each agent tick observes all local workload exits and releases their cleanup
holds before reading remote policy or finalizing any slot. A slow upload or
queue read for one job cannot retain the holds of siblings that already exited;
live workloads remain protected.
If the canonical tree is ever absent or replaced while a job is live, the agent
emits workdir_missing with its exact expected path at heartbeat and
finalization, then retains that marker and the real workload exit code in the
terminal job error. An existing empty command log remains the distinct
wrote no output case.
stado release redeliver PRODUCT RUN_ID DELIVERY --retry-token TOKEN is the
operator recovery path for one delivery from the exact newest completed run. It
does not publish a new candidate or move a channel. The retry token identifies
a fixed per-run CAS transaction and stable queue submission, so repeating the
same command resumes after interruption. A different token is refused while a
transaction is active. Success replaces that delivery's job, output, state, and
receipt evidence before restoring the run's prior state; failure preserves the
previous passed delivery while recording the terminal failure and restoring the
run state.
stado host config-set TARGET KEY VALUE migrates an older deployment profile
through the installed stado config migrate before applying the field.
The migration preserves the exact prior file beside the profile and refuses
newer schemas. No separate migration command is needed;
--reload-service SERVICE activates the change through the declared service policy.
Request failure diagnostics
A storage request failure includes the HTTP client's complete cause chain,
not just error sending request. Read the failed operation and URL together
with the underlying cause: for example, Connection refused (os error 61) means
the connection was refused, not that compilation failed. The operating system
error number varies by platform. A TLS or timeout failure reports the cause
supplied by the HTTP client rather than being relabelled as a connection refusal.
The same message reaches the CLI, a release run's saved failure when that
record can be written, and the native Desktop command API's stderr.
Stado Desktop's Releases screen displays the recorded failure without shortening
its cause chain. If recording the failure also fails, the command keeps the
original error followed by failed to persist release failure and the second
error; the stored run may therefore not contain the latest failure.
This change adds no request, retry, rebuild, or field to stored records. Existing failure classification uses the richer message. It cannot reconstruct causes discarded by an older binary. Inspect the retained command output from that run before attributing an older generic message to a specific network fault.
Release publication authority
The publisher command reads the bearer named by release_api.publishers through
its selected credential store and configured WC_SKARBIEC_CONSUMER grant.
When owner credentials are available, Stado adds only that item's token read
to the same grant before reading it; an already authorized read does not depend
on that repair being available.
The release server uses release_api.skarbiec.url and its separate
stado-release-api-verifier grant to verify the publisher bearer. Release
signing uses the same declared authority with stado-release-coordinator,
which reads only the signing key. The publisher command never authenticates
its credential read as the server verifier, and STADO_API_TOKEN remains a
generic object API credential, never a product release bearer.
The public release route accepts exactly one uri query field. Exact
GET /api/release/object?uri=stado://releases/... reads and byte ranges are
public so a damaged credential plane cannot make signed recovery artifacts
unreachable; duplicate uri, versioned, and unknown query fields are
rejected before storage access. Release writes and exact release listings
remain authenticated by the product publisher contract above and do not
require the generic object credential first.
Before any platform artifact is written, every publisher create-only claims
stado://releases/<product>/<version>/source-revision.json. That version record
is the single arbitration point across changing platform sets; platform claim
records are written only after it agrees. Older platform-only coordinates are
backfilled only when every existing platform claim names the same full source
commit. Release workers set both WISENT_SOURCE_COMMIT and
STADO_SOURCE_REVISION from that verified request, and the Stado build rejects
missing-shape or mismatched overrides.
The object API recovery helper treats a release target's legacy launchd plist
and label as an optional pair. A healthy handoff or a host with no exact orphan
does not need that pair. Recovery validates and bootstraps it only when stopping
an exact dead release-proxy orphan; without a declared pair, that branch refuses
before sending TERM because it has no known service to restore.
Only the pair on the release target authorizes that restore; a similarly named
entry under targets[].services is never inferred as a fallback.
Wait for the stable release endpoint
A blue-green candidate is not active merely because its own port answers.
Stado also checks the stable proxy's process, immutable release identity,
generation, upstream port, and declared readiness endpoint before routing is
reported as complete. Activation, reconciliation, and rollback allow that
endpoint to become ready within the product's
strategy.readiness_timeout_seconds; they do not assume a newly spawned proxy
has bound its socket after a fixed delay.
An exited proxy fails immediately. When the deadline expires, the refusal names the stable URL, allowed seconds, and last connection error or HTTP status. Identity and generation mismatches still fail without waiting.
Read the proxy's own output separately from the candidate's output:
stado release logs brama --target charless-mac-mini --version proxy --stream both --json
stado release logs brama --target charless-mac-mini --version 0.2.69 --stream both --json
stado release quarantine list brama --target charless-mac-mini --json
After repairing the recorded cause, release quarantine clear retires only the
exact digest named by --digest and records the required --reason. It does not
rebuild or replace the signed release.
Resolve the executable that is actually active
stado release active-binary skarbiec --json
release active-binary resolves the local registry target unless --target
names that same host. It reads observed rollout state, not policy.desired: a
newer quarantined candidate cannot displace the healthy predecessor that still
serves the stable bind. The command succeeds only when one live release process
matches the recorded PID, version, candidate port, and immutable release
directory; the exact Stado proxy executable and argument vector target that
port at the recorded generation; and the installed marker, qualification,
artifact digest, manifest digest, platform, directory, and executable all
agree. The executable must be a regular, non-symlink, executable file.
The policy-derived path uses the same canonical helper as the release agent:
{home} expands to the target's declared home, and a relative install root is
resolved beneath that home before the immutable release directory is appended.
JSON output includes state, product, target, version, platform,
artifact_sha256, manifest_sha256, and the absolute path; human output is
the path alone for direct command substitution. If release control declares
the product and target but no release is observably active, the command fails
instead of falling back to an old file. Stado's authenticator-seed freshness
reader and Weles use this same contract for SKARBIEC_BIN, so they cannot
select desired, quarantined, or merely present Skarbiec bytes independently.
Create and run a recipe
Recipes start disabled. This prevents the coordinator from polling an incomplete or newly reviewed definition.
stado builds add \
--name weles-native \
--repo https://github.com/wisent-ai/weles.git \
--branch main \
--command 'cargo build --locked --release' \
--artifact target/release/weles \
--platform darwin-arm64 \
--platform linux-amd64
stado builds enable weles-native
--artifact and --platform are repeatable. --interval-seconds changes the default 300-second polling interval. --auto-declare records the version from an exact semantic-version Git tag on hosts of the matching platform; it still does not promote a release.
To enqueue all platform jobs immediately instead of waiting for the next branch change:
stado builds run weles-native --run-id operator-ticket-1234 --json
The caller retains --run-id: retrying the same token after a crash recovers the same per-platform durable manifests instead of creating more jobs. A distinct intentional run needs a distinct token. The command returns one job ID per platform. The worker clones the repository at the recorded ref, runs the recipe command inside that checkout, and uploads only the declared artifact paths to the normal Stado results store.
Inspect and change recipes
stado builds list --json
stado builds status weles-native --json
stado builds edit weles-native --interval-seconds 60
stado builds disable weles-native
stado builds remove weles-native
status reports the recipe, its last observed Git ref, each platform run, and the current Stado job state. edit changes only the supplied fields. Supplying --artifact or --platform replaces that complete list. Changing the repository or branch clears the previous ref and recorded runs. Disabling a recipe stops new polling; it does not cancel jobs already submitted.
From Stado 0.16.39, completed build states remain readable after the
coordinator removes their queue records. Both the coordinator and
builds status read the terminal outcome kept in the run history.
Failed or cancelled jobs retain the worker's recorded reason.
Missing or inconsistent run history is reported as an error; remaining
artifact files are not used to invent a successful outcome.
Download a completed job's artifacts with the regular results command:
stado results <job-id> ./build-results
Terminal cleanup removes transient queue and status metadata, but keeps
the canonical artifacts and command logs under status/<job-id>/output/.
Repeated coordinator passes therefore leave these downloads available.
This does not recreate files that an older release already deleted,
and deliberately removed run history is not recreated.
A recipe accepts only an https:// clone URL. Credentials do not belong in the URL or command. Build-time secrets use Stado workload secret references and host-local Skarbiec grants, so values do not enter the recipe, process arguments, or registry.
Failure boundaries
| State | Meaning |
|---|---|
| no run for a platform | no job has been submitted for that platform |
| queued | no matching worker has claimed the job yet |
| running | a matching worker claimed the job |
| failed | clone, command execution, or artifact upload failed |
| succeeded | every declared artifact was uploaded and is readable from Stado results |
A successful process exit without every declared artifact is a failed build. A host merely declaring a platform is insufficient: a live worker must publish capacity for that platform.
The quality gate names whose revision refused
.wisent-release.json declares platforms.<platform>.quality: the argv the
release worker executes before it builds. The pull-request gate reads that same
key and runs the same argv, so a pull request is judged by the gate the release
stands behind, with one declaration used twice.
The gate program lives in two places, and neither is a second declaration.
.github/workflows/version-check.yml carries it inline and writes it to
$RUNNER_TEMP/stado-quality-gate.sh, and that is the copy CI executes: a job
that ran a script out of the tree it is judging could be disarmed by the pull
request it is judging. stado-rs/tests/release_quality_gate.sh is the same
program for a person, and stado-rs/tests/quality_gate/main.rs is the test
that drives it. Both read platforms.<platform>.quality from the manifest, so
the argv is declared once however many copies of the runner exist. The
repository has no scripts/ directory: a checked-in third copy of a gate the
job does not run is a program nothing executes and a place for it to drift.
A pull request is judged on its merge result, so a step that already
refuses the base branch refuses every pull request opened against it — for
files the author never touched. On a refusal the script therefore re-runs that
one step against the base revision in its own git worktree and reports:
| Verdict | Meaning | Who repairs it |
|---|---|---|
verdict=introduced |
the step passes on the base and refuses this revision | this change |
verdict=inherited |
the step already refuses the base, at the printed sha | the base, in its own change |
verdict=unattributed |
the base could not be checked out, or a clean tree is being compared against its own commit, so ownership is unknown | read the printed step and decide |
The base is the pull request's base sha, and on a push to main the commit
before that push. Comparing main against origin/main compares the revision
under judgement against itself, which would answer inherited for every
failure and never name the push that caused it; the script refuses that as
evidence and says so.
Running it by hand on a checkout with an uncommitted break still attributes:
the base sha equals HEAD, but the base's own worktree does not carry the
break, so the comparison is real and the answer is introduced.
Every verdict still fails the gate: an inherited failure is a failure, and the release would hit it with a version already spent. What changes is that the message no longer sends the wrong author to reformat somebody else's code.
Run it by hand with the checked-in copy:
$ bash stado-rs/tests/release_quality_gate.sh --base origin/main
--manifest reads a different manifest and --help prints this contract. An
absent platform, an absent quality key or an empty one is a refusal, not a
pass with zero steps.
This exists because on 2026-09-04 and 2026-09-05 main carried unformatted
cli/onboarding.rs, then cli/identity.rs — a file since removed by the split
that made it cli/identity/mod.rs — then dashboard/mod.rs. Each one turned
every open pull request red, the gate said "fix the tree" to authors who had
touched none of those files, and three of them reformatted another revision's
code to get their own work through.
Verify every supported platform
The release platform matrix runs both real journeys on the fleet's macOS ARM64 and Linux AMD64 workers. It checks out one exact public commit on each host, uses a real host-local Skarbiec binary for the isolated signing grant, then runs the native-build and complete release journeys.
For an online host reachable through Stado's managed host channel:
stado host verify-release-platform charless-mac-mini \
--repo https://github.com/wisent-ai/stado.git \
--ref <full-lowercase-commit> \
--json
The command accepts only a public HTTPS repository and a full 40-character lowercase commit. Source is cloned into the host's managed ~/.stado/work area and removed when the run ends. A platform passes only when the build artifact is downloaded and verified and the signed release is published, installed, and executed on that same platform.
Probierz owns the combined platform-matrix journey in stado-rs/tests/platform-matrix/. It runs macOS through the managed host channel and submits Linux to the pinned local-ubuntu-server worker through the normal Stado queue, so an inbound SSH port is not a requirement. The Linux worker verifies the digest of the published Skarbiec binary before using it and keeps Cargo output inside that job's .wisent-output tree, covered by terminal-job cleanup. The journey runs the platforms one after another because both release checks use the same canonical test product and version.
The matrix also cancels a release build and proves that its replacement uses a
different job, then publishes, installs, and executes the product on each
platform. Linux submissions use a stable Probierz-derived run ID, and Probierz
retains the submission identity and complete terminal job report with its logs.
Both paths use the published, digest-pinned Skarbiec 0.1.3 binary rather than
rebuilding a moving dependency branch, and keep temporary files in managed work.
Disposable qualification builds omit debug symbols and incremental caches;
runtime checks and disk admission thresholds remain unchanged. The Linux
journey removes its own Cargo output on success or failure and keeps the
downloaded signing tool in ignored work files, so the recorded source stays
clean.
For disk pressure, stado host disk TARGET --json includes Linux inventory
under the managed home, /home, /mnt, /var, and /opt.
stado host reclaim TARGET --apply --reason TEXT also recognizes the former
~/.stado/work/platform-matrix-cargo-target cache. It refuses linked,
unrecognizable, younger-than-one-hour, or live-held trees; unrelated untagged
directories remain outside that cleanup. host build-caches reports
no-cache-tags, root-protected, or scan-failed rather than making those
cases look like a successful empty scan.
Evidence
The real build journey lives in stado-rs/tests/builds/. It writes a recipe through the compiled CLI, lets the coordinator observe the public Stado repository, lets a real platform-matching worker claim the job, downloads build-output.txt, and verifies its bytes. Probierz registers this as the native-build journey and retains the source-bound report.
The full release journey lives in stado-rs/tests/ci-cd/. It builds committed source on a real worker, reads the signing key through an isolated real Skarbiec grant, publishes and installs the candidate, and executes the installed binary. Probierz registers this separately as release-pipeline, because a native build is not release evidence.
Source: this website