Skip to content

Rollback โ€‹

Authoring recovery next to the step it undoes, and running one consolidated rollback when things go wrong.

Rollback โ€‹

Define rollback commands as an ordered list per step. They run when the operator presses [r] or chooses rollback after a failed verify (see Running a rollback):

yaml
steps:
  - name: Deploy
    session: execution
    command: kubectl apply -f deployment.yaml
    rollback:
      - command: kubectl rollout undo deployment/web
        session: execution
      - command: kubectl delete pod -l app=web --force
        session: execution

A step-level rollback step is structurally just like a normal step: it may also carry an optional name, nested sub_steps for a multi-part rollback, and even foreach/matrix (see below) โ€” all of which render recursively in every manual format. Both entries above render (not just the first):

yaml
    rollback:
      - name: Roll back deployment
        instruction: Undo the deployment in order.
        sub_steps:
          - command: kubectl rollout undo deployment/web
          - instruction: Confirm the previous version is serving traffic.
            expect:
              contains: rolled back

After rolling back step N, SAMARITAN offers to walk back previous steps too.

If no rollback: is defined, the operator is prompted to intervene manually.

Operation-level rollback โ€‹

You can also declare a single top-level rollback: block that describes the rollback procedure for the whole operation (distinct from the per-step rollback above). It supports automatic, conditions, and an ordered list of steps. A rollback step is structurally just like a normal step: a full step body (command/script/instruction/expect/pic/reviewer/evidence) plus an optional name and nested sub_steps for multi-part rollbacks:

yaml
steps:
  - name: Deploy
    command: kubectl apply -f deployment.yaml

# Global rollback plan for the whole operation
rollback:
  automatic: false
  conditions:
    - health_check_failure
    - error_rate_spike
  steps:
    - command: kubectl rollout undo deployment/web
      expect:
        contains: rolled back
    # A multi-part rollback step broken into ordered sub-steps
    - name: Decommission canary
      sub_steps:
        - command: kubectl scale deployment/web-canary --replicas=0
        - instruction: Confirm no canary pods remain.
          expect:
            no_line_contains: web-canary

This renders as a ๐Ÿ”„ Rollback Plan section in every generated manual โ€” Markdown (multi-env table and single-env headings), Confluence ADF/JSON, and Confluence wiki markup โ€” showing the automatic flag, conditions, and each rollback step per environment. Nested sub_steps render recursively as Rollback Step N, N.M, N.M.K, โ€ฆ (see examples/rollback-with-substeps.yaml).

Loop a rollback step with foreach/matrix โ€‹

Because a rollback step is a normal step, it supports the same foreach/matrix expansion. One rollback definition expands at parse time into one rollback step per combination โ€” for both the operation-level plan and per-step rollback::

yaml
rollback:
  steps:
    - name: Restart web
      foreach:
        matrix:
          REGION: [us-east-1, eu-west-1]
          TIER: [web, api]
      command: kubectl rollout restart deployment/web -n ${REGION}-${TIER}

This yields four rollback steps (Restart web (us-east-1, web), โ€ฆ), each rendered in every format. A step may also carry multiple rollback entries (authored or foreach-expanded); all of them render. See examples/rollback-with-foreach.yaml.

Compose a rollback from a reusable file with uses:/with: โ€‹

Because a rollback step is a normal step, it also supports uses:/with: file composition. A rollback entry that references a file with uses: is expanded at parse time into that file's steps (with every ${VAR} filled from with:), producing a flat list of rollback steps. This works for both the operation-level rollback.steps[] plan and per-step Step.rollback[], so you can keep one canonical "restore service" procedure in a shared file and reuse it as the rollback for many operations:

yaml
steps:
  - name: Deploy web service
    command: kubectl apply -f web.yaml
    rollback:
      - uses: ./templates/rollback-restore.yaml   # expands into the file's steps
        with:
          SERVICE: web

rollback:
  automatic: false
  steps:
    - name: Announce rollback
      instruction: Post in #incidents that a rollback is starting.
    - uses: ./templates/rollback-restore.yaml     # plain + imported steps coexist
      with:
        SERVICE: api

