Run migrations safely
Declare the job
Section titled “Declare the job”workloads: migrate: role: job image: ghcr.io/acme/shop:1.4.0 command: ["./bin/migrate"] data_effect: migration when: pre_release needs: [{name: postgres, condition: healthy}]data_effect is required on a job, and it is what the rollback and abort
gates read:
| Value | Meaning |
|---|---|
none | Touches no durable data. Rollback is unconstrained. |
migration | Changes schema or data in a way a rollback may not undo. |
destructive | Removes data. |
unknown | You cannot state it. Treated as the most cautious case. |
when decides when it happens: manual (default), pre_release, or
post_release.
Report what actually happened
Section titled “Report what actually happened”A job can write JSON or key=value data to $OB_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/migratecat > "$OB_RESULT_FILE" <<EOF{"changed": true, "provider": "atlas", "after_revisions": ["202607130001"]}EOFVerify the revisions
Section titled “Verify the revisions”verifications: - migration_revisions: 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.
Gate on a backup report
Section titled “Gate on a backup report”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:
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.jsonThe backup report
Section titled “The backup report”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.
| Field | What it holds |
|---|---|
schema_version | onebox.run/backup-report/v1alpha1 |
plan_digest, operation_digest | exact executable-plan bindings copied by Onebox |
application, environment, server | exact execution target copied by Onebox |
reported_by, reported_at | bounded reporter label and RFC 3339 observation time |
resources[].resource | copied from the plan’s migration_backup.resources |
resources[].backup_id | your backup system’s identifier for the artifact |
resources[].created_at | RFC 3339 time the backup was taken |
resources[].integrity | artifact_digest, method, validated_at |
resources[].restore_test | state 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.
When you have to go anyway
Section titled “When you have to go anyway”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.
Approval ceremony
Section titled “Approval ceremony”When approval policy is enabled, migrations and unknown data effects use the strong ceremony, where the operator types the release ID. That is the difference between confirming a routine deploy and confirming one that may not be reversible.
Release-wide policy
Section titled “Release-wide policy”deployment: migration_policy: manual # manual · auto · expand-onlyexpand-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 data_effect: 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.