Agent docs

Deployment and environments

> Evidence: 3 file:line witnesses as of 2026-08-18.

Evidence: 3 file:line witnesses as of 2026-08-18. Confidence: partial, some rules witnessed, the rest inherited from the seams. What changes it: the first module built against this doc (../PLAN.md forge-1) and its instance breakdown.

How many environments a module has, what separates them, how a build gets to each, and how anyone can tell what is running. Deployment mechanics on GCP are already written well in versable-builder (docs/app-patterns/03-deploying-on-gcp.md: branch-to-environment mapping, /build-info, four traps) and in patterns/02-deploying-a-module.md. This doc carries the rules those procedures implement.

Audience: anyone provisioning a module or debugging why dev behaves like prod.

What the instances do#

versable-runnerspeedwaywalmart-mvpextractor
envsprod + dev, one projectprod (+ staging env file, deploy/envs/)sandbox + prod, one project, main previews to sandbox, release and deploy-prod-* tags to prodone, per the owner ("only understood one env")
identity per envshared: runner-service-dev runs as runner-service@one SAper stackone SA extraction-app@
provisioningdeploy.sh, idempotent, env-var parameterized (enable APIs, SA, bucket, queue, --max-concurrent-dispatches)scripts/release.sh and deploy/deploy.sh, "not interchangeable"deploy.sh + cloudbuild.deploy.yaml, one image four services (api/worker × prod/non-prod)39 revisions, pgbouncer sidecar, VPC connector, minScale 1; CI elsewhere
build identitynone/build-info (commit, branch, kit, built at)/api/build-info (commit)image tagged with a git SHA
CInone; deploys from a workstationCloud Build files presentCloud Build, tag-guardedunproven where

The rule#

Three environments minimum, each with its own identity, storage, queue, secrets, and issuer client; the environment is named in config and on every record; a deploy is verifiable without credentials.

Three environments#

local, dev, prod, and staging when a module needs a pre-prod soak. local is the in-process mode with zero cloud (13-local-dev-and-debugging.md) and it counts: a module that cannot run locally has two environments and a hope. The extractor's one-environment history is the owner's named pain point, and App V5's local/dev/prod is what callers already expect.

Separate identity, separate everything#

Per environment: a service account (or the platform equivalent), a bucket (or a prefix on a per-env bucket, but a bucket is cleaner for lifecycle rules), a queue, a secrets namespace, and a client row at the issuer with the env claim set. A credential from one env is rejected by another even when its signature is valid (02-identity-and-tenancy.md). Sharing a service account across dev and prod, as runner-service-dev does, means dev can read prod's bucket and neither can be audited apart. It is banned.

Ephemeral environments namespace the queue#

A PR preview or a branch deploy that shares a store with its base environment must not drain the base's queue or be drained by it. App V5's answer is a queue field on every task matched against Config.WORKER_QUEUE, set from the Render-injected git branch, so preview and base share one Mongo without stealing each other's work (lib/config/__init__.py:88-93). Same idea for Cloud Tasks: a queue per preview, created and deleted by the deploy. Zero manual config is the bar.

The environment is a value, not a vibe#

One typed config object (09-config-and-secrets.md) has an env field. The module reports it in /build-info and /health/deep, stamps it on every job, outcome, usage event, log line, and trace, and refuses to start if a config value contradicts it (a prod bucket name under env: dev). This is how "it works on my machine but not on prod" becomes a diff of two /health/deep responses instead of a week.

Provisioning is a script, idempotent, in the repo#

versable-runner's deploy.sh is the shape: enable APIs, create the service account, the bucket, the queue with its ceilings, deploy, then re-set the service URL the OIDC audience needs. Runs twice without harm, parameterized by environment, and it is the documentation of what the module needs to exist. Terraform is fine when the estate wants it; the rule is that provisioning is code in the module's repo, not a memory of clicks.

Branches map to environments#

walmart-mvp's mapping is adopted for modules with CI: main previews to dev, release (or a deploy-prod-* tag) ships to prod, and a tag can only deploy an ancestor of main or release (versable-builder/docs/app-patterns/03-deploying-on-gcp.md). Until a module has CI, deploy.sh from a workstation is acceptable and its output must include the commit it deployed.

A deploy is verifiable without credentials#

/build-info reports {commit, built_at, branch?, name, version, contract_version}, snake_case, unauthenticated (contracts/module-surface.md). It is one shape for modules and apps, so a deploy check is the same call whatever was deployed. Confirming a deploy is curl, not a console login. speedway and walmart both serve a build route; versable-runner does not.

Boot runs a preflight, once per commit#

On start in dev and prod, the module checks its substrate read-only (storage reachable, queue reachable, reference snapshots present, schema version expected) and reports the result once per role and commit rather than on every instance start. App V5 does this with a Redis dedup lock keyed on RENDER_GIT_COMMIT and a Slack report (lib/config/__init__.py:79-80, 228-237). /health/deep is the same check on demand.

Topology is a per-module choice, stated#

One service that pushes to itself (versable-runner) is fine at small scale and couples API and worker capacity; an api/worker split from one image (walmart, the extractor) decouples them at the cost of two services. The manifest's runtime block says which, and the trigger for splitting is written down: when an expensive payload starves the submit path, or when worker memory needs differ from API memory needs (walmart's OOM at 512 Mi). Cloud Run specifics worth knowing: minScale 1 costs money and buys the first request its cold start back; the extractor pair is the only instance that pays it, and it pays more than it looks: the api at minScale 1, the worker at minScale 10 (../instances/extraction.md). The other side of that trade was measured on 2026-08-18: the first /health call to a scaled-to-zero services-api timed out at 15 s and the second returned instantly (../contracts/conformance.md). A module that scales to zero states its cold start in the manifest, and a caller's timeout and a conformance runner's warm-up both allow for it; otherwise submit latency checks fail for a reason unrelated to submit. A pgbouncer sidecar and a VPC connector are what a Cloud SQL or Memorystore dependency costs; a module on GCS + Cloud Tasks alone needs neither.

Do-nots#

  • Do not share a service account, bucket, queue, or issuer client between environments. (runner-service-dev)
  • Do not ship a module that only knows one environment. (extractor)
  • Do not deploy without a /build-info that names the commit. (versable-runner)
  • Do not provision by hand. Script it, idempotent, in the repo.
  • Do not let a config value from one env run under another's env.
  • Do not promote by rebuilding. A version is built once and an environment points at it; promoting is moving the pointer, not producing a second artifact from the same source. Two builds of one commit are two artifacts, and which one is live becomes unanswerable. (evidence/20260820-internal-dashboard-reuse.md)
  • Do not name a health route /healthz on Cloud Run. (speedway found this the hard way, server.js:14)
@versable-git/ui · reference, canon, and method, read in place