Release Rollback
GHCR keeps every image by commit SHA and, for releases, by vX.Y.Z tag. build-images prunes
each image to its 10 most recent versions but never deletes a vX.Y.Z one, so every release is
a rollback target. Rollback redeploys a previous tag; Doctrine migrations are not rolled back
automatically.
Symptoms
- Post-deploy smoke test failed (
/api/healthzor/api/healthred after a deploy; an incident issue is opened automatically) - New error surge in Sentry whose first occurrence matches the deploy timestamp
- PWA reports a regression after a release (broken feature, JS errors, 5xx on a previously-working route)
Diagnosis
Identify the live and previous tags:
- GitHub → Actions →
Deployruns, orgit tag --sort=-creatordate | head./api/healthzdoes not expose the commit (SEC-011); on the VM,git -C /opt/bike-trip-planner describe --tagsshows the checked-out tag. - Note both
v*tags: the offending one and the rollback target.
Inspect the last few migrations (on the VM, dc alias from README.md):
dc exec php bin/console doctrine:migrations:list | tail -20
Check the Sentry releases page: confirm the new release is associated with the spike.
Procedure
-
Redeploy the previous tag (images already on GHCR, no rebuild). Either re-run the
deploy-prodjob of the previous tag'sDeployrun, or on the VM:/opt/bike-trip-planner/deploy-prod.sh <previous-tag>Both check out the tag and roll the stack through
btp-compose, which derivesPHP_IMAGE/PWA_IMAGE/PROVISIONER_IMAGEfrom the tag the checkout is on: the images always belong to the release whose compose files run them. -
Verify the smoke test:
curl -sS https://<prod-host>/api/healthz curl -sS https://<prod-host>/api/health | jq -
Handle migrations. Doctrine migrations are forward-only by default. Three scenarios:
- Additive migration only (new column, new table) — leave the schema as-is. The old image ignores the new column; verify there is no NOT NULL without default that would break inserts.
- Destructive migration shipped (dropped column, renamed table): the old image will crash. Revert the schema manually:
dc exec php bin/console doctrine:migrations:execute --down "DoctrineMigrations\\VersionYYYYMMDDHHMMSS"Only attempt this if a
down()exists; otherwise restore from the most recent PG-app backup (ADR-062).- Data migration (UPDATE rows) — generally non-reversible; assess data loss and decide whether to keep the new image patched-forward instead of rolling back.
-
Inform users (GitHub issue or PWA banner) if downtime exceeded 5 min. There is no public status page in beta.
-
Open a follow-up issue linking the failing PR. The PR template (
.github/PULL_REQUEST_TEMPLATE.md) asks the fix to link the error-tracking event ID and the incident issue.
Verification and follow-up
- Application back on the previous green SHA, smoke test green.
- The Sentry release page shows the regression confined to the rolled-back release.
- Issue auto-created by
incident-create.ymlis updated with the rollback timestamp and the linked offending PR. - Migration policy reviewed in the post-mortem: destructive migrations must follow the 2-release rule (add → migrate code → drop deprecated) per ADR-032.
References
- ADR-019 / ADR-061 — Deployment infrastructure (GitHub Actions SSH deploy +
docker compose -p prod) release-checklist.md— pre-release checks that should have caught itincident-template.md— post-mortem template