Releasing
This page covers releasing tcab and gg from a version
tag, and the one-time configuration behind the project’s deployed static sites.
For the whole vX.Y.Z sequence the tag sits inside, from preparing the release
on nightly and rehearsing it on staging to landing the catalog and the
services in production, see
Cutting a Release and its
quickstart. Standing the always-on services
up as staging or production environments is covered by
Deployment; running them on your own machine is covered
by Running; building locally is covered by
Building.
Release tags
Section titled “Release tags”A release is a vX.Y.Z tag on master in Azure Repos. The main pipeline does
not trigger on tags; pushing one runs the
release pipeline,
azure-pipelines-release.yml:
gated, which runsscripts/ci/require-gated-commit.sh. It finds a run of the main pipeline at the tagged commit, on any branch, and waits until that run’s check jobs have passed:rust,web, the four gg test partitions,rust_build,binary_linux,binary_windowsandsubmodule_pins. The image, publish, deploy and docs results never gate a release. When no run exists at that commit (the main pipeline batches pushes, so an intermediate commit may have none), it queues one on the tag ref, which runs the gates stage alone, and waits on that. It fails naming the check that failed, or after 200 minutes; a failed run is not queued again, so re-run it and re-run the release;- then the
binary_linuxandbinary_windowsjobs, publishingtcab; - the
gg_amd64andgg_arm64jobs with the gg version gate, thengg_publish, which uploads gg’s release objects; - the
mirrorjob, which pushes the tag to the GitHub mirror.
The services ship as the images the main pipeline deploys from master, and the
docs site from its docs stage, so a tag releases no service or site of its
own.
The tag names the version of gg, so crates/gg and crates/core are bumped
to it before tagging; see Releasing gg.
Releasing tcab
Section titled “Releasing tcab”The binary_linux and binary_windows jobs release-build tcab, run the suite
in the release profile, and smoke-test the produced binary with
scripts/ci/smoke-binary.sh, on every main-pipeline run. The release pipeline
runs the same jobs for the tag and publishes that smoke-tested binary as a
pipeline artifact:
| Artifact | Contents |
|---|---|
tcab-linux | tcab for Linux x86_64 |
tcab-windows | tcab.exe for Windows |
The tag’s release-pipeline run is where a release of tcab is downloaded from. Every
other platform builds tcab from source with cargo build --release -p test-cabinet-cli.
Releasing gg
Section titled “Releasing gg”gg is fetched by a running deployment into a run container
rather than downloaded by a person, so its release host is the Azure Blob
Storage container gg-releases in the storage account testcabinetartifacts.
The container allows anonymous blob read and no listing, so every object is
readable at a known URL under
https://testcabinetartifacts.blob.core.windows.net/gg-releases/. Each version
is laid out under its own prefix:
| Object | Contents |
|---|---|
v<version>/gg-x86_64-unknown-linux-musl | The static-musl gg for x86_64 |
v<version>/gg-aarch64-unknown-linux-musl | The static-musl gg for aarch64 |
v<version>/gg-reference.tar.gz | gg’s reference documents, identical on every arch |
The main pipeline’s gg_release stage uploads all three on every master
build, and the release pipeline on every v* tag. The gg_amd64 and gg_arm64
jobs build the binaries natively with scripts/ci/gg-dist.sh, which runs
scripts/build-gg-static.sh and then projects and packs the documents. Each job
publishes its binary and its tarball as one gg-<arch> artifact, which is what
the run-image self-check, the backend image and the driver image all consume, so
the release objects and every image’s copy come off one build. The gg_publish
job then runs
scripts/ci/publish-gg.sh under the tcab-gg-publish service connection, a
workload-identity-federated identity holding Storage Blob Data Contributor on
the account. The version prefix is what the x86_64 binary reports, and each
upload is read back without credentials.
A bare executable. The install inside a run container is a single curl of
<base>/v<version>/gg-<target> (core::gg_exec::release_asset_url), so the
object is uploaded under exactly that name with no packaging around it. The base
defaults to the container’s URL and TCAB_GG_RELEASE_URL overrides it. core,
running in the driver, picks the target from its own architecture, so an arm64
deployment asks for gg-aarch64-unknown-linux-musl and an amd64 one for the
x86_64 object; TCAB_GG_RELEASE_TARGET overrides it.
The crate version is the release version. gg --version is read out of the
run container and recorded as a run’s subject.harnessVersion, and core
derives the version it fetches from its own package version. Bump both
crates/gg and crates/core to the release version before tagging. A test pins
them to each other. On a tag, the release pipeline’s gg jobs fail when
gg --version differs from the tag with its v stripped, naming the two crates
to bump, and
publish-gg.sh runs the same check before it uploads anything.
A prerelease tag such as v0.7.0-rc1 equals no crate version, so nothing
resolves it by default. Point a deployment at one explicitly with
TCAB_GG_RELEASE_VERSION=0.7.0-rc1.
gg-reference.tar.gz
Section titled “gg-reference.tar.gz”The same upload publishes the reference documents tcab-backend serves at
GET /gg/reference and GET /gg/reference/{language}: index.json plus one
document per program language, projected by the freshly built gg itself
(gg reference --out). The backend reads them from the directory
TCAB_GG_REFERENCE names because it must not link test-cabinet-gg. The backend
image bakes the identical files at /opt/gg-reference and sets the variable
itself, out of the same artifact this upload takes them from.
Without them the backend starts, serves everything else, logs one warning at
boot, and answers 503 on the two reference endpoints, so the console’s gg
Reference section is the only thing that degrades.
It is a single object with no triple in its name, because the content is JSON
projected from data compiled into gg and is identical on every platform. Both
architectures project and pack it, so gg reference --out is gated on each and each
one’s backend image has a copy to bake; the upload takes the x86_64 one by name.
Static-site topology
Section titled “Static-site topology”The project deploys four static sites, all on Cloudflare Pages. Each is its own Pages project under its own domain; they differ in how they are built.
| Site | Project | Address | Built by |
|---|---|---|---|
Gallery (apps/site) | test-cabinet-site | testcabinet.ai (apex) | Cloudflare (git-connected) |
Docs (apps/docs) | test-cabinet-docs | docs.testcabinet.ai | the Azure pipeline’s docs stage → wrangler (scripts/ci/deploy-docs.sh) |
| Per-run playable builds | test-cabinet-runs | a per-run *.pages.dev URL | tcab publish → wrangler |
| Reference implementations | test-cabinet-references | a per-variant *.pages.dev URL | tcab publish-reference → wrangler |
The docs, per-run builds, and reference implementations are Direct Upload
projects, built elsewhere and pushed with wrangler. The gallery is
git-connected: Cloudflare clones the GitHub mirror and builds it itself. The
gallery is git-connected because it is the only site that must rebuild when
something other than a code push changes, namely the backend’s snapshot. A
git-connected project has a deploy hook, a unique URL that triggers a rebuild on
a bare POST, which is what the backend fires after it uploads a new snapshot (see
TCAB_SITE_DEPLOY_HOOK_URL).
Per-run builds are served from the root of their own pages.dev subdomain (see
Site Hosting and
Results). Serving each at a root rather than
a subpath keeps it playable exactly as the test case’s
build interface requires.
Gallery (Cloudflare Pages, one-time)
Section titled “Gallery (Cloudflare Pages, one-time)”Cloudflare clones the GitHub mirror and builds apps/site itself, on every push
to the production branch and whenever the deploy hook is fired. The pipeline’s
mirror job pushes each master commit that passed the gates to the mirror, so
a gallery code change reaches Cloudflare through the same pipeline that deploys
the services. The test-case and run data the gallery shows come from the
backend’s public R2 snapshot, fetched at build
time. Cloudflare’s git integration is the whole pipeline.
In the Cloudflare dashboard, create a Pages project named test-cabinet-site
connected to the GitHub mirror:
- Set the production branch to
master. - Build command:
npm ci && npm run build:site. Thebuild:siteroot script builds the site’s transitive workspace runtime packages in dependency order (build:packages) before the site itself. Keep that list in the root script rather than inline here, so it stays the single source of truth whenuigains another workspace runtime dependency. - Build output directory:
apps/site/dist. - Set
TCAB_SNAPSHOT_URLas a build environment variable to the snapshot bucket’s public read base URL, the same value the backend advertises asTCAB_SNAPSHOT_PUBLIC_URL. - The build is pure Node. Cloudflare’s build image has no Rust and needs none.
The model catalog is owned by the backend and baked into the public R2 snapshot
as
models.json, which the site consumes at build time, so model curation and refreshed rates reach the gallery through the next snapshot publish. - Add
testcabinet.aias a custom domain on the project, so the gallery is served from the apex. - Create the project’s deploy hook and give its URL to the backend as
TCAB_SITE_DEPLOY_HOOK_URL(see Deployment). The backend fires it after each snapshot upload, so a published run rebuilds the gallery without a code push.
Every other project is served from *.pages.dev or a subdomain, so no
*.testcabinet.ai wildcard or organization domain verification is required.
Each per-run build is deployed under its own Cloudflare Pages branch alias
(--branch=<run-id>), and the served URL is read back from wrangler’s output
rather than constructed, because Cloudflare sanitizes and truncates long
branch-alias subdomains.
Docs (Cloudflare Pages, one-time)
Section titled “Docs (Cloudflare Pages, one-time)”The developer docs (apps/docs) deploy to Cloudflare Pages at
docs.testcabinet.ai from the Azure pipeline’s docs stage, which runs
scripts/ci/deploy-docs.sh on every master and staging build that passed the
gates stage. The deploy target follows the branch: master
publishes to test-cabinet-docs and staging to test-cabinet-docs-staging.
It is a pure static build with no Rust step.
- Create Direct Upload Pages projects named
test-cabinet-docs, with its production branch set tomaster, andtest-cabinet-docs-staging, with its production branch set tostaging. The script passes the branch towrangleras--branch, so each upload is its project’s production deployment. - Add
docs.testcabinet.aias a custom domain ontest-cabinet-docs, with adocs.testcabinet.aiCNAME pointing attest-cabinet-docs.pages.dev. - Create a Cloudflare API token with the “Cloudflare Pages: Edit” permission and
note the account ID. Set both on the Azure pipeline as the secret variables
CLOUDFLARE_API_TOKENandCLOUDFLARE_ACCOUNT_ID.
Per-run builds (Cloudflare Pages)
Section titled “Per-run builds (Cloudflare Pages)”A publish deploys each run’s
static build to Cloudflare Pages under a per-run branch alias
(--branch=<run-id>), served at the *.pages.dev URL wrangler reports and
embedded by the gallery from there. This is the operator’s half of a publish, so
the operator holds the Cloudflare credentials it uses (see
CLI Authentication). Beyond those
credentials there is nothing to configure, and builds served from pages.dev
need no custom DNS.
Reference implementations (Cloudflare Pages, one-time)
Section titled “Reference implementations (Cloudflare Pages, one-time)”A reference implementation
is a test-case variant’s authored, correct static build, deployed out-of-band by
tcab publish-reference to its own Cloudflare Pages project. Its served URL is
written into a committed lockfile (test-cases/reference-builds.lock.json)
rather than pushed to the backend, because the backends are VPN-only; the backend
ingests it from its own checkout on the next
scripts/reingest-cluster.sh. The operator workflow,
prerequisites, and the release gate live in the
reference-implementation guide.
- Create two Direct Upload Pages projects:
test-cabinet-referencesfor production andtest-cabinet-references-stagingfor staging.tcab publish-referencepicks between them with its required--envflag, so a publish always names its target. Neither needs a custom domain: each variant is served from the*.pages.devURLwranglerreports, under a per-variant branch alias (<slug>-<version-with-dots-as-dashes>-<variant>), and that URL is read back fromwranglerrather than constructed. - Both reuse the same
CLOUDFLARE_API_TOKEN(“Cloudflare Pages: Edit”) andCLOUDFLARE_ACCOUNT_IDas the docs deploy. They are the only secrets involved, since there is no backend push. After committing the updated lockfile, an operator runsscripts/reingest-cluster.sh --env <env>from a VPN-connected machine.
The lockfile holds a URL per environment, keyed by environment first. Each
backend reads only its own environment’s entries, selected by its TCAB_ENV, so
one file serves both.