Skip to content

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.

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 runs scripts/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_windows and submodule_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_linux and binary_windows jobs, publishing tcab;
  • the gg_amd64 and gg_arm64 jobs with the gg version gate, then gg_publish, which uploads gg’s release objects;
  • the mirror job, 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.

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:

ArtifactContents
tcab-linuxtcab for Linux x86_64
tcab-windowstcab.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.

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:

ObjectContents
v<version>/gg-x86_64-unknown-linux-muslThe static-musl gg for x86_64
v<version>/gg-aarch64-unknown-linux-muslThe static-musl gg for aarch64
v<version>/gg-reference.tar.gzgg’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.

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.

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.

SiteProjectAddressBuilt by
Gallery (apps/site)test-cabinet-sitetestcabinet.ai (apex)Cloudflare (git-connected)
Docs (apps/docs)test-cabinet-docsdocs.testcabinet.aithe Azure pipeline’s docs stage → wrangler (scripts/ci/deploy-docs.sh)
Per-run playable buildstest-cabinet-runsa per-run *.pages.dev URLtcab publish → wrangler
Reference implementationstest-cabinet-referencesa per-variant *.pages.dev URLtcab 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.

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. The build:site root 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 when ui gains another workspace runtime dependency.
  • Build output directory: apps/site/dist.
  • Set TCAB_SNAPSHOT_URL as a build environment variable to the snapshot bucket’s public read base URL, the same value the backend advertises as TCAB_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.ai as 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.

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 to master, and test-cabinet-docs-staging, with its production branch set to staging. The script passes the branch to wrangler as --branch, so each upload is its project’s production deployment.
  • Add docs.testcabinet.ai as a custom domain on test-cabinet-docs, with a docs.testcabinet.ai CNAME pointing at test-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_TOKEN and CLOUDFLARE_ACCOUNT_ID.

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-references for production and test-cabinet-references-staging for staging. tcab publish-reference picks between them with its required --env flag, so a publish always names its target. Neither needs a custom domain: each variant is served from the *.pages.dev URL wrangler reports, under a per-variant branch alias (<slug>-<version-with-dots-as-dashes>-<variant>), and that URL is read back from wrangler rather than constructed.
  • Both reuse the same CLOUDFLARE_API_TOKEN (“Cloudflare Pages: Edit”) and CLOUDFLARE_ACCOUNT_ID as the docs deploy. They are the only secrets involved, since there is no backend push. After committing the updated lockfile, an operator runs scripts/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.