Skip to content

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_versions row 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 accept paths?: string[] (subset). When omitted, full rollback (current behavior).
  • New POST /api/v1/sites/{slug}/revert-window body {minutes: int = 30} → 202 with batch ID; fans out per-push rollback chained with the pre-apply restore.

UI

  • <JustAppliedToast> (NEW) — mounted in TopBar.tsx. Subscribes to a global event wpp:push-completed emitted 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}/rollback with optional paths.
  • <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

  1. 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.
  2. Disk-space alarm: surface storage used by pre-apply snapshots in /fleet/health. Out of scope v1.