ADR-0015: Reversibility — pre-apply snapshot, sticky revert, granular rollback¶
Status: Proposed Date: 2026-05-31 Deciders: Ramón Kamibayashi + Claude Opus 4.7 Related: ADR-0010, ADR-0013, ADR-0014, pending-changes unified flow
Story: With the unified Apply-to-prod flow, every push to prod is an explicit choice. To make that choice psychologically easy, the operator must trust that any push is one click away from reversal. This ADR adds: automatic snapshot pre-apply, sticky "just applied — revert" affordance, inline diff before revert, per-file revert, and time-window revert.
Context¶
Today's rollback support:
- Per-push rollback exists (pushes.py rollback flow) but is buried in the Activity feed.
- No automatic state snapshot pre-apply — recovery requires the latest version save which may pre-date the apply by hours.
- No "did I just shoot myself in the foot? Undo!" affordance in the seconds after a push.
- No per-file revert (push touched 3 files → rollback all 3 or none).
Decision¶
5 features, all in one phase, single sub-agent.
| # | Feature | UX |
|---|---|---|
| C1 | Pre-apply auto-snapshot | Apply-all-to-prod creates an unnamed site_version row labeled Pre-apply YYYY-MM-DD HH:MM BEFORE the push hits the agent. Operator can restore from this in one click. |
| C2 | Sticky "Just applied — Revert" toast | After ANY successful push DONE, a sticky toast appears in TopBar for 10 min: "Pushed 3 files to hotelaldamagolf.com at 14:32 · [Undo]". Click Undo → confirm modal → triggers rollback. Reuses the existing RollbackBanner from ADR-0012 Phase 1 but elevates its position (top-right global, not site-local). |
| C3 | Inline diff before revert | Revert never fires blind. Click Undo → modal: "This restores 3 files + 0 DB rows from Pre-apply 14:32. Files: wp-config.php, footer.php, style.css. Continue? [Cancel] [Revert]". |
| C4 | Per-file revert | Within the revert modal, checkboxes per file. Apply subset. Backend: extend rollback flow to accept paths: [...] filter. |
| C5 | Time-window revert | "Revert all changes from the last N minutes" — picks every push DONE in the window AND the pre-apply snapshot before the earliest. One operation, one confirmation. |
Data model¶
Reuse existing site_versions table for C1 (auto-snapshots are just versions with source="pre_apply" and name="Pre-apply {timestamp}"). No alembic.
Add column pushes_v2.pre_apply_version_id uuid REFERENCES site_versions(id) (nullable) so we know which snapshot to revert TO when reverting a push. Alembic 021.
API¶
POST /api/v1/sites/{slug}/apply-all-to-prod— extend the existing endpoint to:- Create a
site_versionsrow first (source="pre_apply"). - Persist its ID on the resulting push rows (
pushes_v2.pre_apply_version_id). - Return it in the response.
POST /api/v1/sites/{slug}/pushes/{push_id}/rollback— already exists. Extend body to acceptpaths?: string[](subset). When omitted, full rollback (current behavior).- New
POST /api/v1/sites/{slug}/revert-windowbody{minutes: int = 30}→ 202 with batch ID; fans out per-push rollback chained with the pre-apply restore.
UI¶
<JustAppliedToast>(NEW) — mounted inTopBar.tsx. Subscribes to a global eventwpp:push-completedemitted by the PendingChangesModal success state (and the existing push code paths). Renders a sticky toast for the most recent push, with countdown timer. Click → opens<RevertModal>.<RevertModal>(NEW) — receives a push_id. Fetches push detail + pre_apply_version_id, lists files/DB-rows, shows checkboxes (per-file), confirm button. Submits to/pushes/{push_id}/rollbackwith optionalpaths.<TimeWindowRevertButton>— in the site detail header (next to PendingChangesBadge), only renders if there have been pushes in the last 60 min. Click → modal asking for minutes (slider 5/15/30/60) → confirm → POST/revert-window.
Edge cases¶
- Multiple pushes within the 10-min toast window → toast shows the MOST RECENT, but "Revert" link offers "Undo this push" vs "Undo last N pushes" sub-options.
- Push includes DB changes (from mass S&R): the pre-apply snapshot covers DB too (site_version captures both files + DB). Revert restores both.
- A revert push is itself logged as a push; reverts can be reverted (within reason). Don't recurse — second revert just makes a new push to the original state.
- The pre-apply snapshot adds latency to apply (~3-10 s for snapshot). Show "Snapshotting…" step in the apply progress.
Telemetry¶
Log revert_used events. Track time-to-revert (median expected: < 5 min after the bad push). If consistently high, the issue isn't the affordance.
Rollout¶
Single phase, single sub-agent. Order: 1. Alembic 021 + pre-apply snapshot wiring. 2. Sticky toast component. 3. Revert modal with per-file checkboxes. 4. Time-window revert.
Open questions¶
- Snapshot pruning: pre-apply snapshots accumulate fast (1 per push). Auto-prune snapshots > 30 days old AND not referenced by an active revert window. Default schedule retention to 30 days.
- Disk-space alarm: surface storage used by pre-apply snapshots in
/fleet/health. Out of scope v1.