Skip to content

chore(ci): make staging ephemeral — provision per deploy, destroy after - #832

Merged
manavgup merged 1 commit into
mainfrom
chore/ephemeral-staging-deploys
Jul 19, 2026
Merged

chore(ci): make staging ephemeral — provision per deploy, destroy after#832
manavgup merged 1 commit into
mainfrom
chore/ephemeral-staging-deploys

Conversation

@manavgup

Copy link
Copy Markdown
Owner

Problem

The June Fly.io invoice was ~$61/mo. About $25/mo of that was the staging environment: a 24/7 clone of production (web + worker machines, Postgres cluster, volume) plus a leftover managed Upstash Redis — all to serve as a smoke-test gate that's actually exercised for about ten minutes per deploy.

Solution

Staging now only exists while a deploy is running. The pipeline provisions a fresh staging stack, smoke-tests it, promotes to production, and destroys staging — pass or fail.

  • Clean-slate provisioning: deploy-staging destroys any leftovers, then creates the app, volume, and Postgres, attaches, and sets secrets. Destroy-then-create (rather than create-if-absent) is immune to partial state from a failed teardown.
  • Fresh empty DB per run is safe: migrations run via the alembic release_command; the smoke-test JWT resolves user_id from its sub claim with no DB lookup (src/wikimind/api/deps.py); every checked endpoint returns a valid empty array on a fresh DB.
  • New teardown-staging job with if: always() destroys wikimind-staging + wikimind-staging-db after the smoke gate.
  • Redis: staging queues on the shared self-hosted wikimind-redis at DB index 1 (prod uses DB 0). No dedicated staging Redis. (Already live: I repointed the running staging at DB 1 and destroyed the Upstash instance — verified healthy, worker connected, background_mode: arq.)
  • Docling: staging reuses the prod sidecar (stateless, scale-to-zero) instead of deploying its own 4GB-image copy per run. wikimind-staging-docling is no longer referenced and will be destroyed after this merges.

Changes

  • .github/workflows/deploy.yml — provision/teardown jobs, staging Redis URL staged by CI, staging docling deploy removed, header docs rewritten
  • fly.staging.toml — docling URL → prod sidecar; header notes the ephemeral model
  • README.md, docs/adr/adr-025-docling-serve-sidecar.md — deploy-flow descriptions updated

Notes

  • First deploy after merge replaces the current always-on staging stack automatically (clean-slate provision + teardown). I'll destroy wikimind-staging-docling manually once this lands.
  • Trade-off: ~2 min extra per deploy for Postgres provisioning (deploy-staging timeout bumped 10 → 15 min); staging DB state never persists between runs.
  • Companion PR chore(infra): scale prod web to zero when idle #831 handles prod web scale-to-zero + the uptime monitor.

🤖 Generated with Claude Code

@github-actions
github-actions Bot force-pushed the chore/ephemeral-staging-deploys branch from d6f2120 to 94d4b1c Compare July 19, 2026 14:22
@manavgup
manavgup force-pushed the chore/ephemeral-staging-deploys branch 2 times, most recently from d6f2120 to 57b4272 Compare July 19, 2026 14:23
manavgup added a commit that referenced this pull request Jul 19, 2026
CVE-2026-52869 (HIGH, fixed in 1.27.2) was published after #833 went
green, failing the Trivy scan on main's post-merge Docker build — which
skipped the deploy — and on PR #832's rebased checks. Lockfile-only
bump; full verify passes (2134 tests).

