Skip to content

Run migrations safely

workloads:
migrate:
role: Job
image: ghcr.io/acme/shop:1.4.0
command: ["./bin/migrate"]
dataEffect: Migration
deploymentPhase: PreRelease
needs: [{name: postgres, condition: healthy}]

dataEffect is required on a job, and it is what the rollback and abort gates read:

ValueMeaning
noneTouches no durable data. Rollback is unconstrained.
migrationChanges schema or data in a way a rollback may not undo.
destructiveRemoves data.
unknownYou cannot state it. Treated as the most cautious case.

deploymentPhase decides whether it runs during deployment: none (default), pre_release, or post_release. operatorRun independently controls explicit invocation (allowed or disabled); release-phase jobs default to disabled.

A job can write JSON or key=value data to $ONEBOX_RESULT_FILE using the onebox.run/job-result/v1alpha1 protocol. Provider-aware evidence records changed, provider, and ordered before_revisions / after_revisions.

#!/bin/sh
./bin/migrate
cat > "$ONEBOX_RESULT_FILE" <<EOF
{"changed": true, "provider": "atlas", "after_revisions": ["202607130001"]}
EOF
checks:
migrations:
- job: migrate
provider: atlas
applied_revisions: ["202607130001"]

This binds the expected provider and applied revisions to the evidence captured during the release, so a migration that silently applied something else fails verification instead of activating.

An environment can require a plan-bound report about an existing backup before a release with migration risk:

environments:
production:
policy:
require_migration_backup: true
migration_backup_maximum_age: 24h
require_migration_restore_test: true
migration_backup_key_material: [production-kms-key]

Write the plan-bound template, fill its placeholders from real backup-system results, then bind that exact report into the local confirmation and execution:

Terminal window
ob plan \
--out ob-plan.json \
--backup-report-out ob-backup-report.json
# Fill ob-backup-report.json from the backup system's actual results.
ob approve \
--plan ob-plan.json \
--backup-report ob-backup-report.json \
--out ob-approval.json
ob deploy \
--plan ob-plan.json \
--approval ob-approval.json \
--backup-report ob-backup-report.json

The report template is projected directly from the plan, with protected resources already filled in and deliberately invalid REPLACE-... values. It uses onebox.run/backup-report/v1alpha1 and records artifact, integrity, restore-test, and key-usability observations — never backup bytes or secrets.

FieldWhat it holds
schema_versiononebox.run/backup-report/v1alpha1
plan_digest, operation_digestexact executable-plan bindings copied by Onebox
application, environment, serverexact execution target copied by Onebox
reported_by, reported_atbounded reporter label and RFC 3339 observation time
resources[].resourcecopied from the plan’s migration_backup.resources
resources[].backup_idyour backup system’s identifier for the artifact
resources[].created_atRFC 3339 time the backup was taken
resources[].integrityartifact_digest, method, validated_at
resources[].restore_teststate is passed or not_tested. passed requires method, tested_at and a lowercase sha256: validation_digest; not_tested refuses all three
key_material[]one per entry in the plan’s required_key_material: name, backup_id, created_at, integrity, and usability (method, validated_at, validation_digest)

Unknown fields are refused, so a typo is named rather than ignored.

Terminal window
ob deploy --plan ob-plan.json --approval ob-approval.json \
--override-migration-backup "incident reason"

An audited override requires the exact plan’s strong or break-glass local confirmation. It is recorded, not silent.

Migrations and unknown data effects use the strong ceremony, where the operator types back what the plan acts on: the release ID for a deploy, the job name for a job run. That is the difference between confirming a routine deploy and confirming one that may not be reversible. A migration job asks whether or not approval policy is enabled.

deployment:
migrationPolicy: Manual # manual · auto · expand-only

expand-only is your promise, not a check Onebox performs. It declares that every migration in this project is additive — that a release running the old code against the new schema still works. Onebox never inspects your SQL; it takes the declaration and, on the strength of it, keeps the rollback gate open after a dataEffect: Migration job runs, so a failed verify can roll back automatically instead of halting.

That makes it the right setting for a genuinely expand-only workflow and the wrong one everywhere else. Under manual or auto, a migration job closes the gate: recovery stops and asks you, because rolling the application back over a schema it can no longer read is worse than staying put.