Submitting and resuming a release

How a product source enters the Stado-native release pipeline: its contract, its version, submission, the ownership chain, resuming a run, and the same operations in Stado Desktop.

Every product carries one strict .wisent-release.json v1 contract. A product that is not released declares only schema_version, product, releases:false, and a reason. A releasing product declares its version source, platform recipes, argv-array quality and build commands, stage mapping, promotion policy, immutable inputs, and post-publication deliveries. A runtime contract is required only for products reconciled onto registry hosts. Repository URLs, branches, hosts, buckets, tokens, signing-key bytes, and provider credentials are forbidden from that file.

The product version is selected by AutoVersion and passed explicitly. A release is made from a build: either the build of a checkout, made first, or a build that has already passed, named by its id:

stado release submit --source DIR --version V --channel candidate
stado release submit --build BUILD_ID --channel candidate

--build takes the id stado build submit or stado build status prints. It is refused while that build is waiting or has failed — build <id> is waiting, not passed: a release consumes only a passed build; \stado build status ` shows what its platforms did— and records no run.--sourcerecords the build of the named commit first, then the run;--commitand--versionbelong to--source`, and the two forms are exclusive. The build and its commands are described in builds.

Stado verifies V against the manifest's checked-in json, regex, or text version source. By default it requires a clean checkout and captures its full HEAD commit before reading the manifest, version and archive. It never contacts a Git remote.

To publish an exact committed source while other work is in progress:

stado release submit --source DIR --commit FULL_GIT_SHA --version V --channel candidate

--commit names a 40-character lowercase Git commit already present in DIR. It reads Git objects directly: no checkout, clone, worktree, stash or index change is performed. The manifest, version source and archive all belong to that commit, even if the current branch, staged edits, working files or untracked files differ. --version must match that committed source, not the current working file. Omitting --commit still refuses a dirty checkout with release source must be a clean committed Git tree. Malformed commit names are refused with --commit must be 40 lowercase hexadecimal characters; a tree or blob is refused with --commit must name a Git commit object. Missing objects and committed files retain Git's actual read error. These failures occur before any build is queued.

The ownership chain is:

committed tree
  -> deterministic source.tar.gz
  -> stado://sources/<product>/<sha256>/source.tar.gz
  -> build stado://<queue-namespace>/runs/build/<build id>/run.json
  -> fleet queue quality/build job per platform, owned by the build
  -> status/<job>/output/{receipt.json,release.tar.gz}
  -> release run stado://<queue-namespace>/runs/release-pipeline/<id>/run.json
  -> signed stado://releases/<product>/<version>/<platform>/
  -> registry.release_control desired CAS
  -> release_agent reconciliation
  -> exact-digest deployment receipt

The source object is create-only and carries metadata for its exact Git commit, source digest, and pipeline-manifest digest. The build record joins those identities to the platform jobs, their builders and what their receipts said; its inputs, manifest copy and per-platform requests are staged under runs/build/<product>/<build id>/. The durable release run names the build it consumes (build_id), mirrors that build's platform records, and adds the published coordinates, delivery jobs, state, and failure. Repeating submit with the same inputs resumes that run and does not rebuild a platform whose published output is already recorded; a build's identity carries no channel, so a stable release of the same commit consumes the same build as its candidate.

