# Guide 26: VigilCareRecords CI/CD (Gitea Actions + Docker Compose)

How VigilCareRecords is built, tested, and deployed on the same Gitea Actions + `act_runner` + container registry setup already used by VigilCareClinical. See [`docs/25-gitea-cicd-docker-deploy.md`](25-gitea-cicd-docker-deploy.md) for the general pattern this guide instantiates; this doc only covers what is specific to this repo.

Related files:

- [`.gitea/workflows/ci.yml`](../.gitea/workflows/ci.yml) / [`.gitea/workflows/cd.yml`](../.gitea/workflows/cd.yml)
- [`docker-compose.yml`](../docker-compose.yml) (local + CI dependencies)
- [`docker-compose.prod.yml`](../docker-compose.prod.yml) (production app stack)
- [`.env.example`](../.env.example)
- [`VigilCareRecordsAPI/Dockerfile`](../VigilCareRecordsAPI/Dockerfile) / [`vigilcare-records-web/Dockerfile`](../vigilcare-records-web/Dockerfile)

---

## 1. Topology

VigilCareRecords ships two images:

| Image | Dockerfile | Build context |
|---|---|---|
| `vigilcare-records-api` | `VigilCareRecordsAPI/Dockerfile` | repo root |
| `vigilcare-records-dashboard` | `vigilcare-records-web/Dockerfile` | `vigilcare-records-web/` |

Unlike VigilCareClinical, there is no gateway service, and the frontend does not bake in an absolute API URL at build time — `src/api/client.ts` and `src/api/fhirClient.ts` use relative base URLs (`/api/v1`, `/fhir`). The dashboard's `nginx.conf` reverse-proxies those paths to the `api` container, so the same image works behind any hostname without a build-arg.

## 2. Shared production infrastructure

Per the "Integrated Database Deployment" decision documented in [`vigilcare-records-clinical-overview.md`](vigilcare-records-clinical-overview.md), VigilCareRecords does **not** run its own Postgres/Redis/MinIO/Seq in production. It joins the external `shared-services` Docker network already created by VigilCareClinical's own compose project and connects to those same containers, using:

- Its own logical Postgres database (`vigilcare_records`, separate schema/tables from VigilCareClinical's `patients`/`encounters`/`observations` — see the promotion service for how the two connect at the application layer, not the infrastructure layer)
- A dedicated Redis logical database (`defaultDatabase=2` in `.env.example`) so batch-assignment locks never collide with VigilCareClinical's keys
- A dedicated MinIO bucket (`vigilcare-records-scans`) separate from any clinical document buckets

**Before the first deploy**, confirm the VigilCareClinical infra stack (or whatever compose project owns `shared-services`) is already running on the deploy host — `docker network ls | grep shared-services` should show it. `docker compose -f docker-compose.prod.yml up` will fail to find the network otherwise.

## 3. One-time host setup

```bash
ssh deploy@YOUR_HOST "mkdir -p /opt/vigilcare-records"
scp .env deploy@YOUR_HOST:/opt/vigilcare-records/.env
ssh deploy@YOUR_HOST "chmod 600 /opt/vigilcare-records/.env"
```

Fill in `.env` from [`.env.example`](../.env.example) first — generate `JWT_SECRET` with `openssl rand -base64 48`, and get real credentials for the shared Postgres/Redis/MinIO/Seq services from whoever manages that stack. CD never uploads or overwrites `.env`; only the `IMAGE_TAG` line is patched automatically on each release.

Create the Records MinIO bucket once (the API also creates it on startup if missing):

```bash
ssh deploy@YOUR_HOST bash -euo pipefail <<'EOF'
cd /opt/vigilcare-records
env_val() { sed -n "s/^${1}=//p" .env | tail -n1 | tr -d '\r'; }
ACCESS="$(env_val MINIO_ACCESS_KEY)"
SECRET="$(env_val MINIO_SECRET_KEY)"
BUCKET="$(env_val MINIO_BUCKET_NAME)"; BUCKET="${BUCKET:-vigilcare-records-scans}"
docker run --rm --network shared-services --entrypoint /bin/sh minio/mc \
  -c "mc alias set local http://minio:9000 '${ACCESS}' '${SECRET}' && mc mb --ignore-existing local/${BUCKET}"
EOF
```

## 4. Gitea secrets and variables

Repo → **Settings** → **Actions**.

### Secrets

| Secret | Used by |
|---|---|
| `REGISTRY_USERNAME` | `docker login` |
| `REGISTRY_TOKEN` | `docker login` (access token / PAT with package write) |
| `PG_CONNECTION_DDL` | migrate job only — DDL-privileged connection (`Host=postgres` on `shared-services`), never given to the API container |
| `DEPLOY_HOST` | SSH / SCP |
| `DEPLOY_USER` | SSH / SCP |
| `DEPLOY_SSH_KEY` | Private key PEM / OpenSSH private key body |

### Variables

| Variable | Used by |
|---|---|
| `REGISTRY` | Optional override of the default `git.vectur45.com/trent/vigilcare-records` |

## 5. Release flow

```bash
# CI green on master, then:
git tag v1.0.0
git push origin v1.0.0
```

This triggers `.gitea/workflows/cd.yml`: build & push both images → SSH the EF migrations bundle onto the deploy host and run it on `shared-services` with `PG_CONNECTION_DDL` → SSH deploy (`scp` the prod compose file, patch `IMAGE_TAG`, `pull` + `up -d`) → smoke test (`/health/ready`, dashboard `/`) → automatic rollback to the previous `IMAGE_TAG` on failure (schema changes are not reverted; see the expand/contract note in `cd.yml`).

Manual redeploy of an existing tag: Gitea UI → Actions → CD → Run workflow, with `image_tag` input.

## 6. Pre-production checklist (application-level, not part of this CI/CD change)

- **Demo data seeding runs unconditionally on startup.** `Program.cs` calls `DataSeeder.SeedAsync(db)` whenever `ASPNETCORE_ENVIRONMENT` is not `Testing` — including `Production`. Unlike the VigilCareClinical example (`Seeding__EnableDemoData=false`), this app has no seeding toggle. Before a real clinical deploy, either add a guard around `DataSeeder.SeedAsync` for `Production`, or confirm the twelve seeded demo accounts (all password `password`) are acceptable/rotated before go-live.
- Confirm `Cors:AllowedOrigins` / `DASHBOARD_ORIGIN` matches the real public dashboard origin if the dashboard is ever served from a different origin than the API (not the default same-origin nginx-proxy setup described above).
- Rotate `JWT_SECRET` from any value used during testing before the first real deploy.
