Files
eden/docs/release-safety.md

133 lines
7.1 KiB
Markdown

# 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 root layout skips its authenticated session lookup only when every
capability represented by the root assistant/navigation shell is disabled;
if a separate asset, identity, session-token, subscription, or assistant
capability remains enabled, it preserves the existing user session state.
- 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:
```sh
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`](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.