CI/CD Integration¶
How to integrate Queen migrations into your deployment pipeline.
Overview¶
Queen uses a code-first approach — migrations are Go code compiled into your application. This differs from file-based tools (goose, golang-migrate, flyway) where migrations are separate SQL files.
Key Characteristics¶
Code-first (Queen): - Migrations are Go structs registered at compile time - Type safety and compile-time validation - Requires rebuild when migrations change - Migrations versioned with application code
File-based (goose, golang-migrate):
- Migrations are separate .sql files
- Can use the same binary with different migration files
- No rebuild needed for new migrations
- Migrations can be managed separately from code
When Queen Works Well¶
✅ Queen is ideal for:
- Go applications where migrations deploy with the app
- Docker/Kubernetes deployments with image rebuilds
- Monorepo setups with migrations in the same repository
- Teams that prefer type-safe, testable migrations
- Applications using Go functions for complex migrations
When Queen May Not Fit¶
❌ Queen may not be ideal for:
- Pure SQL workflow without Go involvement
- Separate migration runner as a standalone service
- DevOps teams without access to application code
- Legacy systems requiring file-based migration tools
- Workflows where migrations are added without rebuilding
Deployment Patterns¶
Pattern 1: Migrations Embedded in Application¶
Most common pattern. Application runs migrations on startup.
Structure:
myapp/
├── cmd/
│ └── app/
│ └── main.go # App with embedded migrations
├── migrations/
│ └── register.go # Migration registration
└── internal/
└── ...
Application code:
package main
import (
"context"
"github.com/honeynil/queen"
"myapp/migrations"
)
func main() {
q := queen.New(driver)
migrations.Register(q)
// Run migrations on startup
if err := q.Up(context.Background()); err != nil {
log.Fatal("migrations failed:", err)
}
// Start application
startServer()
}
CI/CD (GitHub Actions):
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.24'
- name: Build application
run: go build -o app ./cmd/app
- name: Deploy
run: |
# App runs migrations on startup
./app
Advantages: - Single binary deployment - Migrations always match application version - Automatic rollback if migrations fail
Disadvantages: - Application won't start if migrations fail - Can't run migrations separately for debugging
Pattern 2: Separate Migration Binary¶
Build separate CLI binary for migrations. Run before deploying application.
Structure:
myapp/
├── cmd/
│ ├── app/
│ │ └── main.go # Application server
│ └── queen/
│ └── main.go # Migration CLI
└── migrations/
└── register.go
Migration CLI:
// cmd/queen/main.go
package main
import (
"github.com/honeynil/queen/cli"
"myapp/migrations"
)
func main() {
cli.Run(migrations.Register)
}
CI/CD (GitLab CI):
stages:
- build
- migrate
- deploy
build:
stage: build
script:
- go build -o queen ./cmd/queen
- go build -o app ./cmd/app
artifacts:
paths:
- queen
- app
migrate:
stage: migrate
script:
- ./queen up --yes
dependencies:
- build
deploy:
stage: deploy
script:
- deploy-app ./app
dependencies:
- build
Advantages: - Migrations run before app deployment - Can retry migrations independently - Better separation of concerns
Disadvantages: - Two binaries to manage - Migrations must be rebuilt for every change
Pattern 3: Docker Multi-Stage Build¶
Build migrations and application in separate Docker stages.
Dockerfile:
# Stage 1: Build migrations CLI
FROM golang:1.24 AS migration-builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o queen ./cmd/queen
# Stage 2: Build application
FROM golang:1.24 AS app-builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o app ./cmd/app
# Stage 3: Runtime image
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
# Copy both binaries
COPY --from=migration-builder /app/queen .
COPY --from=app-builder /app/app .
# Run migrations, then start app
CMD ["sh", "-c", "./queen up --yes && ./app"]
CI/CD (GitHub Actions with Docker):
name: Docker Deploy
on:
push:
branches: [main]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: docker build -t myapp:latest .
- name: Push to registry
run: docker push myapp:latest
- name: Deploy to Kubernetes
run: kubectl set image deployment/myapp myapp=myapp:latest
Advantages: - Single Docker image with everything - Consistent build environment - Easy rollback (previous image)
Disadvantages: - Larger image size (two binaries) - Migrations run inside container
Docker Examples¶
Embedded Migrations (Single Binary)¶
FROM golang:1.24 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o app ./cmd/app
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/app .
CMD ["./app"]
Application runs migrations on startup.
Separate Init Container (Kubernetes)¶
Deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
template:
spec:
initContainers:
- name: migrations
image: myapp:latest
command: ["/root/queen", "up", "--yes"]
env:
- name: QUEEN_DSN
valueFrom:
secretKeyRef:
name: db-credentials
key: dsn
containers:
- name: app
image: myapp:latest
command: ["/root/app"]
Migrations run in init container before app starts.
CI/CD Platform Examples¶
GitHub Actions¶
Full example with testing:
name: CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: testdb
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.24'
- name: Run tests
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/testdb?sslmode=disable
run: |
go test ./...
- name: Build migration CLI
run: go build -o queen ./cmd/queen
- name: Test migrations
env:
QUEEN_DSN: postgres://postgres:postgres@localhost:5432/testdb?sslmode=disable
run: |
./queen check --ci
./queen up --yes
./queen status
./queen validate
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Build and push Docker image
run: |
docker build -t myapp:${{ github.sha }} .
docker push myapp:${{ github.sha }}
- name: Deploy
run: kubectl set image deployment/myapp myapp=myapp:${{ github.sha }}
GitLab CI¶
stages:
- test
- build
- deploy
variables:
POSTGRES_DB: testdb
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
test:
stage: test
image: golang:1.24
services:
- postgres:16
variables:
DATABASE_URL: "postgres://postgres:postgres@postgres:5432/testdb?sslmode=disable"
script:
- go test ./...
- go build -o queen ./cmd/queen
- ./queen check --ci
- ./queen up --yes
- ./queen validate
build:
stage: build
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
only:
- main
deploy:
stage: deploy
script:
- kubectl set image deployment/myapp myapp=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
only:
- main
CircleCI¶
version: 2.1
orbs:
go: circleci/go@1.7
jobs:
test:
docker:
- image: cimg/go:1.24
- image: postgres:16
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: testdb
steps:
- checkout
- go/load-cache
- go/mod-download
- go/save-cache
- run:
name: Run tests
command: go test ./...
- run:
name: Build migration CLI
command: go build -o queen ./cmd/queen
- run:
name: Test migrations
environment:
QUEEN_DSN: postgres://postgres:postgres@localhost:5432/testdb?sslmode=disable
command: |
./queen check --ci
./queen up --yes
./queen validate
deploy:
docker:
- image: cimg/base:stable
steps:
- checkout
- setup_remote_docker
- run:
name: Build and deploy
command: |
docker build -t myapp:latest .
docker push myapp:latest
workflows:
version: 2
test-and-deploy:
jobs:
- test
- deploy:
requires:
- test
filters:
branches:
only: main
Best Practices¶
1. Exit Codes for CI/CD¶
Use queen's exit codes for pipeline control:
# Fail pipeline if gaps detected
queen check --ci || exit $?
# Allow pending migrations but fail on gaps
queen check --no-gaps --yes
Exit codes:
- 0 — Success
- 1 — General error
- 2 — Configuration error
- 3 — Validation failed
- 4 — Gaps detected
- 5 — Pending migrations (with --no-pending)
2. Environment-Specific Config¶
Use .queen.yaml for different environments:
development:
driver: postgres
dsn: "postgres://localhost/myapp_dev?sslmode=disable"
staging:
driver: postgres
dsn: "postgres://staging-db/myapp?sslmode=require"
production:
driver: postgres
dsn: "postgres://prod-db/myapp?sslmode=verify-full"
require_confirmation: true
require_explicit_unlock: true
3. Avoid Confirmation Prompts¶
Always use --yes flag in CI/CD:
4. Validation Before Deploy¶
Run validation checks in CI:
# Check for gaps and pending migrations
queen check --ci
# Validate migration checksums
queen validate
# Dry run to preview changes
queen plan
5. Rollback Strategy¶
Plan for rollback scenarios:
# Tag Docker images with version
docker tag myapp:latest myapp:v1.2.3
# If deployment fails, rollback
kubectl rollout undo deployment/myapp
# Or deploy previous image
kubectl set image deployment/myapp myapp=myapp:v1.2.2
6. Database Credentials¶
Never hardcode credentials. Use environment variables or secrets:
# Kubernetes secret
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
type: Opaque
stringData:
dsn: postgres://user:password@host/db
Trade-offs and Limitations¶
Advantages of Code-First¶
✅ Type safety — Compile-time validation catches errors early
✅ Versioning — Migrations version with application code
✅ Testing — Use queen.NewTest() for migration tests
✅ Go functions — Complex migrations with Go logic
✅ Single artifact — One binary for app + migrations
Limitations¶
❌ Rebuild required — New migrations require recompiling ❌ No pure SQL workflow — Must use Go to register migrations ❌ DevOps access — Team needs Go tooling and code access ❌ No dynamic loading — Can't add migrations without rebuild
Workarounds¶
For SQL-first teams:
Use queen import to convert SQL files to Go code:
# Convert goose SQL files to Queen Go code
queen import --from goose ./sql-migrations
go build -o queen ./cmd/queen
./queen up
For separate migration runner:
Build a "thick" migration CLI with all migrations:
// cmd/migrations/main.go
package main
import (
"github.com/honeynil/queen/cli"
"mycompany/all-migrations" // All migrations from all services
)
func main() {
cli.Run(all_migrations.Register)
}
Deploy this as a separate service.
Next Steps¶
- Gap Detection — Handle missing migrations in CI/CD
- Diagnostics — Commands for CI/CD validation
- Testing — Write tests for migrations
- Docker Deployment — Production deployment patterns