Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,14 @@ gh workflow run database-migration.yml \
-f direction=down \
-f steps=2

# Rollback 1 migration on AWS prod (requires typed confirmation)
gh workflow run database-migration.yml \
-f cloud=aws \
-f environment=prod \
-f direction=down \
-f steps=1 \
-f confirm=rollback-prod

# Apply to all clouds
gh workflow run database-migration.yml \
-f cloud=all \
Expand All @@ -351,10 +359,11 @@ gh workflow run database-migration.yml \

### Safety Features

- **Validation** - Checks migration files exist
- **Production Warnings** - Warns on prod down migrations
- **Audit Trail** - Records all migrations
- **Step Control** - Apply specific number of migrations
- **Validation** - Checks migration files exist before running
- **Explicit steps required** - `direction=down` requires an explicit positive `steps` value; `steps=0` (the default, which would run `down -all` and drop the entire schema) is rejected
- **Production confirmation** - `direction=down` on `environment=prod` additionally requires typing `rollback-prod` in the `confirm` input; omitting or mistyping it blocks the run
- **Defense in depth** - each migrate job independently re-validates the positive-steps constraint, so a future validate regression cannot reach `down -all`
- **Audit Trail** - Records all migrations in the step summary

---

Expand Down
77 changes: 54 additions & 23 deletions .github/workflows/database-migration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,15 @@ on:
options: [up, down]
default: up
steps:
description: 'Number of migrations to apply/rollback (0 = all)'
description: 'Number of migrations to apply/rollback (0 = all; for direction=down an explicit positive value is required)'
required: false
type: number
default: 0
confirm:
description: 'Type "rollback-prod" to confirm a down migration on prod (ignored otherwise)'
required: false
type: string
default: ''
workflow_call:
inputs:
cloud:
Expand Down Expand Up @@ -83,6 +88,11 @@ jobs:

- name: Safety checks
id: check
env:
ENVIRONMENT: ${{ inputs.environment }}
DIRECTION: ${{ inputs.direction }}
STEPS: ${{ inputs.steps }}
CONFIRM: ${{ inputs.confirm }}
run: |
IS_SAFE=true

Expand All @@ -92,11 +102,25 @@ jobs:
IS_SAFE=false
fi

# Warn on production down migrations
if [[ "${{ inputs.environment }}" == "prod" ]] && [[ "${{ inputs.direction }}" == "down" ]]; then
echo "⚠️ WARNING: Attempting to rollback migrations on PRODUCTION"
echo "This operation is destructive and may cause data loss!"
# For production, we don't auto-fail, but require manual confirmation
# Down migrations must specify an explicit positive step count.
# steps=0 (the default) would run 'migrate down -all' and drop the
# entire schema, so it is rejected here for direction=down.
if [[ "$DIRECTION" == "down" ]]; then
if ! [[ "$STEPS" =~ ^[1-9][0-9]*$ ]]; then
echo "❌ direction=down requires an explicit positive 'steps' value (got '${STEPS:-<empty>}')."
echo "Rolling back ALL migrations at once is not supported by this workflow."
IS_SAFE=false
fi

# Production down migrations additionally require typed confirmation.
if [[ "$ENVIRONMENT" == "prod" ]]; then
echo "⚠️ WARNING: Attempting to rollback migrations on PRODUCTION"
echo "This operation is destructive and may cause data loss!"
if [[ "$CONFIRM" != "rollback-prod" ]]; then
echo "❌ Production rollback requires typing 'rollback-prod' in the 'confirm' input."
IS_SAFE=false
fi
fi
fi

# Check migration files
Expand All @@ -116,6 +140,11 @@ jobs:

echo "is_safe=$IS_SAFE" >> $GITHUB_OUTPUT

if [[ "$IS_SAFE" != "true" ]]; then
echo "Validation failed; refusing to run migrations."
exit 1
fi

- name: Display migration plan
run: |
echo "## Database Migration Plan" >> $GITHUB_STEP_SUMMARY
Expand Down Expand Up @@ -148,7 +177,7 @@ jobs:
persist-credentials: false

- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod

Expand Down Expand Up @@ -188,13 +217,13 @@ jobs:
migrate -path ${{ env.MIGRATIONS_PATH }} -database "$DB_URL" up ${{ inputs.steps }}
fi
else
if [[ "${{ inputs.steps }}" == "0" ]]; then
echo "Rolling back all migrations..."
migrate -path ${{ env.MIGRATIONS_PATH }} -database "$DB_URL" down -all
else
echo "Rolling back ${{ inputs.steps }} migration(s)..."
migrate -path ${{ env.MIGRATIONS_PATH }} -database "$DB_URL" down ${{ inputs.steps }}
# Defense in depth: validate already rejects this, but never run 'down -all'.
if ! [[ "${{ inputs.steps }}" =~ ^[1-9][0-9]*$ ]]; then
echo "❌ Refusing to roll back without an explicit positive 'steps' value."
exit 1
fi
echo "Rolling back ${{ inputs.steps }} migration(s)..."
migrate -path ${{ env.MIGRATIONS_PATH }} -database "$DB_URL" down ${{ inputs.steps }}
fi

- name: Get migration version
Expand Down Expand Up @@ -224,7 +253,7 @@ jobs:
persist-credentials: false

- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod

Expand Down Expand Up @@ -265,11 +294,12 @@ jobs:
migrate -path ${{ env.MIGRATIONS_PATH }} -database "$DB_URL" up ${{ inputs.steps }}
fi
else
if [[ "${{ inputs.steps }}" == "0" ]]; then
migrate -path ${{ env.MIGRATIONS_PATH }} -database "$DB_URL" down -all
else
migrate -path ${{ env.MIGRATIONS_PATH }} -database "$DB_URL" down ${{ inputs.steps }}
# Defense in depth: validate already rejects this, but never run 'down -all'.
if ! [[ "${{ inputs.steps }}" =~ ^[1-9][0-9]*$ ]]; then
echo "❌ Refusing to roll back without an explicit positive 'steps' value."
exit 1
fi
migrate -path ${{ env.MIGRATIONS_PATH }} -database "$DB_URL" down ${{ inputs.steps }}
fi

# Run Azure migrations
Expand All @@ -290,7 +320,7 @@ jobs:
persist-credentials: false

- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod

Expand Down Expand Up @@ -329,11 +359,12 @@ jobs:
migrate -path ${{ env.MIGRATIONS_PATH }} -database "$DB_URL" up ${{ inputs.steps }}
fi
else
if [[ "${{ inputs.steps }}" == "0" ]]; then
migrate -path ${{ env.MIGRATIONS_PATH }} -database "$DB_URL" down -all
else
migrate -path ${{ env.MIGRATIONS_PATH }} -database "$DB_URL" down ${{ inputs.steps }}
# Defense in depth: validate already rejects this, but never run 'down -all'.
if ! [[ "${{ inputs.steps }}" =~ ^[1-9][0-9]*$ ]]; then
echo "❌ Refusing to roll back without an explicit positive 'steps' value."
exit 1
fi
migrate -path ${{ env.MIGRATIONS_PATH }} -database "$DB_URL" down ${{ inputs.steps }}
fi

# Summary
Expand Down
Loading