docs: gate staging deploy on pending migrations

This commit is contained in:
Jesse_Chen
2026-07-20 17:50:11 +08:00
parent 1fb6d4ffd7
commit d913291cee
2 changed files with 36 additions and 10 deletions
@@ -18,6 +18,7 @@
- `.env.staging.database` is server-side only, mode `0600`, and excluded from Git/rsync. It contains bootstrap and migration credentials. `.env.staging` must not contain them.
- Normal web/API containers never receive `SCHEMA_DATABASE_URL`.
- App deployment never runs schema migration. Migration is manual and separately serialized.
- Before changing app containers, staging deploy runs the exact SHA image in read-only migration-check mode. No pending migration means automatic continuation. Pending or checksum-drifted migration stops before app changes; a successful manual migration dispatches staging deploy again for the same full SHA.
- Production defaults remain manual-only and unchanged.
- Staging image tags are immutable full Git SHAs; never deploy `latest`.
- Finish each task with the focused commit shown.
@@ -205,6 +206,19 @@ services:
condition: service_healthy
networks: [app]
migration-checker:
image: ${WEB_IMAGE:-jyotisha-web:local}
profiles: ["migration-check"]
restart: "no"
env_file:
- ${DATABASE_ENV_FILE:-../.env.staging.database}
working_dir: /app/frontend
command: ["npm", "run", "db:migrate:check"]
depends_on:
postgres:
condition: service_healthy
networks: [app]
volumes:
postgres_data:
@@ -379,7 +393,8 @@ npm install pg drizzle-orm
npm install --save-dev @types/pg
```
Add `"db:migrate": "node scripts/db-migrate.mjs"`.
Add `"db:migrate": "node scripts/db-migrate.mjs"` and
`"db:migrate:check": "node scripts/db-migrate.mjs --check"`.
- [ ] **Step 4: Add typed URL config and lazy client**
@@ -460,6 +475,7 @@ create table if not exists migration.schema_migrations (
- Matching row: log `already applied <filename>`.
- Changed checksum: throw `migration checksum mismatch: <filename>`.
- New file: `BEGIN`, execute file, insert ledger row, `COMMIT`; rollback on error.
- With `--check`, perform no DDL/DML: compare exact files with the existing ledger, print pending filenames only, exit `0` when current, exit `3` when any file is pending, and exit `1` on checksum drift or unsafe failure. A missing ledger means every file is pending.
- Release lock and close in `finally`.
- Never log URL, SQL, env, or driver config.
- Direct invocation defaults to `frontend/db/migrations`, requires `SCHEMA_DATABASE_URL`, prints safe filename-only errors, and exits `1`.
@@ -729,7 +745,9 @@ Assert:
- Staging listens to successful `Staging Backend Quality Gate`; manual validation queries `backend-quality-gate.yml` for exact SHA.
- Every staging Compose invocation uses server and Postgres files plus explicit app/database env, Caddyfile, hostname, API image, and web image.
- Workflow logs into GHCR, pulls, and runs `up -d --no-build`.
- It never contains `db:migrate`, `migrator`, or `--profile migration`.
- It never invokes the applying `db:migrate` command, `migrator` service, or `--profile migration`.
- It runs the exact web image through `migration-checker`/`db:migrate:check` before changing any app container.
- Pending migrations stop before `api`, `web`, or `caddy` changes and print the manual workflow name plus exact SHA.
- Rollback uses recorded prior image names.
- [ ] **Step 2: Confirm red**
@@ -770,7 +788,8 @@ WEB_IMAGE=ghcr.io/jesse-ux/jyotisha-web:<full-sha>
- Validate both env files and Compose config.
- Send GHCR token through `docker login --password-stdin`; never save it in either env file.
- Pull `api web postgres`, then `up -d --no-build --remove-orphans`.
- Pull `api web postgres`, start/wait for PostgreSQL, then run the exact web image through `--profile migration-check run --rm migration-checker`.
- Continue to `up -d --no-build --remove-orphans` only after check exit `0`; treat exit `3` as a safe stop with no application changes.
- Record prior container image names before switching. Roll back with those exact image names and `--no-build`.
- Log out in an always-running cleanup step.
- Never run migrations.
@@ -810,7 +829,7 @@ git commit -m "ci: deploy immutable staging images"
- [ ] **Step 1: Add failing contracts**
Assert manual-only dispatch, full 40-character SHA, `staging` environment, successful exact-SHA backend gate, pinned web image, both env validators, Postgres-only start, `--profile migration run --rm migrator`, filename-only ledger output, and no web/API/Caddy restart.
Assert manual-only dispatch, full 40-character SHA, `staging` environment, successful exact-SHA backend gate, pinned web image, both env validators, Postgres-only start, `--profile migration run --rm migrator`, filename-only ledger output, and no web/API/Caddy restart. Also assert that success dispatches `deploy-staging.yml` with the same full SHA.
- [ ] **Step 2: Confirm red**
@@ -836,7 +855,7 @@ concurrency:
cancel-in-progress: false
permissions:
contents: read
actions: read
actions: write
packages: read
jobs:
migrate:
@@ -870,7 +889,7 @@ docker compose -p jyotisha-staging \
'select filename from migration.schema_migrations order by filename'
```
Authenticate GHCR through stdin and log out in cleanup. Do not start/restart app services.
Authenticate GHCR through stdin and log out in cleanup. Do not start/restart app services. After migration and ledger reporting succeed, call the GitHub workflow-dispatch API for `deploy-staging.yml` with `ref: staging` and `inputs.deploy_sha` equal to the validated full SHA.
- [ ] **Step 4: Verify and commit**
@@ -934,8 +953,8 @@ Document order:
1. Merge to `staging`.
2. Wait for backend quality gate and SHA images.
3. If migration exists, manually run `Migrate Staging Database` with full SHA.
4. Let exact-SHA staging deploy run.
3. If automatic deploy reports pending migrations, manually run `Migrate Staging Database` with the reported full SHA.
4. The successful migration workflow re-dispatches exact-SHA staging deploy automatically.
5. Check `https://staging.jyotisha.chat/api/health`.
6. Run:
@@ -989,7 +1008,7 @@ Expected: all PASS.
- [ ] **Step 2: Boundary and secret scans**
```bash
rg -n 'db:migrate|migrator|profile migration' \
rg -n 'db:migrate([^:]|$)|migrator|profile migration' \
.github/workflows/deploy-staging.yml
rg -n 'up -d.*--build|docker compose build' \
.github/workflows/deploy-staging.yml
@@ -999,7 +1018,7 @@ rg -n 'sb_secret_|sb_publishable_|postgresql://[^:<[:space:]]+:[^<[:space:]]+@'
.github deploy frontend/db frontend/scripts frontend/src/lib/db frontend/tests
```
Expected: first two commands have no matches; third matches manual migration; fourth finds no real credential (inspect and allow only explicit test fixtures or documentation placeholders).
Expected: first two commands have no applying-migration/build matches (the read-only `db:migrate:check` is allowed); third matches manual migration; fourth finds no real credential (inspect and allow only explicit test fixtures or documentation placeholders).
- [ ] **Step 3: Inspect final state**
@@ -175,6 +175,13 @@ Application deployment and database migration remain different operations:
- data import/reconciliation is a separate migration-runner operation;
- normal deploys never run database migration implicitly.
Before switching application containers, the staging deployment workflow runs the
exact SHA web image in read-only migration-check mode. With no pending migrations
it continues automatically. With pending or drifted migrations it stops before
touching the running application. After the operator runs the manual migration
workflow successfully, that workflow dispatches staging deployment again for the
same full SHA. The check may read the migration ledger but may never apply SQL.
Deployment records the previous application SHA and image digests. Public and private health checks must pass before a deployment is marked successful. Application rollback does not claim to roll back database state.
## Automatic backend quality gate