Ctrl K
ComunidadGuías…

docs/adr/0061-release-migrate-before-roll.md

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# ADR-0061: Release Migrate-Before-Roll — Explicit Schema Migrations with a Dedicated Timeout

## Metadata

| Field          | Value                                            |
| -------------- | ------------------------------------------------ |
| **Status**     | Accepted                                         |
| **Date**       | 2026-08-18                                       |
| **Authors**    | AlessioBrillo                                    |
| **Deciders**   | AlessioBrillo                                    |
| **Supersedes** | N/A                                              |
| **Relates to** | ADR-0005, ADR-0027, ADR-0054                     |
| **Project**    | DOMINUS                                          |

## Context

Until v1.1.0, schema migrations ran **at boot only**: the composition-root
runs `assertSchemaCompatible(applied, manifest)` and then `runMigrations()`
before health checks. The deploy pipeline (ADR-0054 era, migration gate)
rolls the new image and, on failure, auto-rolls the previous one — with the
gate blocking that rollback whenever the database is ahead of the old image's
manifest. That covers the *post-migration* failure case (release booted,
migrated, then failed health): the rollback is refused and the operator
restores from PITR or deploys the fixed release.

The remaining gap is the **migration itself failing mid-roll**:

- The new image boots, starts migrating, and dies (buggy DDL, connection
  timeout, container OOM, hung migration). The rollout then observes a failed
  health check and attempts the rollback path — at which point the gate has
  to *probe the database* to decide whether the rollback is even allowed.
- A partially applied migration set leaves the database in an ambiguous
  state: the old image cannot boot (ahead-of-manifest), the new image is
  gone, and the operator must manually determine what ran.
- Migration time is unbounded during a rollout: a hung migration occupies
  the health-check window and the rollout's own timeout, conflating "slow
  migration" with "dead migration" and making the failure diagnosis slower.

The same problem exists in reverse for the community edition (SQLite): the
image that migrates is the image that boots, and a failed local upgrade has
no rollback path at all.

## Decision Drivers

1. **Fail fast, fail before the roll** — migration failures should be
   detected *before* any image is rolled, while the old image is still
   serving; aborting a deploy must not require a PITR restore for the common
   case (the migration step itself failing).
2. **Bounded migration time** — a hung migration must be killed and reported
   after a dedicated timeout, not left to consume the rollout's window.
3. **Target image correctness** — the migration must run against the exact
   image being deployed (its manifest and migration code), never a floating
   or previous tag.
4. **Single control flow** — one self-contained script the deploy workflow
   runs, consistent with the existing migration gate, with behaviour tests
   like `test-rollout.sh`.
5. **Additive-first compatibility (migration-policy)** — the old image keeps
   serving while migrations apply; this is safe only because every migration
   is additive (ADR-0054 era policy §3), so the previous image's SQL never
   breaks against the post-migration schema.

## Considered Options

### Option A: Explicit migrate-before-roll deploy step (chosen)

A new `scripts/deploy/migrate.sh` runs *before* the roll: it pulls the
target image, runs the standalone migrate CLI (`dist/db/migrate-cli.js`) as
a `docker compose run --no-deps` container against the
live stack's database, and waits under `timeout` with a dedicated
`MIGRATE_TIMEOUT_SECONDS` (default 600 s). The CLI is a separate entrypoint
(`src/db/migrate-cli.ts`), not a main-CLI command, because the main CLI
boots dependencies — which migrate — before parsing.

- **Pros**: migration failure aborts the deploy with the old image still up
  (no rollback needed for the common case); hung migrations are killed and
  reported; the migrate container joins the stack network and uses the
  target image's own code; timeout is tunable per deploy; the workflow step
  is a single explicit `bash /tmp/dominus-migrate.sh` call.
- **Cons**: an extra deploy step and an extra script to maintain; the
  migrate container shares the api service's env (including `DATABASE_URL`
  interpolation); requires the image to be pullable before the roll (already
  guaranteed by the rollout's pull).

### Option B: Keep boot-only migrations (status quo)

Migrations run exclusively inside the new image's boot; the rollout timeout
covers both boot and migration.

- **Pros**: no new moving parts; one place where migrations run.
- **Cons**: a mid-roll migration failure lands in the ambiguous
  half-migrated state and forces gate probing during the rollback path;
  migration time is unbounded and conflated with boot/health time; the
  operator cannot distinguish "migration failed" from "app failed" from the
  deploy log alone.

### Option C: Migrate inside the roll step (post-roll migrate)

The rollout rolls the new image first, and migrations run as a separate
container *after* the roll succeeds.

- **Pros**: no wait for migration before serving.
- **Cons**: the new image boots against an un-migrated schema — the boot
  preflight (`assertSchemaCompatible`) is *designed* to refuse that, so the
  new image would fail closed at boot and the deploy would fail; it defeats
  the gate's entire premise.

## Decision

Option A. Releases get an explicit migrate-before-roll step: the workflow
runs `migrate.sh` after the gate and before the roll; on failure the deploy
aborts while the previous image keeps serving; on success the roll proceeds
and the new image's boot preflight passes. Exit contract of `migrate.sh`:
`0` — schema is up to date; nonzero — abort the deploy (do not roll).

## Consequences

### Positive

- The common failure case (migration step fails) no longer requires a PITR
  restore: the old image is still running, the deploy is aborted cleanly.
- Hung migrations are bounded by `MIGRATE_TIMEOUT_SECONDS` and killed, with
  a log line directing the operator to the migration-policy runbook.
- The migrate CLI is independently smoke-tested in CI (image-level and
  script-level behavior tests), so a broken migration path is caught before
  it reaches the node.

### Negative

- Deploys take longer: migration time is added before the roll.
- One more script and workflow step to keep in sync with the gate and the
  rollout (covered by `test-migrate.sh` behavior tests).

### Neutral

- The migrate container uses the api service's env block; `DOMINUS_IMAGE_TAG`
  is forced via the shell environment so the *target* image runs even though
  `.env` still holds the currently deployed tag.
- Boot-time migrations remain (community edition and first boot), so there
  are now two migration paths sharing one CLI implementation.