Skip to content

Deploy, CI & coverage gate: no GitHub Actions, fake healthcheck, unpinned deps, stale nginx (5 PRs) #27

Description

@ahueb

Five deploy/supply-chain items. Land I1 first — it is the gate every other issue's PRs will run through, so it pays back across the whole backlog.

Line numbers current as of origin/main @ b7edcbc (2026-07-30).

Suggested order: I1 → I2 → I3 → I4 → I5.

PR I1 — GitHub-side CI + full local gate (small — includes the coverage ratchet)

No .github/ directory exists (re-confirmed 2026-07-30). PR #17 added only a local, bypassable (git push --no-verify) pre-push hook at .git/hooks/pre-push running scripts/ci.sh, which lints only tests/ (not src/, by explicit choice at ci.sh:8-10) and runs pytest with a COV_MIN=35 floor (:25) — but no type check and no alembic upgrade head gate. Fix: a GitHub Actions workflow running ruff (including src/) + pytest + a type check + alembic upgrade head against a scratch DB, on every PR.

Folded in — the coverage ratchet (was tracked separately as T1): bash scripts/ci.sh was run end-to-end on 2026-07-30 and passed (exit 0): 523 passed in 41.44 s, total coverage 38.99 %. (Commit 10c240c reports 39.08 %; the ~0.1 pt delta is environmental — py3.12 host venv vs py3.11 container.) Up from 487/37 % at the original audit baseline as the six post-9548c08 commits added 36 test functions (433 → 469). scripts/ci.sh pins COV_MIN=35 (ci.sh:25), ~4 points below actual. Concrete task in this PR: raise COV_MIN to 38 and keep it a one-way ratchet.

Confirmed 0 % in that run: worker/main.py, agent/pi_handler.py, agent/main.py, cli.py. Measured low: profile_export.py 8 %, agent_page.py 10 %, tools.py 10 %, admin.py 11 %, grantbot.py 12 %, email_notifications.py 13 %, invite.py 17 %, slack_provisioning.py 18 %, email_inbound.py 19 %, simulation.py 30 %. The integration harness (conftest.py + testcontainers-PG + TestClient) already exists from PR #17 and the test venv is at .venv-test/, so no test-infra build-out is needed.

This is the definition of done for every issue in this backlog: each PR ships a test that covers its defect line and fails against the pre-fix code. Issues #20, #21 and #23 touch the largest 0 %/low-coverage files and therefore carry most of the ratchet.

PR I2 — Real /api/health DB check + a migrate step (small — live failure observed)

main.py:144-147 /api/health returns an unconditional {"status":"ok"} with no SELECT 1, and there is no alembic upgrade in the build/deploy path. The prod compose adds a healthcheck plus nginx depends_on: service_healthy, which makes this worse — it marks an unmigrated app healthy and sends it traffic.

This was observed live on 2026-07-30, not predicted. The local dev database is at alembic_version = 0018 while the code requires 0021. In the same process, at the same moment:

GET /api/health                     -> HTTP 200 {"status":"ok"}
first ORM read of agent_messages    -> ProgrammingError:
    <class 'asyncpg.exceptions.UndefinedColumnError'>: column agent_messages.content does not exist

So the endpoint reports healthy on an app for which every read of the primary conversation table fails.

Fix: a migrate step gated before app traffic + a SELECT 1 in the health route. (Separately from this PR, that dev DB needs alembic upgrade headalembic upgrade head was verified to apply cleanly 0001→0021 on a fresh database.)

PR I3 — Dockerfile hardening (medium — verified against the built image)

Verified on 2026-07-30 by building and inspecting copiscience-app:

claim observed
no .dockerignore file absent
runs as root docker compose exec app iduid=0(root) gid=0(root); image Config.User empty
single-stage 11 layers, one FROM
COPY src/ src/ before pip install . busts the dependency layer cache on every source change
COPY . . bakes junk /app/.git = 5.9 MB, /app/tests, /app/.notes

The .notes entry is the important one: COPY does not honour .gitignore, so the gitignored .notes/ directory — this backlog, the issue drafts and every prior audit document — ships inside the production image. That, not cache efficiency or image size, is the reason to do this.

Fix: add .dockerignore (at minimum .git, .notes, tests, .venv-test, logs); split dep-install from source copy; non-root USER; multi-stage build.

PR I4 — Lockfile + dependency pinning (medium)

All 17 runtime deps are >= floors with no upper caps and no lockfile (pyproject.toml:11-29); anthropic>=0.26.0 floats across majors; plotly>=5.20.0 is a runtime dep but imported only in scripts/. Fix: a hash-pinned lockfile installed in the Dockerfile; raise floors past known CVEs; cap pre-1.0/major SDKs; move plotly to dev/scripts.

PR I5 — Reconcile nginx.conf + add resource limits (small-medium)

nginx/nginx.conf is a stale Next.js template: "Next.js app on port 3000" comments at :14/:37/:69/:127/:166 (the upstream is actually app:8000), proxy_read_timeout 120s (:124/:145/:237), no CSP add_header, and a dead proxy_cache_valid (:169) with no proxy_cache_path anywhere in the file. No mem_limit/cpus on any prod service; prod bind-mounts ./prompts over the baked copy (docker-compose.prod.yml:41/97/126). (SEC-15 rate-limit zones were already added.) Fix: reconcile the nginx template; add container resource limits.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions