On this page

Walkthrough: release end-to-end

How does a committed source tree become the exact bytes every fleet host runs, and how do you take it back? This page walks one release from source to fleet and back as commands and their readings. The strict manifest and catalog contract lives in release; what a release is lives in primitives/release; the operational entry points live in operations.

Catalog first

Stado owns product release policy independently of repository hosting:

stado release catalog sync --catalog /path/to/release-catalog.json
stado release catalog audit

Sync imports the fleet's reviewed central catalog — including explicit releases:false manifests — refuses missing or duplicate product names, and CAS-updates stado://system/release-catalog/<product>.json; --root ROOT bootstraps a catalog from local registered checkouts. Audit reads only Stado catalog objects and refuses malformed, duplicate, or silent catalogs; it does not enumerate a Git forge or require forge tokens.

Add an application that has no manifest yet

An application checkout without a .wisent-release.json joins the pipeline with one command, which reads what the checkout's own project states instead of a hand-copied manifest:

stado release catalog adopt /path/to/app-ios --kind ios-xcode
stado release catalog adopt /path/to/app-ios --kind ios-xcode --apply \
  --owner <vault-owner-host> --client <submitting-host>

--kind ios-xcode reads the one *.xcodeproj at the checkout root. The project name is the default scheme (--scheme overrides it). The bundle identifier is the shortest one that is not a test bundle. It also reads the DEVELOPMENT_TEAM and the MARKETING_VERSION, which must be two or three numbers. The product name defaults to the folder name (--product overrides it).

Without --apply the command prints the four files it would write and changes nothing:

  • .wisent-release.json, with the version read from the project file;
  • release/build.sh, which archives, signs with the declared distribution certificate and provisioning profile, and exports the IPA;
  • a quality script under release/ in the new repository, which lints the project file and compiles for the simulator without signing;
  • release/archive-tree.py.

With --apply it writes those files and registers the product exactly as catalog sync --root registers a checkout. When --owner and --client are given, it first declares the publisher exactly as catalog declare-publisher does.

Refusals, each naming its cause:

  • the path is not a git checkout;
  • it already declares a .wisent-release.json (use catalog sync --root);
  • there is no Xcode project at its root, or there are several;
  • the project states no application bundle, team or readable marketing version;
  • a file it would write already exists.

The files must then be committed and pushed. A release builds a pushed commit, and it signs only once the declared signing credential holds the provisioning profile.

The whole workspace at once

One command releases every product that is due:

stado release newest --plan
stado release newest

newest reads the workspace — the directory holding one checkout per repository, which is the parent of the checkout it is run in unless --root names another — and for every product checkout it finds it reads the commit that checkout stands on and the version that commit declares. Nothing here decides a version: the rule that says which slot advances lives once for the whole fleet in AutoVersion, and its number is already committed in the product's own version_source by the time a release is cut.

Each product then reads as exactly one of six things, and the listing shows all six rather than hiding the ones it will not act on:

standing what it means
releasable no run published the declared version, no other commit holds it, and no run is releasing this commit; newest submits it
published a run published that version, named in the listing
version_taken a run of another commit, named with that commit as taken_by, holds the version; commit a new version before releasing this one
in_flight a run is releasing this very commit and has not ended; follow it with stado release status
declares_no_releases the manifest says releases: false, with its reason
unreadable the checkout could not be read — an uncommitted change, a malformed manifest — with the refusal

A submission attests its commit as the version's source for good, whether the run publishes or not, and the store refuses a second commit under the same version. A fix for a failed run therefore needs a new version; only the same commit whose run failed or was superseded can be submitted again at the same version. version_taken means the version belongs to another commit, not that publication succeeded.

The standings are read from every recorded run object (runs/release-pipeline/<run>/run.json). A run object that is not JSON, or that names no product, version, run_id or source_commit, refuses the whole listing with its path — release run object <path> names no source_commit, so the release it records cannot be told — rather than reading its version as never run or as taken by an empty commit.

--plan stops there and submits nothing. Without it every releasable product goes through the pipeline below, one product's refusal does not stop the rest, and the command exits nonzero naming every product that refused. --product NAME narrows the run to one product and refuses a name the workspace does not hold; --channel is the channel of every submission.

Stado Desktop runs the same two operations from the Releases screen: "Read what every product would release and the delivery queue of repaired commits" and "Deliver the queue of repaired commits, then release every product from the commit and version it declares".

The queue of repaired commits

stado serve --release-loop-root ~/src
stado release newest --plan

Every commit that carries a repair recorded in Oko is queued for its release with its defects named, put through its product's format gates and lock check, versioned, released through the steps below and recorded in Oko: The queue of repaired commits follows a commit through each step and every outcome the queue keeps.

