Skip to content

chore(ops): self-hosted release/ops acceptance gate and runbook#200

Merged
parthrohit22 merged 1 commit into
Second-Origin:devfrom
parthrohit22:chore/115-self-hosted-release-gate
Jul 26, 2026
Merged

chore(ops): self-hosted release/ops acceptance gate and runbook#200
parthrohit22 merged 1 commit into
Second-Origin:devfrom
parthrohit22:chore/115-self-hosted-release-gate

Conversation

@parthrohit22

Copy link
Copy Markdown
Collaborator

Summary

Extends the local Docker Compose acceptance script into the repeatable
clean-clone release/ops gate issue #115 asks for, and adds the self-hosted
operations runbook it requires.

Linked issue

Closes #115

What changed

Infra (scripts/validate-compose.mjs) — after /ready returns 200, the
script now runs a full in-container acceptance sequence instead of just
tearing down:

  1. Migrations — re-runs alembic upgrade head in the live api
    container and asserts repositories/analysis_jobs/ri_snapshots exist;
    proves a downgrade -1 / upgrade head round trip is clean; proves the
    bug: alembic upgrade crashes when DATABASE_URL contains '%' (env.py interpolation) #110 percent-encoded-password regression against a real live
    connection
    (not just the existing offline unit test) by creating an
    isolated scratch Postgres role/database whose password contains a literal
    @ percent-encoded as %40 in DATABASE_URL, running the full migration
    set through it via docker compose exec -e DATABASE_URL=... api alembic upgrade head, then dropping the scratch role/db.
  2. Auth + owner isolation — registers two disposable users; user1
    imports a real tiny public GitHub repo (octocat/Hello-World, already
    this API's own OpenAPI example); asserts user2 gets exactly 404 (never
    403) reading user1's repository.
  3. Analysis lifecycle — starts analysis and polls /status to
    completed using the frontend's resilient-polling shape (capped
    exponential backoff on transient network errors, immediate stop on any
    terminal status, bounded deadline — mirrors useAnalysisPipeline.ts's
    1ca0daf/bug: analysis page reports 'Analysis Failed' when a status poll drops, while the durable job is still running #164 design); asserts a real sealed ri.v1 snapshot exists via
    GET /analysis/{id}/revision-manifest (snapshotSchemaVersion,
    canonicalGraphHash, sealedAt, verificationState: "verified").
  4. Restart/recoverydocker compose restart api, re-waits /ready,
    asserts the repository/analysis/sealed-snapshot hash are unchanged, and
    queries analysis_jobs directly over psql to assert exactly one row in
    completed state (no orphaned/in-flight job).
  5. Backup/restore — a real pg_dump/pg_restore round trip into a
    scratch database; asserts the restored ri_snapshots.canonical_graph_hash
    matches the original. The ri.v1 sealed-snapshot consumer path is already
    fully live (since bug: sealing a snapshot fails the entire analysis when a dependency is declared in more than one manifest #156/feat(dependencies): serve the dependency graph from the sealed ri.v1 snapshot #158), so this is implemented and asserted for
    real, not deferred.
  6. Secrets — captures docker compose logs once before teardown and
    asserts none of this run's generated secrets (both user passwords, the
    scratch DB password) appear verbatim.

It always tears the stack down in finally (docker compose down -v) and
exits non-zero on any assertion failure.

Docs — new docs/operations/SELF_HOSTED.md, linked from
docs/README.md's index: required environment variables, security
boundaries (owner-isolation 404 semantics, Fernet provider-key encryption, AI
egress allowlist — links the existing AI_PROVIDER_EGRESS.md rather than
duplicating it), unsupported deployment modes, and backup/restore
expectations for sealed snapshots.

No application code changed.

Acceptance criteria completed

  • A clean checkout can start the documented stack with one reproducible
    command sequence (npm run docker:configdocker:updocker:validate).
  • Postgres and Redis paths are exercised in an isolated acceptance
    environment (docker:validate runs the real Compose stack with both).
  • Readiness checks distinguish unavailable/starting/ready (pre-existing
    /ready + container healthchecks; now exercised by the gate).
  • Migration up/down/upgrade validation covers percent-encoded database
    URLs, against a real live connection.
  • Auth, owner isolation, and the first proof workflow are exercised
    end-to-end.
  • Restart/recovery and cleanup are tested without orphaned jobs or data
    corruption.
  • Backup/restore expectations for sealed snapshots are documented and
    tested (the snapshot path is live).
  • A self-hosted runbook lists required environment variables, security
    boundaries, and unsupported deployment modes.

