Files
eden/docs/release-safety.md
T

6.8 KiB

Release and rollout safety (T01)

This repository uses Gitea Actions for CI. The workflow in .gitea/workflows/ci.yml installs the frozen lockfile, runs local tests/check/build, compiles the checked-in synthetic reference fixture, runs an isolated demo web smoke, and prepares a separate Docker/Compose artifact job. It does not receive deployment credentials, trigger a remote deployment, or write a persistent Convex backend.

Server-side rollout gate

APP_CAPABILITY_ALLOWLIST is a comma-separated deployment setting parsed by src/lib/server/release-gate.ts. An omitted setting preserves the existing private asset/sign-in baseline (asset_search, asset_valuation, asset_sync, assistant_dispatch, session_token, identity, and subscriptions). An explicitly empty setting disables all capabilities. Unknown names fail configuration rather than becoming an accidental opt-in; known capabilities that are not in the baseline (for example wallet_balance, inventory_export, and manufacturing_opportunities) are default-deny.

The web server checks the allowlist at the relevant boundary:

  • /api/chat checks assistant_dispatch before invoking manual or external intent routing.
  • /assets and assistant execution check the selected asset capability plus asset_valuation, because the current shared report intentionally returns a reference valuation even for browse results.
  • /api/sync and the application synchronizer check asset_sync.
  • /api/session/token checks session_token.
  • Character management and OAuth entry points check identity.
  • The Convex browser status subscription checks subscriptions; sync bridge mutations check asset_sync in the backend authorization plane too. The allowlist is copied into Convex by bun run convex:configure; that setting is required there rather than inferred from the web container.

This is an admission control mechanism, not a UI feature flag. Disabled operations return an explicit unavailable/503 response and do not invoke a provider. The current character status subscription is gated in Convex; no durable user-job service exists in this baseline, so future job kinds remain closed until they have reviewed enqueue and worker entry points.

Policy authority and update order

The web and Convex processes receive the same value, but parse it in separate processes. The web parser preserves the private baseline when its setting is omitted for development compatibility; the Convex/backend parser requires an explicit, valid setting and fails closed when propagation is missing, empty, or malformed. Convex is authoritative for persisted subscriptions and bridge mutations; the web gate is an earlier ingress/provider guard, not a replacement for backend authorization.

Update the backend setting first, validate a status query and representative bridge mutation in an isolated deployment, then update the web setting and verify the HTTP gate. A web-only narrowing does not revoke existing Convex subscriptions, sessions, or grants, and this lane makes no such claim. A backend-only narrowing may cause already-running web requests to receive backend denials until the web setting converges. Record both settings and the validation timestamps in the release evidence; rollback must restore the backend policy before reopening web entry points.

Immutable release metadata

APP_RELEASE_ID is carried into the runtime image and returned as the non-secret x-application-release response header. Local/fixture runs use local or fixture. Staging and production must provide a non-placeholder identifier (normally the source commit or an operator-approved image digest). bun run release:check requires an explicit RELEASE_ENV for live runs and rejects placeholder staging/production identifiers, unpinned staging/production backend/dashboard images or Dockerfile bases, demo mode in staging/production, invalid capability names, tracked secret-like files, and managed secret names in built browser artifacts. The Dockerfile uses digest-pinned Bun bases and accepts --build-arg APP_RELEASE_ID=...; Compose passes the runtime value explicitly.

Isolated local validation

Use a separate worktree-local output directory and a unique Compose project name for backend experiments. Never point these commands at a persistent backend or copy a live .env/volume:

APP_MODE=demo APP_RELEASE_ID=fixture bun run build
APP_MODE=demo APP_RELEASE_ID=fixture bun run release:check
APP_MODE=demo APP_RELEASE_ID=fixture bun run test:fixture

test:fixture compiles the checked-in reference sources into a temporary folder, starts the built server on loopback with synthetic data, checks the release header and matching /assets/chat totals, then kills the server and removes the temporary artifact directory.

For an isolated Compose rehearsal, set COMPOSE_PROJECT_NAME to a unique throwaway value, keep all binds on loopback, use a fixture/demo environment, and remove the project only after recording the result. The release gate does not itself perform migrations, backups, restores, or cleanup of an operator's persistent deployment.

Restore and migration evidence

The synthetic restore/migration rehearsal is specified in docs/restore-rehearsal.md. It includes Convex data and instance-credential backup/restore, application rollback, migration rollback prerequisites, session/grant invalidation decisions, resource budgets, and a redacted evidence template. It has not been executed in this lane.

Evidence boundaries

Read-only tea inspection on 2026-09-19 reported no Gitea workflows or runs for jb/eden; runner labels, Docker availability, protected-ref enforcement, and action availability were therefore not validated. The workflow is prepared locally but has not run remotely. The source job runs a strict staging release check after building the client. The separate container job is currently disabled (if: ${{ false }}), not merely push-filtered: its rootless runner and protected-ref policy require operator verification and approval before enablement. The local infrastructure plan mentions personal-isolated, ubuntu-latest, and ubuntu-22.04 labels, but those are not confirmed registered labels. The plan describes a dedicated runner VM with a Docker-socket-backed daemon; rootless execution, online status, action availability, and protected refs remain unverified. Its draft commands use per-run image, volume, and container names and clean up only those names, but are not release evidence while the job is disabled. No browser automation, OAuth callback failure injection, Convex migration rollback, instance-credential backup/restore, key rotation, or live revocation/transfer evidence is claimed by this lane. Those remain explicit operator/integration gates and must use synthetic or separately consented isolated environments.