CI/CD Patterns¶
Patterns for running DBWarden migrations in automated pipelines.
Core principle¶
Run migrations from exactly one job. Serialize migration and deploy. Never run migrate in parallel across multiple agents or containers targeting the same database.
GitHub Actions¶
Minimal migration job¶
name: Deploy
on:
push:
branches: [main]
jobs:
migrate:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: uv add -e ".[migrations]"
- name: Check migration status
run: dbwarden status --database primary
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
- name: Apply migrations
run: dbwarden migrate --database primary
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
- name: Verify post-migration status
run: dbwarden status --database primary
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
deploy:
needs: migrate
runs-on: ubuntu-latest
steps:
- name: Deploy application
run: ...
The needs: migrate dependency ensures migrations are fully applied before the application starts.
Preventing concurrent migration runs¶
cancel-in-progress: false queues duplicate runs instead of cancelling mid-flight, which avoids leaving a stale lock on the database.
Multi-database migration¶
- name: Apply all migrations
run: dbwarden migrate --all
env:
PRIMARY_DATABASE_URL: ${{ secrets.PRIMARY_DATABASE_URL }}
ANALYTICS_DATABASE_URL: ${{ secrets.ANALYTICS_DATABASE_URL }}
Or migrate databases sequentially to control order:
- name: Migrate primary
run: dbwarden migrate --database primary
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
- name: Migrate analytics
run: dbwarden migrate --database analytics
env:
ANALYTICS_DATABASE_URL: ${{ secrets.ANALYTICS_DATABASE_URL }}
With backup before migration¶
- name: Apply migrations with backup
run: |
dbwarden migrate --database primary \
--with-backup \
--backup-dir ./migration-backups
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
- name: Upload backup artifact
uses: actions/upload-artifact@v4
with:
name: migration-backup-${{ github.sha }}
path: ./migration-backups/
retention-days: 30
GitLab CI¶
stages:
- migrate
- deploy
migrate:
stage: migrate
image: python:3.12
script:
- uv add -e ".[migrations]"
- dbwarden status --database primary
- dbwarden migrate --database primary
- dbwarden status --database primary
variables:
DATABASE_URL: $DATABASE_URL # set in GitLab CI/CD settings as masked variable
resource_group: production-database # prevents concurrent runs
deploy:
stage: deploy
needs: [migrate]
script:
- ...
resource_group serializes the migrate job across concurrent pipelines.
Sandbox testing in PR pipelines¶
Instead of running against a shared staging database, use --sandbox
to apply migrations to a temporary in-memory SQLite database or a
Docker-backed instance. This isolates PR checks from each other:
sandbox-check:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- run: uv add -e ".[migrations,testcontainers]"
- name: Apply migrations to sandbox
run: dbwarden migrate --sandbox --database primary
The sandbox starts a fresh database, applies all pending migrations, reports results, and tears down. It never touches the real database.
Dry-run check in PR pipelines¶
Use --dry-run to preview SQL without any database access:
migration-check:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- run: uv add -e ".[migrations]"
- name: Check for pending migrations
run: dbwarden status --database primary
env:
DATABASE_URL: ${{ secrets.STAGING_DATABASE_URL }}
This surfaces "pending migrations exist" warnings in PR checks without modifying the database.
For a deeper check that validates the SQL actually runs, chain
--dry-run before --sandbox:
- name: Preview SQL
run: dbwarden migrate --dry-run --database primary
- name: Validate in sandbox
run: dbwarden migrate --sandbox --database primary
Plan output in deploy pipelines¶
The make-migrations --plan flag prints the generated migration plan
as JSON without writing files. Use it in deploy pipelines to capture
what would be generated as a deploy artifact:
- name: Generate migration plan
run: dbwarden make-migrations --database primary --plan > plan.json
- name: Upload plan artifact
uses: actions/upload-artifact@v4
with:
name: migration-plan-${{ github.sha }}
path: plan.json
The plan JSON includes detected changes, operation counts, and auto-generated migration names.
Exit codes¶
DBWarden exits non-zero on:
- Migration failure
- Checksum mismatch
- Lock acquisition failure
- Configuration error
CI pipelines treat non-zero as job failure by default. No extra configuration needed.
Recommendations¶
- Store
DATABASE_URLas an encrypted secret, not a plain environment variable - Archive migration output logs as artifacts for audit trails
- Use
dbwarden historyoutput as a post-migration artifact - Run
dbwarden statusbefore and aftermigrate; before confirms what will run, after confirms nothing is pending
See also: Safe Deployment | Credentials and Secrets | Migration Locking | Cookbook: Offline & CI