Co-authored-by: manavgup <manavgup@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
github-actions Bot pushed a commit that referenced this pull request Jul 19, 2026
Three times in four days a freshly published advisory failed the Trivy
scan on code that was green at review time (#833's two rounds, then
CVE-2026-52869 against mcp blocking the post-#833 deploy and #832).
A hard fail-on-publish gate freezes every in-flight merge on findings
unrelated to the change being merged.

Add an OPA ignore policy: HIGH vulnerabilities published within the
last 7 days are ignored, leaving a window to bump the dependency in an
orderly PR. After 7 days the scan fails as before, so nothing can be
ignored indefinitely. CRITICAL findings are never ignored, and findings
without a parseable PublishedDate fail closed.

Verified locally with trivy 0.70: the 7-day window still reports old
HIGH CVEs on python:3.11-slim, and a test 10-year window suppresses
them — confirming the rule keys on Severity + PublishedDate.

Also add .trivyignore and .github/trivy/** to the workflow's path
filters so editing scan-ignore rules re-triggers the scan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@manavgup
manavgup force-pushed the chore/ephemeral-staging-deploys branch from 57b4272 to 641ee06 Compare July 19, 2026 14:52
manavgup added a commit that referenced this pull request Jul 19, 2026
…837)

'Postgres integration tests' is a required status check on main, but
its workflow is path-filtered to src/tests/dependency paths. A PR
touching none of those (workflow-only or docs-only changes) never gets
the check reported and stays BLOCKED forever — this deadlocked #836
and #832 despite every triggered check passing.

Add GitHub's documented mirror pattern: an identically named job that
succeeds instantly, triggered by the exact inverse filter
(paths-ignore mirroring the real workflow's paths). Code PRs run the
real tests; non-code PRs get the green check from the mirror. This PR
unblocks itself the same way.

Co-authored-by: manavgup <manavgup@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
manavgup added a commit that referenced this pull request Jul 19, 2026
Three times in four days a freshly published advisory failed the Trivy
scan on code that was green at review time (#833's two rounds, then
CVE-2026-52869 against mcp blocking the post-#833 deploy and #832).
A hard fail-on-publish gate freezes every in-flight merge on findings
unrelated to the change being merged.

Add an OPA ignore policy: HIGH vulnerabilities published within the
last 7 days are ignored, leaving a window to bump the dependency in an
orderly PR. After 7 days the scan fails as before, so nothing can be
ignored indefinitely. CRITICAL findings are never ignored, and findings
without a parseable PublishedDate fail closed.

Verified locally with trivy 0.70: the 7-day window still reports old
HIGH CVEs on python:3.11-slim, and a test 10-year window suppresses
them — confirming the rule keys on Severity + PublishedDate.

Also add .trivyignore and .github/trivy/** to the workflow's path
filters so editing scan-ignore rules re-triggers the scan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@manavgup
manavgup force-pushed the chore/ephemeral-staging-deploys branch from 641ee06 to 7d211d6 Compare July 19, 2026 15:39
manavgup added a commit that referenced this pull request Jul 19, 2026
Three times in four days a freshly published advisory failed the Trivy
scan on code that was green at review time (#833's two rounds, then
CVE-2026-52869 against mcp blocking the post-#833 deploy and #832).
A hard fail-on-publish gate freezes every in-flight merge on findings
unrelated to the change being merged.

Add an OPA ignore policy: HIGH vulnerabilities published within the
last 7 days are ignored, leaving a window to bump the dependency in an
orderly PR. After 7 days the scan fails as before, so nothing can be
ignored indefinitely. CRITICAL findings are never ignored, and findings
without a parseable PublishedDate fail closed.

Verified locally with trivy 0.70: the 7-day window still reports old
HIGH CVEs on python:3.11-slim, and a test 10-year window suppresses
them — confirming the rule keys on Severity + PublishedDate.

Also add .trivyignore and .github/trivy/** to the workflow's path
filters so editing scan-ignore rules re-triggers the scan.

Co-authored-by: manavgup <manavgup@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The staging stack (web + worker machines, Postgres, volume) ran 24/7 as
a full clone of production but is only exercised for ~10 minutes per
deploy as a smoke-test gate. Together with a leftover managed Upstash
Redis it accounted for roughly $25/mo of the ~$61 June Fly invoice.

Rework the deploy pipeline so staging only exists during a deploy:

- deploy-staging provisions from a clean slate (destroy leftovers, then
  create app + volume + Postgres + attach + secrets). A fresh empty DB
  each run is fine: migrations run via the alembic release_command, the
  smoke-test JWT resolves users from its sub claim without a DB lookup,
  and all checked endpoints return valid empty collections.
- new teardown-staging job destroys the staging app and database after
  the smoke gate, pass or fail.
- staging queues on the shared self-hosted Redis at DB index 1
  (production uses DB 0) instead of a dedicated Upstash instance.
- staging reuses the production docling sidecar (stateless,
  scale-to-zero) instead of deploying its own 4GB-image copy per run;
  wikimind-staging-docling is no longer referenced.

Part of the Fly.io cost-reduction effort. The always-on staging apps
themselves are destroyed on the first deploy after this merges (the
clean-slate provision step replaces them, and teardown removes them).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions
github-actions Bot force-pushed the chore/ephemeral-staging-deploys branch from 7d211d6 to eb22ff8 Compare July 19, 2026 15:53
@manavgup
manavgup merged commit b54555b into main Jul 19, 2026
9 checks passed
@manavgup
manavgup deleted the chore/ephemeral-staging-deploys branch July 19, 2026 16:18
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.

1 participant