The same required-variable rule as normal-step uses: applies: every ${VAR} in the imported file must be supplied via with: (or a template default). A uses:-imported rollback step can itself contain foreach/matrix, which expands after import. See examples/rollback-with-uses.yaml (and examples/templates/rollback-restore.yaml).

Group per-step rollbacks into the global rollback (aggregate_step_rollbacks) โ€‹

Set aggregate_step_rollbacks: true on the operation-level rollback: to group every step's own rollback into the global plan. After the explicit steps: above, SAMARITAN appends each step's rollback in reverse step order (most-recently-completed step first), each labelled with the step it undoes (โ†ฉ Rollback for "Deploy app"). Only the step order is reversed: a step with several rollback entries keeps them in the order you wrote them, so restore then verify the restore stays restore-then-verify. This lets you author each undo next to the step it reverses and still see โ€” or run โ€” one consolidated recovery:

yaml
rollback:
  automatic: false
  aggregate_step_rollbacks: true   # group step.rollback[] into this plan (reverse order)
  steps:
    - name: Page the on-call SRE
      instruction: Notify the on-call engineer before rolling back.

In the generated manuals, this flag also centralizes the per-step rollbacks so the main step flow stays readable. Without it, each step's rollback is repeated up to three times โ€” inline after the step, in a Rollback Procedures section, and folded into the Rollback Plan. With it on:

  • the inline block after each step collapses to a compact jump-link โ€” โ†ฉ **Rollback:** [Rollback for "<step>" โ†“](#rollback-for-<step>) โ€” pointing at that step's folded entry in the bottom Rollback Plan. The label names the step (not a numeric position) so the link text and its target heading share one searchable phrase;
  • the folded entry in the Rollback Plan is the jump target: a renderer-safe heading slug in Markdown (### Rollback for "<step>", so the link jumps in sanitized previews like GitHub โ€” a hand-authored <a id> would be rewritten to user-content-โ€ฆ and never resolve), a {anchor} macro in Confluence wiki, an anchor-macro node in ADF;
  • the now-redundant Rollback Procedures section is dropped, so the full rollback content lives in exactly one place.

The full recovery therefore reads top-to-bottom as one Rollback Plan, and each step links down to its own entry. Applies to every output format (Markdown multi-/single-env, Confluence wiki, ADF). With the flag off, rendering is unchanged.

During samaritan run, every step prompt offers [g] global rollback: it previews the consolidated recovery (explicit plan steps + the completed steps' rollbacks, reversed), asks for confirmation (a single y), runs it as described in Running a rollback, then aborts the operation โ€” a full rollback ends forward progress, and the session is resumable. See examples/global-rollback-aggregated.yaml.

Running a rollback โ€‹

How [r] / [g] deliver rollback commands depends on the run mode:

RunDelivery
sidecar (default) โ€” --attach, [t], or spawn-own sessions:Operator-driven, like a forward step (below)
manual with no tmuxOperator-driven, without [p]/[v]
non-sidecar with spawn-own sessions:samaritan sends each command to its tmux session (with Enter) and waits for the prompt

Operator-driven rollback walks the entries one at a time โ€” โ†ฉ Rollback 1/3: <name>, then the instruction and the ${VAR}-resolved command:

KeyAction
โ†ตdone โ€” I ran it
ppaste into the attached pane (bracketed paste, no Enter โ€” same as the forward [p], including its refusal while an earlier paste is still un-run on the pane's input line)
yafter p: samaritan presses Enter in the pane, only while the pane still holds that paste unrun (same guard as the forward [y]), then waits for it to finish and verifies the entry's expect
ccopy the command
vverify the entry's expect against the pane output since it was shown
sskip this entry
aabort the rest of the rollback

The run report's Rollback Events records what actually happened. Each entry is listed as โœ… run by operator / โญ skipped / โ›” not run, with pasted into pane, Enter sent from samaritan [y], and the verify โœ…/โŒ result where they apply. The status is โœ… completed only when every entry was confirmed run. Otherwise it is โš ๏ธ partial or โŒ NOT executed. A rollback that was only shown is never recorded as a success. Commands in the audit log are resolved (${REGION} โ†’ us-east-1), so the log is replayable.

Finishing the last step does not take rollback away. When anything can be rolled back, the run stops on an All steps done screen (โ†ต finish ยท r roll back a step ยท g global rollback) before it exits.

Released under the ISC License.