On this page
Web hosting: the release declaration
The .wisent-release.json shape for a web product
A web product declares one platform whose key is web. runner_platform
names the host that builds it. The quality gate and the build both call
stado web; the gate is declared "kind": "format", so the pre-push check
runs it as the product's formatting gate (its own format, type and lint
scripts), and the stage map names the one tarball the unit is installed
from.
{
"schema_version": 1,
"product": "preferences-landing",
"releases": true,
"version_source": { "kind": "json", "path": "package.json", "pointer": "/version" },
"platforms": {
"web": {
"runner_platform": "darwin-arm64",
"quality": [
{ "name": "web-quality", "kind": "format", "argv": ["stado", "web", "quality"] }
],
"build": { "argv": ["stado", "web", "build"] },
"stage": {
"dist/preferences-landing-web.tar.gz": "preferences-landing-web.tar.gz",
"dist/preferences-landing-web.tar.gz.sha256": "preferences-landing-web.tar.gz.sha256"
}
}
},
"promotion": { "channels": ["candidate", "stable"], "reconcile": false }
}
stado web quality reads the checkout the release worker prepared
(WISENT_SOURCE_DIR), installs the locked dependency tree, and runs the
product's own typecheck and lint scripts when it declares them. A project
with a tsconfig.json must declare typecheck: its build checks types, so a
gate without the check passed commits the build then refused. Such a project
is refused before the install with stado web quality: <product> has a tsconfig.json, so its build checks types, but its package.json declares no typecheck script for this gate to run first; declare "typecheck": "tsc --noEmit" in <package.json>. stado web build runs
the product's build script and stages a tarball holding .next, public,
package.json, the production node_modules, and a generated launcher that
executes the product's start script on the port the unit passes it. A product
whose build needs a credential names it in the platform's secret_env as
VAR: "role#field"; the vault decides which item plays the role.
A product whose web application is not at the repository root — a
frontend/ beside a backend — names that directory with --package on both
steps: stado web quality --package frontend and stado web build --package frontend. Its package.json, install, checks, build and staged tree are that
directory's, and --root is read relative to it; the artifact's product name
and source revision stay the repository's. A path that leaves the checkout,
or a directory the checkout does not have, is refused by name.
A dependency the package reaches outside its checkout is answered from a
release input the manifest declares under inputs, on both steps. A private
Git dependency fetched over SSH from GitHub takes --git-input INPUT=OWNER/REPOSITORY.git, where the input is a Git bundle: both GitHub
spellings of that repository resolve to the bundle through a Git config only
this build reads. A file: dependency on a sibling repository takes
--link-input PATH=INPUT[/SUBPATH]: PATH, relative to the package, becomes a
link to that directory of the input, for example --link-input ../echo-web/packages/onboarding-web=onboarding-web/packages/onboarding-web.
An input the manifest does not declare for this platform is refused by its
variable name, a link outside the release work area or over an existing file
is refused, and the link the quality step made is accepted again by the build.
Before npm ci, both steps read every private GitHub dependency the lockfile
pins over SSH. One a --git-input answers needs no credential: the step says
the declared release inputs answer every one and installs from the bundles.
One no input answers is fetched over HTTPS with a GITHUB_TOKEN the platform
declares under secret_env; with no token, the step is refused before npm
runs, naming each unanswered repository URL, with the two ways to answer it
(a Git bundle input, or the token). GitHub's Git origin is read from one
declaration in Stado, tooling/inputs/github.json.
tests/release/inputs/web-git-input.mjs runs stado web quality on a
product answered by its input, checks the installed file, and on a product
with one unanswered pin checks the refusal names only that pin and that
nothing was installed.
Publish that bundle from the canonical dependency checkout through Stado:
stado release catalog pin-input "$PRODUCT_CHECKOUT" \
--name "$INPUT_NAME" --source "$DEPENDENCY_CHECKOUT" \
--revision "$LOCKED_COMMIT" --git-bundle --json
The command exports the resolved commit and its reachable history, not other
branches or tags. The bundle advertises that exact commit as HEAD, which
npm requires even for a dependency pinned by commit. Stado creates temporary
bare Git metadata under the checkout's build directory and reads objects from
the canonical repository; it makes no clone or second checkout and changes no
source reference, index or file. Failed export or metadata cleanup refuses
publication and names the failed operation.
--git-bundle cannot be combined with --cargo or --path.
The object is published create-only under its SHA-256 before the product
manifest is changed. The saved input has extract: false and a
<input-name>.bundle mount. The JSON receipt reports source_commit, uri,
sha256, bytes and git_bundle. A failed upload leaves the manifest
unchanged; a failed manifest write reports its path. Configure the same
--git-input INPUT=OWNER/REPOSITORY.git argument on both web steps, and
republish when the lockfile selects another commit. Git bundle bytes may
differ between exports; consumers verify the recorded digest and commit.
Stado Desktop's Releases → Publish an immutable build input offers Export Git bundle for web dependencies on the selected API host. It uses the same operation, reports the same receipt and preserves its refusal. Neither a publication receipt nor a successful Git read qualifies the web application's build or graphical behavior.
tests/release/inputs/git-bundle-input.mjs runs real publication and readback against
an isolated local Stado object store, installs the pinned package with npm
offline, compares an installed file to that commit, and checks conflicting
options and missing source/revision refusals without changing the manifest
or source references:
STADO_BIN="$CANDIDATE_STADO" \
STADO_TEST_GIT_SOURCE="$DEPENDENCY_CHECKOUT" \
STADO_TEST_GIT_REVISION="$LOCKED_COMMIT" \
STADO_TEST_GIT_PACKAGE="$PACKAGE_NAME" \
STADO_TEST_GIT_PACKAGE_FILE="$SHIPPED_PACKAGE_FILE" \
node tests/release/inputs/git-bundle-input.mjs
Use a package whose installation needs no build or unavailable registry
dependency. The offline consumer uses Git's local-path rewrite: a file://
URL asks Git for a repository directory rather than a bundle. This tests
bundle consumption, not npm's separate GitHub archive-download optimization
or the full web gate. Reports under .build/git-bundle-input/run-*/ retain
source and binary identities, command output, npm logs, exit statuses and the
observed result. An installed Stado without --git-bundle, or one that exports
no HEAD, fails the journey; it is not a passing qualification of newer
source. Desktop observation and web build qualification remain separate runs.
A build-time value that is not a credential goes in the platform's env
instead, as a literal:
"env": { "NEXT_PUBLIC_SITE_URL": "https://content.wisent.ai" }
stado web build exports both maps into the install and the build. The split
is not cosmetic. secret_env names a Skarbiec item and field, so a public
constant kept there becomes a credential the fleet grants, syncs and audits
for nothing, and its value stops being visible in the diff that changed it.
echo-web's build refuses to run without NEXT_PUBLIC_SITE_URL, and that URL
is on the public internet. One variable in both maps is refused: it would have
two answers.
A static site is a web product too
Six of these sites are directories of files — byk-landing,
handtohuman-landing, kronika-landing and transcript-lake-landing are
index.html and a stylesheet at the top of the repository, jeden's site is
its web directory, and tama-landing builds one into dist/. Vercel served
them by copying the directory, and they are hosted here the same way rather
than being rewritten as Node servers.
One rule decides which a product is: a product that declares a start
script is a server, and a product that does not is a static site. Nothing
else is consulted — not next.config.*, not a dependency on next, not which
directories exist. start is what the launcher runs, so "is there a server to
start" and "what does this product declare" have to be one question. A product
with a build and no start is a site whose build writes its files; a product
with neither is a site whose files are committed, and it needs no
package.json at all.
The site root is what --root names in the build argv, and the checkout
root when nothing names one. There is deliberately no probe order over
public, site, dist and .: a probe order is a guess, and a build that
guesses stages a directory nobody declared.
"platforms": {
"web": {
"runner_platform": "darwin-arm64",
"quality": [{ "name": "web-quality", "kind": "format", "argv": ["stado", "web", "quality", "--root", "dist"] }],
"build": { "argv": ["stado", "web", "build", "--root", "dist"] },
"stage": {
"dist/tama-landing-web.tar.gz": "tama-landing-web.tar.gz",
"dist/tama-landing-web.tar.gz.sha256": "tama-landing-web.tar.gz.sha256",
"dist/SOURCE_REVISION": "SOURCE_REVISION"
}
}
}
The artifact holds the site under one fixed directory, site/, plus the same
bin/start-web launcher and a bin/serve-static.mjs server generated into the
tarball. That server uses only node:http, node:fs and node:path: a global
serve would be a dependency of the host rather than of the release, and
npx serve would fetch from the npm registry the first time a unit started —
a network call in the run path, resolving a version nobody pinned, on a host
whose whole point is that it runs the bytes someone published. It resolves a
request as an exact file, then <path>.html, then <path>/index.html, which
is the cleanUrls behaviour these sites were served with, answers 404.html
when the product ships one, refuses anything but GET and HEAD, and confines
every resolved path to the site root.
The launcher is the same script with one line different, so the unit's
contract does not change: the install root resolved from $0, PORT passed by
the unit and refused when unset, WEB_ENV_FILE sourced with assignments
exported, and the loopback address only. stado web deploy is unchanged — it
installs a tarball and runs bin/start-web, and it cannot tell the two kinds
apart. When the site root is the checkout root, .git, .github, .vercel,
.next, node_modules, release/ and any .env* file are left out: they are
not part of the site, and one of them is a credential.
A web platform beside a binary platform
A product can host a site and ship a binary from one repository. jeden is a
Rust CLI whose documentation site is its web directory, and its
.wisent-release.json declares a runtime contract — the binary, its
launcher and the schema versions the rollout state machine reads.
That contract is declared once for the product, and it applies to the
platforms it describes. A platform whose stage map contains both the
runtime's binary and its launcher ships the runtime and is held to the
contract; a platform whose stage map contains neither ships something else,
like a site tarball, and the contract says nothing about it. A platform with
exactly one of the two is still refused, because that is the half-staged case
the check exists for: a rollout would install something the host cannot
start. A runtime that no platform stages at all is refused too — it is a
declaration nothing checks against the world, and the first rollout would be
what discovered it.
stado release submit reads the same rule when it publishes: a platform that
ships no runtime publishes no binary path, no launcher and no schema
versions, rather than inheriting the product's and claiming a binary that
exists in none of its own bytes. Rollout is unaffected either way — a rollout
target names the platform it rolls out, in the product's rollout policy, so a
web platform is only ever rolled out if someone declared it there.
Source: this website