Testing performed

npm run docker:validate
# run twice in a row from a clean `docker compose down -v` state
# both runs: exit 0, full sequence passes, clean teardown

cd apps/backend && PYTHONPATH= .venv/bin/python -m pytest
# 0 failures, 4 expected env-gated skips (unaffected by this change)

No frontend files were touched, so frontend lint/test/build were not run
(nothing to verify).

Screenshots

Not applicable (no UI change).

Security and data considerations

  • Reinforces, rather than changes, existing controls: the gate's headline
    assertion is that a non-owner gets 404 (never 403) on another user's
    repository, end-to-end against a real running stack.
  • The percent-encoded-password migration check runs against a disposable,
    isolated scratch role/database created and dropped within the same run —
    it never touches the main app database or any real credential.
  • All fixture credentials (two user passwords, the scratch DB password) are
    freshly generated per run and asserted absent from docker compose logs
    before teardown.
  • No secrets, .env files, or credentials are committed.

Dependencies and blocked work

None.

Scope changes or remaining work

.github/workflows/ci.yml's docker-compose job currently hand-rolls its
own up/wait-for-ready/down steps rather than calling
scripts/validate-compose.mjs, so CI does not yet run this deeper
acceptance sequence — only the local/manual gate does. The acceptance
criterion ("Postgres and Redis paths are exercised in CI or an equivalent
isolated acceptance environment
") is satisfied by the script itself as
written; wiring CI to call it too is a reasonable low-risk follow-up, but is
left out of this change to keep it scoped to what #115 asked for (extend the
script + add the runbook) rather than also changing CI behavior.

Contributor checklist

  • This PR targets dev
  • I claimed the issue and had it assigned or acknowledged before starting substantial work
  • The branch was created from an up-to-date upstream/dev
  • The branch is rebased on the latest upstream/dev
  • This PR addresses one clearly scoped issue
  • Every acceptance criterion I claim as complete is actually complete
  • Relevant tests pass
  • Documentation is updated for any user-visible change
  • No secrets, credentials, local env files, or generated artifacts are included
  • No unrelated files were changed
  • Closing syntax (Closes) is used only because the issue is fully resolved
  • Dependencies and follow-up work are linked

scripts/validate-compose.mjs now runs a full in-container acceptance
sequence after /ready returns 200, instead of only checking readiness:
re-runs and verifies Alembic migrations (core tables exist, a
downgrade/upgrade round trip is clean, and a live connection through a
percent-encoded DATABASE_URL password succeeds -- the Second-Origin#110 regression,
proven against an isolated scratch role/database rather than only the
existing offline unit test); registers two disposable users and proves
owner isolation (a non-owner gets 404, never 403); runs a real analysis
to a sealed ri.v1 snapshot and polls status with the frontend's
resilient-polling shape (capped backoff on transient errors, immediate
stop on any terminal state); restarts the api container and confirms
the repository, analysis, and sealed snapshot all survive with no
orphaned job rows; proves a pg_dump/pg_restore round trip preserves a
sealed snapshot; and checks docker compose logs for the secrets this
run generated. It always tears the stack down and exits non-zero on
any assertion failure.

Add docs/operations/SELF_HOSTED.md (linked from docs/README.md):
required environment variables, security boundaries, unsupported
deployment modes, and backup/restore expectations for the self-hosted
stack.

Testing performed:
- npm run docker:validate, twice from a clean teardown state: both
  exit 0, full sequence passes.
- cd apps/backend && PYTHONPATH= .venv/bin/python -m pytest: 0
  failures, 4 expected env-gated skips (unaffected by this change).
@parthrohit22
parthrohit22 merged commit 32c9733 into Second-Origin:dev Jul 26, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

chore: establish clean-clone self-hosted release gate and operational readiness

1 participant