Beta release checklist
Run this smoke matrix against a candidate image before tagging it orb-vX.Y.Z or an -rc/-beta prerelease. Each scenario is a mode a real operator will run; CI only exercises the plain SQLite + Redis + direct-App default.
Every scenario below shares the same core check — scripts/smoke-selfhost.sh boots one container against a fresh Redis on an isolated network, waits for it to become healthy, and asserts on /health, /ready, /metrics, and startup log events. What changes per scenario is the env you pass in and which events you expect (or forbid).
# Build (or use a published tag) once, then run each scenario against the same image:
docker buildx build --load -t gittensory:rc-candidate .
./scripts/smoke-selfhost.sh gittensory:rc-candidatebash1. Direct GitHub App mode (default)
No ORB_ENROLLMENT_SECRET — the container uses its own GitHub App private key. Telemetry export is always-on in this mode too; a clean run produces no export error.
# A private key is multiline PEM -- mount it as a file instead of an env value (SELFHOST_SMOKE_EXTRA_ENV
# is line-delimited and would truncate it). GITHUB_APP_PRIVATE_KEY_FILE is loaded into
# GITHUB_APP_PRIVATE_KEY at startup, same as every other *_FILE variable.
SELFHOST_SMOKE_EXTRA_VOLUMES="${TEST_APP_PRIVATE_KEY_PATH}:/run/secrets/github-app-private-key.pem:ro" \
SELFHOST_SMOKE_EXTRA_ENV="GITHUB_APP_ID=123456
GITHUB_APP_PRIVATE_KEY_FILE=/run/secrets/github-app-private-key.pem" \
SELFHOST_SMOKE_FORBID_EVENTS="selfhost_orb_export_error,selfhost_orb_relay_register" \
./scripts/smoke-selfhost.sh gittensory:rc-candidatebashselfhost_orb_relay_register must NOT appear here — relay registration is brokered-only and silently skips in direct mode (see GitHub App and Orb).
2. Brokered mode (private / managed-beta only)
ORB_ENROLLMENT_SECRET set — the container gets tokens from the central Orb instead of its own App key. A working push-mode registration logs selfhost_orb_relay_register; a broken one is fatal for push mode (logged at error, not warn).
SELFHOST_SMOKE_EXTRA_ENV="ORB_ENROLLMENT_SECRET=${TEST_ENROLLMENT_SECRET}
PUBLIC_API_ORIGIN=https://selfhost-smoke.example" \
SELFHOST_SMOKE_EXPECT_EVENTS="selfhost_orb_relay_register" \
SELFHOST_SMOKE_FORBID_EVENTS="selfhost_orb_relay_register_failed" \
./scripts/smoke-selfhost.sh gittensory:rc-candidatebash3. Air-gapped / no-telemetry mode
ORB_AIR_GAP=true disables the fleet-calibration export entirely. There is no "air-gap confirmed" log event — the export function returns before doing anything, so silence (no export error, no export attempt) is the signal. Confirm at the network level too: no outbound request to the collector URL.
SELFHOST_SMOKE_EXTRA_ENV="ORB_AIR_GAP=true" \
SELFHOST_SMOKE_FORBID_EVENTS="selfhost_orb_export_error,selfhost_orb_relay_register" \
./scripts/smoke-selfhost.sh gittensory:rc-candidatebash4. AI provider: Claude Code / Codex / both
Each provider choice must log selfhost_ai_provider and must NOT log selfhost_ai_cli_missing (a CLI-subscription provider whose binary isn't on PATH silently produces no review output — this must be caught here, not in production).
# Claude Code only
SELFHOST_SMOKE_EXTRA_ENV="AI_PROVIDER=claude-code
CLAUDE_CODE_OAUTH_TOKEN=${TEST_CLAUDE_TOKEN}" \
SELFHOST_SMOKE_EXPECT_EVENTS="selfhost_ai_provider" \
SELFHOST_SMOKE_FORBID_EVENTS="selfhost_ai_cli_missing" \
./scripts/smoke-selfhost.sh gittensory:rc-candidate
# Codex only (requires the fail-closed opt-in)
SELFHOST_SMOKE_EXTRA_ENV="AI_PROVIDER=codex
GITTENSORY_ENABLE_UNSAFE_CODEX_REVIEWER=1" \
SELFHOST_SMOKE_EXPECT_EVENTS="selfhost_ai_provider" \
SELFHOST_SMOKE_FORBID_EVENTS="selfhost_ai_cli_missing" \
./scripts/smoke-selfhost.sh gittensory:rc-candidate
# Both, synthesized
SELFHOST_SMOKE_EXTRA_ENV="AI_PROVIDER=claude-code,codex
AI_COMBINE=synthesis
CLAUDE_CODE_OAUTH_TOKEN=${TEST_CLAUDE_TOKEN}
GITTENSORY_ENABLE_UNSAFE_CODEX_REVIEWER=1" \
SELFHOST_SMOKE_EXPECT_EVENTS="selfhost_ai_provider" \
SELFHOST_SMOKE_FORBID_EVENTS="selfhost_ai_cli_missing" \
./scripts/smoke-selfhost.sh gittensory:rc-candidatebash/ready (it probes the configured AI provider). Where credentials aren't available for a given RC run, at minimum confirm selfhost_ai_cli_missing does NOT appear — that alone catches the release-blocking case (image built without INSTALL_AI_CLIS=true).5. SQLite trial mode / Postgres production mode
SQLite is the default — the base smoke command above already covers it (no DATABASE_URL set). For Postgres, boot a Postgres container on the same network first and point DATABASE_URL at it.
docker network create gt-pg-smoke
docker run -d --name gt-pg --network gt-pg-smoke -e POSTGRES_PASSWORD=devpw -e POSTGRES_DB=gittensory postgres:16-alpine
SELFHOST_SMOKE_NETWORK=gt-pg-smoke \
SELFHOST_SMOKE_EXTRA_ENV="DATABASE_URL=postgres://postgres:devpw@gt-pg:5432/gittensory" \
./scripts/smoke-selfhost.sh gittensory:rc-candidate
docker rm -f gt-pg && docker network rm gt-pg-smokebash6. Redis cache + optional Qdrant RAG
Redis is always-on in every scenario above (the base script already boots it) — confirm selfhost_redis_ready appears with githubResponseCacheEnabled matching whatever GITHUB_CACHE_TTL_SECONDS you set. For the optional Qdrant RAG path, boot Qdrant on the same network and point QDRANT_URL at it.
SELFHOST_SMOKE_EXPECT_EVENTS="selfhost_redis_ready" \
./scripts/smoke-selfhost.sh gittensory:rc-candidate
# With Qdrant RAG:
docker network create gt-rag-smoke
docker run -d --name gt-qdrant --network gt-rag-smoke qdrant/qdrant:v1.18.2
SELFHOST_SMOKE_NETWORK=gt-rag-smoke \
SELFHOST_SMOKE_EXTRA_ENV="QDRANT_URL=http://gt-qdrant:6333" \
SELFHOST_SMOKE_EXPECT_EVENTS="selfhost_vectorize" \
./scripts/smoke-selfhost.sh gittensory:rc-candidate
docker rm -f gt-qdrant && docker network rm gt-rag-smokebashExpected startup events
- selfhost_listening
- Always. HTTP server bound and accepting connections.
- selfhost_migrations_applied
- Always. The smoke script asserts this on every scenario.
- selfhost_redis_ready
- Always. Confirms the mandatory Redis dependency is reachable.
- selfhost_ai_provider
- Only when AI_PROVIDER is set. Confirms the provider chain resolved.
- selfhost_vectorize
- Only when QDRANT_URL is set. Confirms the Qdrant RAG backend is wired.
- selfhost_orb_relay_register
- Only in brokered mode. Confirms relay registration with the central Orb.
Known warnings: acceptable in beta vs. release-blocking
- selfhost_orb_relay_register_failed (pull mode)
- Acceptable in beta. Logged at warn — pull-mode relay still drains events outbound even when the announce fails.
- selfhost_orb_relay_register_failed (push mode)
- Release-blocking. Logged at error — a failed push-mode announce means the container looks alive but never receives events.
- selfhost_ai_cli_missing
- Release-blocking. A CLI-subscription provider that can't run silently produces zero review output in production.
- selfhost_orb_export_error (isolated, one-off)
- Acceptable in beta if transient (e.g. a single collector timeout) — the hourly retry recovers. Persistent recurrence across the whole smoke run is release-blocking.
After every applicable scenario passes, continue with the normal upgrade flow to cut the tag and publish the image.