Skip to content
Self-hosting

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-candidate
bash

1. 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-candidate
bash

selfhost_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-candidate
bash

3. 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-candidate
bash

4. 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-candidate
bash
These need real credentials to reach a genuinely healthy /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-smoke
bash
SQLite is the trial/single-node default; recommend Postgres for production in release notes whenever this mode is what beta testers actually validated.

6. 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-smoke
bash

Expected 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.