On this page
Creating a product and adding surfaces
stado product create provisions the private repositories of a new preview product through Wisent Integrations and registers it in the catalog. stado product surface add provisions one more repository for a product the catalog already holds and records it as that product's surface. Both run the same checks in the same order, and both have a plan that runs every check with reads only and prints what would be created.
The creation request
A creation request is one JSON file:
{
"schema_version": 1,
"request_id": "example-create",
"initiative_id": "task-example",
"product": {
"id": "example",
"name": "Example",
"description": "What the product does, in at most 350 UTF-8 bytes.",
"family": "wisent",
"visibility": "private",
"star_repository": "example-org/example-public"
},
"repositories": [
{ "surface": "cli", "repository": "example-org/example" },
{ "surface": "desktop", "repository": "example-org/example-desktop" },
{ "surface": "ios", "repository": "example-org/example-ios" }
],
"evidence_refs": ["where the product was asked for"]
}
surface is one of cli, desktop, web, docs, service, ios and android, the catalog's word for that surface; a product has one surface of each kind, so a documentation site a product keeps in its own repository beside its web application is its docs surface. The first repository becomes the product's owner repository. Every repository is created private and receives the product's description, which is why the description is held to GitHub's 350-byte limit. product.star_repository is the public OWNER/NAME the product's GitHub star request links to; the catalog requires it of every product because a private repository answers strangers with a 404, and it is written into the record as star_repository.
A request is refused, naming the field, before anything else is read:
- not JSON:
the creation request is not JSON: <parser error>; - a field the request does not hold or one it lacks:
product.owner: unknown field; product holds id, name, description, family, visibility, star_repository,initiative_id: missing; the request holds schema_version, request_id, initiative_id, product, repositories, evidence_refs; - a value of the wrong kind:
product.family: unknown variant `dating`, expected `wisent` or `standalone`,repositories[2].surface: unknown variant `ipad`, expected one of `cli`, `desktop`, `web`, `docs`, `service`, `ios`, `android`; schema_version: <n> is not supported; this Stado reads creation requests of schema 1;- an identity (
request_id,initiative_id,product.id, each part of a repository or ofproduct.star_repository) that is not lowercase letters, digits and inner hyphens starting with a letter, at most 100 bytes:request_id: "<value>" is not a creation identity: …; aproduct.star_repositorythat is not one:product.star_repository: "<value>" is not OWNER/NAME; product.visibility: creation requires private; public publication is a separate grant;product.name: empty; a product needs its display name,product.description: empty; a product needs what it does in one sentence,product.description: <n> UTF-8 bytes; a repository description holds at most 350;evidence_refs: empty; creation requires at least one evidence reference,evidence_refs[<i>]: empty; every evidence reference names its source;repositories: empty; creation requires explicit repository surfaces,repositories[0].repository: "example-org" is not OWNER/NAME;repositories[3].repository: example-org/example is already repositories[0]; each repository is provisioned once,repositories[3].surface: cli is already repositories[0]; a product has one surface of each kind.
Plan a request
stado product create --plan request.json
stado product create --plan request.json --json
--plan needs no --allow-create and writes nothing: no repository, checkout, journal entry or catalog line. It reads the creation journal without taking its lock, the catalog, GitHub with gh api repos/OWNER/NAME (a read), each checkout path, and the integration grant without presenting it, and builds the catalog record registry add would write, validated with the whole catalog in memory. It prints each repository with its surface, what GitHub answered and where its checkout would be, the description, and the record:
Plan for create request example-create. Nothing was created.
Product example (Example), family wisent, visibility private, catalog status preview.
cli example-org/example free on GitHub, would be created private; checkout would be created at <workspace>/example
desktop example-org/example-desktop free on GitHub, would be created private; checkout would be created at <workspace>/example-desktop
Repository description: What the product does, in at most 350 UTF-8 bytes.
Catalog <workspace>/stado/catalog/products.yml: would register example (owner repository example-org/example) with surfaces cli=example-org/example, desktop=example-org/example-desktop.
Wisent Integrations at <origin> would provision the repositories (not contacted).
It ends 0 when create would go ahead, and 1 with Error: create would stop before provisioning: <reason> when it would stop on something outside the request, such as STADO_PRODUCT_INTEGRATION_URL must name an HTTPS integration origin or STADO_PRODUCT_INTEGRATION_TOKEN_FILE must name the owner-only caller grant. A request already carried out to the end says it is already provisioned, so create would create nothing. --json prints the plan as {operation, request_id, state, product, repositories, registration, integration}.
Create
stado product create --request request.json --allow-create
stado product create --status example-create
stado product create --resume example-create --allow-create
Request content cannot grant creation: without --allow-create the request is refused with authority_required: --allow-create is required; request content cannot grant repository creation; `--plan` shows what the request would create without it. Each request is kept in the creation journal (~/.local/state/stado/product, or STADO_PRODUCT_STATE_DIR) for --status and --resume; a request id sent again with another payload is refused with request_id_conflict.
A new product is checked against what exists
Before the first repository is provisioned, and in the plan, the request is held against every repository the catalog claims (each product's owner repository and surfaces) and against what GitHub serves for each name. It is refused, and nothing is provisioned, when:
- the product exists and no request of this id registered it:
product.id: product <id> already exists in the catalog and no creation request <request> registered it; add a surface to it with `stado product surface add` instead; - the product id is the name of a repository the catalog claims:
product.id: product <id> is not new: repository <owner/name> already exists and catalog product <product> claims it; extend that product with `stado product surface add`, or register the existing repository with `stado product registry add`, instead of creating it again, or one GitHub serves:product.id: product <id> is not new: GitHub serves <owner/id> as <owner/name> and no catalog product claims it yet (`stado product catalog --unclaimed <owner>` lists it); …; - a repository belongs to a catalog product:
repositories[<i>].repository: repository <owner/name> already belongs to catalog product <product>; creation provisions new repositories only, so name one that does not exist yet; - a repository exists on GitHub and no product claims it:
repositories[<i>].repository: repository <owner/name> already exists on GitHub and no catalog product claims it (`stado product catalog --unclaimed <owner>` lists it); record it with `stado product registry add` for a new product or `stado product registry set PRODUCT --add-surface KIND=<owner/name>` for an existing one instead of creating it again; - a repository is the old name of a renamed one:
repositories[<i>].repository: repository <owner/name> is the old name of <owner/new>, which GitHub redirects to it; creating <owner/name> would break that redirect, so choose another name; - its checkout path is taken:
canonical checkout already exists and was not created by this request: <path>.
A GitHub answer other than a repository or 404 is refused with `gh api repos/<owner/name>` failed (<status>): <gh's error>; GitHub could not say whether <owner/name> exists. A repository the same request provisioned on an earlier run is its own, so --resume is not refused by it.
Add a surface to a product
stado product surface add example --kind ios --repository example-org/example-ios --plan
stado product surface add example --kind ios --repository example-org/example-ios --allow-create
surface add provisions the repository the way create provisions each one: the same checks of what exists, the same Wisent Integrations call with the product's catalog description, the same canonical checkout and journal, under the identity surface-<product>-<kind>. It then records the surface with stado product registry set PRODUCT --add-surface KIND=OWNER/NAME, reads the catalog back and prints the repository, the checkout and the product's record. Its checks read the journal without its lock; the journal is opened for writing only when a repository is to be provisioned. A run that stopped is finished by running the same command again; once the surface is recorded it answers "state": "provisioned" and does nothing. A surface the catalog already declares with the same repository, recorded by create or by hand, answers "state": "declared" with product <id> already declares the <kind> surface <owner/name>; nothing was added, and writes nothing. --plan runs every check with reads only and prints the plan as create --plan does (the catalog already declares this surface, so surface add would create nothing in that case), ending 1 with Error: surface add would stop before provisioning: <reason> when the integration grant is missing.
Refusals:
- neither
--plannor--allow-create:authority_required: --allow-create is required to provision a repository; `--plan` shows what would be created without it; both together are a usage error (exit 2); - an unknown kind is a usage error (exit 2):
invalid value 'ipad' for '--kind <kind>': unknown variant `ipad`, expected one of `cli`, `desktop`, `web`, `docs`, `service`, `ios`, `android`; --repository: "<value>" is not OWNER/NAME;PRODUCT: no catalogued product <id> in <catalog>; `stado product create` provisions a new product;--kind: product <id> already declares the <kind> surface <owner/name>; a product has one surface of each kind, so remove it first with `stado product registry set <id> --remove-surface <kind>` if <owner/new> replaces it;PRODUCT: <id>'s catalog description is <n> UTF-8 bytes and a repository description holds at most 350; shorten it with `stado product registry set <id> --description …`;--repository: the <kind> surface of <id> was started as <owner/name>; run that request again to finish it, or remove what it made before asking for <owner/new>;- the checks of what exists above, naming
--repository.
Removing a surface from the catalog is stado product registry set PRODUCT --remove-surface KIND; it leaves the repository on GitHub. stado product surface names these repository surfaces only; the Swift package API check is stado product swift api.
Desktop
Stado Desktop's Products screen carries Show what a creation request would provision (creates nothing) (create --plan), beside provisioning, status and resume, and Show what adding a repository surface to a product would provision (creates nothing) and Provision a private repository and add it to a product as a surface (surface add --plan and surface add with --allow-create as an explicit checkbox). Each runs on the selected Stado API host and shows the plan, the result or the refusal as the CLI prints it.
Source: this website