Submit

stado release submit --source /path/to/product --version <exact-version> \
  --channel candidate

Submit requires a clean committed tree and never contacts a Git remote. It verifies --version against the product's checked-in version source, archives the exact committed tree, publishes the create-only source object at stado://sources/<product>/<sha256>/source.tar.gz, records source and manifest identity in the catalog, records the build of that commit, and the run queues one provider-neutral job per platform, owned by the build and pinned to a registry builder whose release_platform matches the recipe.

A build made beforehand — stado build submit, or stado build newest for the whole workspace — is released without compiling again:

stado build status <build-id>
stado release submit --build <build-id> --channel candidate

The release is refused while the build is waiting or has failed; the build's own commands and states are in builds.

Run identity is derived from product, version, channel, source digest, and manifest digest, so repeating the same submit resumes the run instead of starting another. The durable run record at stado://<queue-namespace>/runs/release-pipeline/<id>/run.json shows job IDs, output coordinates, delivery state, and failure. A terminal successful platform output is read from JobStorage and published, never rebuilt.

Qualification, signing, publication

Builders receive no repository coordinate or repository token: they materialize only the exact source URI and declared immutable inputs, run the manifest's quality argv in order and one build argv, and write status/<job>/output/{receipt.json,release.tar.gz} through the queue's canonical job-output collection.

Publication under the immutable coordinate stado://releases/<product>/<version>/<platform>/ is ordered:

release.tar.gz -> qualification.json -> release.sig -> release.json

The signed manifest (release.json) is written last and is the commit marker: required delivery jobs run only after it exists and consume its exact URI and digest. The signing key stays in Skarbiec — only its configured credential reference and the trusted key ID are configuration, never a secret value in a manifest or command line. Optional mirrors (a Git forge among them) do not gate canonical success.

Promote

stado release promote <product> <version>

Promotion changes references, never bytes: it re-fetches every platform, verifies the exact bytes, signature, and passed qualification, then compare-and-swaps one desired registry.release_control generation. It never rebuilds.

Hosts converge

The per-host declaration is targets[].managed_versions — the exact semantic version each stado-managed binary must run on that host, written by the operator:

stado release version declare --host <target> --binary <name> --version <X.Y.Z>
stado release version show --host <target>

version show compares each declared version against what the host actually runs. Verdicts are in-sync, host-behind, host-ahead, host-missing, and unknown for a binary whose installed version could not be read; unknown is never folded into another verdict, so an uninstalled reporting helper cannot masquerade as drift. version show exits non-zero on drift alone; stado release version converge --host <target> delivers the declared version of every binary behind its declaration, re-reads the installed versions afterwards, and exits non-zero unless every binary in scope is confirmed in-sync. Neither writes the registry: the declared version is the operator's statement of intent, and a converge that edited the document to match the host would turn a drift report into a rubber stamp.

Runtime products on registry hosts also converge without an operator: stado release agent reconciles desired releases on its exact registry target from the release_control desired generation.

Verify the rollout

stado release status <product>

status joins desired state with each host's observed rollout state and the host's own software report, and exits non-zero for a host that has never reported, reports stale, or runs unmanaged bytes — silence is a failure, never a pass. The rollout is done when deployment.json exists: it is written only after every declared target reports the promoted version, artifact digest, and manifest digest exactly.

Back out

stado release rollback <product>

Rollback atomically restores the previous desired release — the exact previously recorded coordinate, never a rebuild in place.

A release that is already serving is not rolled back over one refused readiness read. Each pass of the agent reads the live release once; while its process runs and the readiness window its release policy declares, counted from the first refusal, is still open, the pass leaves everything as it is and a later pass reads it again. A process that has already exited is lost at once — there is nothing to read again. The recorded reason carries the read's own sentence, so the quarantine entry still says which failure it was:

active release lost readiness: <declared-readyz-url> <refusal>, still after
the <N>s readiness window

If one host keeps refusing a version it was already given, check quarantine. The release agent quarantines a digest that failed to become ready and never retries it on its own — correct, since a candidate that dies in ninety seconds must not respawn in a loop:

stado release quarantine list <product> --target <target>
stado release quarantine clear <product> --target <target> \
  --digest <digest> --reason "<why this digest gets another chance>"

clear starts nothing, restarts nothing, and kills nothing: it removes one map entry, and the agent's next tick finds the desired digest no longer quarantined and rolls it out on its own — the same path it would have taken had the digest never failed. --target and --reason are never inferred or defaulted; the reason is recorded in the audit trail beside the host's rollout state.

Symptom-first triage for a release that did not land lives in the runbook; flag-by-flag detail lives in cli.

Source: this website