submit ends when the platform builds are queued: it prints the run id with state waiting and returns. The builds run on the fleet's builders, and the control host's release agent — the stado release agent on the host that owns the object store and the signing grant — finishes every run whose builds have ended: it verifies and signs the outputs, publishes them, queues the deliveries and records the result on the same run object. A build job has ended when any of these says so, read in this order by every part of the pipeline that asks (the agent's pass, publishing, a delivery's verdict, the build's platform refresh and release status): its terminal queue record; the receipt its worker wrote with its exit; the outcome the queue's run reaper retained in the job's run manifest (runs/<run>.json, found through runs/jobs/<job>) when it retired the record — a cancelled job, or one that failed before its worker wrote a receipt, has no other witness; or the job's transition record. A platform whose job ended that way reads failed with build job <job> ended cancelled (or ended failed) and the job's recorded error, a required one fails the run, and the agent finishes the run on that instead of waiting for a job that is over. A job the agent finds in none of those places and in no queue state is the refusal release job <job> has not reached a terminal state: no queue record, no receipt, no reaped run retains it and no transition record names how it ended; release status reports such a leg as lost. The agent never blocks on a builder or on a delivery host: a run whose builds are still running is left for the next tick, and a run whose deliveries are still queued or running on their hosts stays delivering until a later pass finds them ended and judges them. submit therefore returns at once instead of holding the operator's terminal for the length of every build. stado release status PRODUCT follows a run; stado release resume RUN_ID runs one pass by hand, synchronously, in the terminal that asks. status lists every recorded run, newest first; --limit N keeps only the newest N and --limit 0 is refused. --run and --version read however far back the match is, and a refusal for no match names the runs it read. A platform whose job failed carries the failure as the run recorded it, and status prints it whole under the platform line: the sentence naming the job and how it ended, then the step names and the last lines the worker wrote, which say which step failed and why (release/fmt.sh: line 21: exec: npx: not found). Nothing is cut to a first line; the same text is platforms.<platform>.failure in --json. Stado Desktop offers the same read as Read release runs and what each platform's job did, with the same fields. The real tests of a reaped platform job and of a failure printed whole are tests/build_status/, against declared runs of the real queue.

An unreadable or missing run object whose unfinished index marker remains does not stop the agent from finishing other runs in that pass. The agent logs the indexed run ID and the read or parse error (or that the object is absent), then reports the incomplete pass with the number of failed reads and the IDs it did finish. The faulty marker is not treated as a successful release or silently removed; stado release status PRODUCT --run RUN_ID --json remains the authoritative read of that run's persisted state.

When the checkout has changed, resume the recorded run instead of submitting the new checkout under an already published version:

stado release status PRODUCT --json
stado release resume RUN_ID --json

Use the full run_id from the status JSON. resume reads no local Git tree or local product manifest. It verifies the stored run identity, manifest coordinate and manifest digest, then uses the same build, publication and delivery processing as submit. Queued and running jobs keep their IDs; published platforms are verified rather than rebuilt. Failed or cancelled deliveries get an idempotent replacement keyed by their previous terminal job, so repeating an interrupted retry does not create a second replacement. A stale delivery summary is checked against the queue.

Malformed IDs are refused with run ID must be 32 lowercase hexadecimal characters. An unknown ID reports release run <id> does not exist; a changed stored manifest reports release run manifest digest mismatch. These refusals do not replace the recorded source. Other failures retain the actual operation's error in the same run and in command output.

Stado Desktop's Releases screen has Resume selected run. The review dialog shows the original source revision and exact command before execution; Release resume result retains stdout and stderr, including a refusal. The request and read-back use the selected Stado API endpoint through POST /api/operator/run, with the existing RUN_MUTATION confirmation; Desktop does not launch a local CLI for resumption. The native API's ordinary 300-second request bound applies. If that bound expires, the recorded run and jobs remain resumable with the same command. Resuming already published platforms needs their verification keys, not a private signing grant; a platform still requiring publication needs that grant.

Available operations… → Submit a release source on the same Releases screen sends the repository path, optional full commit, version and channel through the selected Stado API. The repository path is on that API host, not implicitly on the desktop computer. Leaving the commit blank requests the clean-HEAD behavior. The form displays the exact command and requires review before submission; its receipt retains the process result, stdout and stderr. The same API request bound applies, and the recorded run can be continued with Resume selected run.

Available operations… → Build a committed source on Releases uses stado build submit on the selected API host. A required platform without post-build tests is refused before its source or jobs are written; the receipt retains the platform, cause and awaiting_tests consequence in stderr. The release-source operation above applies the same enrollment refusal.

Source: this website