Rehearsed rollback with human production authority
PLA rehearsed rollback pipeline reverses a harmful production change with one protected command. A versioned contract names the target release, preconditions and recovery checks, and a deploy job with short-lived credentials executes it. Coding agents draft the decision and verify recovery, while a named human, or a deterministic alert rule for pre-approved low-risk services, starts the rollback.
It is 16:40 on a Friday. The release an agent wrote at lunch passed every check, and checkout errors have just tripled. Someone opens a terminal to run git revert, someone else asks whether last week’s migration is reversible, and nobody remembers when the rollback script last ran. This page is for the developer who owns the release pipeline and the tech lead who carries the pager. It turns rollback from an improvisation into the most rehearsed path you have.
Scorecard question (Q21): How are rollbacks managed if a deployment introduces a regression?
Maximum-score answer: Rehearsed single-command rollback exposed via scoped MCP tools, capable of being triggered deterministically when control bands breach.
“Scoped” is the operative word. On this page the agent’s MCP tools read deployment state and draft the decision; the command itself runs in a protected job, and it is started by a person or by a deterministic alert rule, never by a model’s judgment. That keeps the production approval gate intact during an incident, which is when people most want to bypass it.
What a rehearsed rollback pipeline gives you
Section titled “What a rehearsed rollback pipeline gives you”- A rollback contract in YAML that sits next to the deploy workflow and names the target, preconditions, abort conditions and recovery checks.
- A GitHub Actions rollback job that runs through the same approval gate as a deploy, with a release ID as its only required input and no standing credentials.
- A read-only agent setup for Claude Code, Codex and Cursor that drafts the rollback decision record while the incident channel is still assembling.
- Three copy-paste prompts: the decision record, the recovery verdict and the rehearsal audit.
- A rehearsal routine and a readiness checklist that prove the path works without anyone reading the script line by line.
Why rollback matters more once agents write the code
Section titled “Why rollback matters more once agents write the code”Agents raise the volume of change, and every change is a chance to break production. The 2025 DORA report found both sides of that trade: “we observe a positive relationship between AI adoption on both software delivery throughput and product performance”, and “AI adoption does continue to have a negative relationship with software delivery stability” (Google Cloud / DORA, 2025-09-23). DORA names the mechanism as missing control systems such as “strong automated testing, mature version control practices, and fast feedback loops”.
A rehearsed rollback is one of those controls. It is also what lets you stop reading every diff: when a bad change costs minutes of partial impact instead of an evening of hotfixes, you can trust tests, progressive delivery and recovery checks to carry the risk.
Should you roll back, roll forward or turn off a flag?
Section titled “Should you roll back, roll forward or turn off a flag?”A rollback is not automatically the safe option. Decide by what the change touched, and write the answer into the contract before the incident, not during it.
| The harmful change… | Prefer | Why |
|---|---|---|
| Sits behind a feature flag | Turn the flag off | Takes effect in seconds, needs no deploy, and leaves the release in place |
| Is stateless code or config, and the previous release is retained | Roll back to the previous immutable release | Fastest full reversal; this page’s main path |
| Includes an expand-only migration (new nullable column, new table) | Roll back the code; leave the schema | The old release ignores the new schema |
| Includes a contract migration (drop, rename, type change) or rewrote data | Roll forward with a fix, or run a tested data-restore plan | The old release cannot read the new data |
| Changed queue message formats, cache formats or a public API that clients already use | Roll forward, or roll back only if both formats are readable | Old consumers choke on messages the new release already produced |
| Shipped to mobile clients | Flag off server-side and roll forward | Installed apps cannot be rolled back |
Canary aborts and flag kill switches belong to progressive delivery. This page covers the release-level rollback that stays in reserve when those are not enough.
How do you build a one-command rollback?
Section titled “How do you build a one-command rollback?”-
Write the rollback contract next to the deploy workflow. Commit it as
deploy/rollback.yml, and review it like code. The example below is illustrative; replace the command and checks with the tested interface of your platform.# deploy/rollback.yml — reviewed by the service ownerservice: checkout-apirisk_class: standard # low | standard | high, as in progressive deliverytarget: previous-retained-release # an immutable, signed release ID, never a branchcommand: ./scripts/rollback.sh --release "$RELEASE_ID"dry_run: ./scripts/rollback.sh --release "$RELEASE_ID" --dry-runexecutor_identity: github-environment:production # the same gate as a deployapprover_role: incident-commanderauto_trigger: # deterministic, never model-decidedallowed: false # true only for low-risk, stateless servicesalert_rule: checkout-error-budget-burn-fastpreconditions:- current release equals EXPECTED_RELEASE- target release artifact is retained and its signature verifies- no migration since target is marked "contract"abort_conditions:- target release has an active security block- a data migration since target is not backward compatiblerecovery_checks:- error-budget fast-burn alert clears and stays clear for 10 minutes- checkout synthetic passes from two regions- p95 latency within the pre-incident bandlast_rehearsal: 2026-09-18 # updated by the rehearsal job, not by handevidence: incident record + deployment audit logThree fields do most of the work.
preconditionsandabort_conditionsmake the risky cases from the decision table machine-checkable.recovery_checksdefines “recovered” before anyone is under pressure.last_rehearsallets CI refuse a release when the rollback path has gone stale. -
Make the script refuse, not guess.
scripts/rollback.shchecks every precondition and abort condition before it changes anything, and exits non-zero with the reason when one fails.--dry-runruns the same checks and prints the plan. The script never picks its own target: it takes one release ID and verifies it. -
Run the script only inside a protected job. Add a
workflow_dispatchworkflow that runs in the sameproductionenvironment as your deploy, so the required reviewer, the OIDC trust policy and the audit trail from Production approval as a system boundary apply unchanged..github/workflows/rollback.yml name: rollbackon:workflow_dispatch:inputs:release_id:description: Immutable release ID to restorerequired: truetype: stringtarget_env:description: Environment to roll backrequired: truetype: choiceoptions: [staging, production]default: productionpermissions:contents: readconcurrency:group: ${{ inputs.target_env }}-deploy # the deploy workflow's group: never race a deploycancel-in-progress: falsejobs:dry-run:runs-on: ubuntu-latestpermissions:contents: readid-token: write # read-only cloud credentials for the precondition checkssteps:- uses: actions/checkout@v7with:persist-credentials: false# Add a read-only OIDC login here (a role that can list releases and read the registry,# nothing more), so the preconditions can check the current release and the target artifact.- name: Validate input and preconditionsenv:RELEASE_ID: ${{ inputs.release_id }} # passed as env, never interpolated into the scriptrun: |[[ "$RELEASE_ID" =~ ^[a-z0-9._-]{1,64}$ ]] || { echo "invalid release id"; exit 1; }./scripts/rollback.sh --release "$RELEASE_ID" --dry-run | tee -a "$GITHUB_STEP_SUMMARY"rollback:needs: dry-runruns-on: ubuntu-latestenvironment: ${{ inputs.target_env }} # waits for that environment's required reviewerpermissions:contents: readid-token: write # short-lived cloud credentials after approvalsteps:- uses: actions/checkout@v7with:persist-credentials: false# Add your cloud's OIDC login step here; each role trusts only its own environment subject.- name: Roll backenv:RELEASE_ID: ${{ inputs.release_id }}run: ./scripts/rollback.sh --release "$RELEASE_ID"The
target_envinput exists for rehearsals: the same file runs againststaging, whose environment has its own required reviewer and its own OIDC role. The dry run gets only read-only credentials and lands in the job summary, so the approver sees the plan on the same page as the Review deployments button. No agent step runs in this workflow.The shared
concurrencygroup keeps a rollback from running alongside a deploy, but it does not queue them: GitHub Actions keeps at most one pending run per group. A later run in the same group cancels a pending one, so pause automated deploys first (gh workflow disable deploy.yml) and confirm the rollback run is queued, not cancelled.Give
production(andstaging) the same deployment-branch rule, default branch only, so the approved job always runs the reviewedscripts/rollback.sh. Without it, anyone with write access can dispatchrollback.ymlfrom their own branch, and the reviewer approves a script nobody reviewed. -
Wire the deterministic trigger, only where the contract allows it. The
productionenvironment always waits for its required reviewer, so an alert that dispatchesrollback.ymlonly queues a request: the alert pages the on-call engineer, who approves. That is the default, and it is the only path for a service whose contract saysauto_trigger.allowed: false. For a service whose contract saystrue, give the automatic path its own door instead of weakening the shared one:- A second workflow,
.github/workflows/rollback-auto.yml, with the same steps asrollback.yml, bound to a second environment,production-auto. production-autohas no required reviewer. Its protection is a deployment-branch rule that allows only the default branch, and a cloud OIDC trust condition that accepts only theenvironment:production-autosubject, and only for the roles of the pre-approved services.- The alerting system (not an agent) dispatches it through a GitHub App whose token no person or agent holds, passing the release ID of the previous successful deployment. The job refuses any other caller and any service whose contract does not allow it.
# .github/workflows/rollback-auto.yml: only the differences from rollback.ymlconcurrency:group: production-deploy # still never races a deployjobs:rollback:if: github.actor == 'alert-bridge[bot]' # the alerting system's GitHub Appenvironment: production-auto # no reviewer: branch rule + OIDC trust onlysteps:# checkout, read-only login and dry run as in rollback.yml, then:- name: Refuse unless the contract allows automatic rollbackrun: test "$(yq '.auto_trigger.allowed' deploy/rollback.yml)" = "true"The branch rule matters: without it, anyone who can push a branch could edit the
if:line and reach the unreviewed environment. Deterministic anomaly detection covers how to make the alert rule replay-tested and versioned. - A second workflow,
-
Make CI enforce rehearsal freshness. Add a step to the deploy workflow, before the deploy job, that reads
last_rehearsalfrom the contract and fails when it is older than your policy, for example 30 days.yqis preinstalled on GitHub’s Ubuntu runners.The gate also blocks the roll-forward hotfix that the decision table recommends during an incident, so give it one documented exit: a deploy dispatched with an incident ID skips the check, records the ID in the job summary and opens a rehearsal ticket automatically.
.github/workflows/deploy.yml on:push:branches: [main]workflow_dispatch:inputs:incident_id:description: Incident ID for an emergency roll-forward (skips the freshness gate)required: falsetype: string# ...in the job that gates the deploy (it needs `issues: write` for the ticket):- name: Block the release when the rollback rehearsal is staleif: ${{ !inputs.incident_id }}run: |last=$(yq '.last_rehearsal' deploy/rollback.yml)if [[ $(date -d "$last" +%s) -lt $(date -d '-30 days' +%s) ]]; thenecho "Rollback last rehearsed on $last; rehearse before shipping."; exit 1fi- name: Record the incident bypass and open a rehearsal ticketif: ${{ inputs.incident_id }}env:INCIDENT_ID: ${{ inputs.incident_id }}GH_TOKEN: ${{ github.token }}run: |echo "Freshness gate skipped for incident $INCIDENT_ID" >> "$GITHUB_STEP_SUMMARY"gh issue create --title "Rehearse rollback after incident $INCIDENT_ID" \--body "Deploy run $GITHUB_RUN_ID skipped the rehearsal-freshness gate."A stale rollback path then blocks routine shipping instead of surprising you mid-incident, and every bypass leaves a named incident and an open ticket behind. The gate reads a date from the repository, and on
pushthe workflow itself also comes from the repository, so a pull request (an agent’s included) can movelast_rehearsalorauto_trigger.allowed, or delete the step. Putdeploy/rollback.yml,scripts/rollback.shand/.github/workflows/underCODEOWNERSfor the service owner with a required code-owner review, and reject anylast_rehearsalchange that does not come from the rehearsal job’s pull request.
For a human-initiated rollback, the on-call engineer runs one command and approves in the platform:
# terminal, as the on-call engineergh workflow run rollback.yml -f release_id=2026-09-26.3 -f target_env=productionHow do agents help without touching production?
Section titled “How do agents help without touching production?”The agent’s job is the part humans do badly at 16:40 on a Friday: gathering evidence, checking preconditions against the contract and writing a clear decision record. Give it read-only access to deployment state, for example the GitHub MCP server’s Actions toolset in read-only mode (https://api.githubcopilot.com/mcp/x/actions/readonly, per GitHub’s github-mcp-server README), and a token for a diagnostic identity that cannot dispatch workflows. The setup differs by tool.
Connect the read-only toolset with a token from your secret store, then run the decision prompt headless with an allowlist. The shell expands the token and Claude Code stores it in your local config; use a short-lived, read-only token and rotate it. dontAsk denies every tool call that would prompt, so the agent cannot run gh workflow run or gh api unless you allow them; mcp__github-actions-ro allows every tool on the read-only server.
# terminal, as the diagnostic identityclaude mcp add --transport http github-actions-ro \ https://api.githubcopilot.com/mcp/x/actions/readonly \ -H "Authorization: Bearer $GITHUB_READONLY_TOKEN"
claude -p --permission-mode dontAsk \ --allowedTools "Read,Grep,Glob,mcp__github-actions-ro,Bash(gh run view *),Bash(gh run list *),Bash(gh release view *)" \ --output-format json \ "$(cat prompts/rollback-decision.txt)" > rollback-decision.jsonFlags checked against claude --help, v2.1.283.
Register the same read-only endpoint with the token read from an environment variable, and run the prompt under the built-in read-only permission profile (beta).
# terminal, as the diagnostic identitycodex mcp add github-actions-ro \ --url https://api.githubcopilot.com/mcp/x/actions/readonly \ --bearer-token-env-var GITHUB_READONLY_TOKEN
codex exec -c default_permissions=":read-only" --ephemeral \ -o rollback-decision.md "$(cat prompts/rollback-decision.txt)"Do not add --sandbox to the same command: OpenAI says the profile and legacy sandbox systems do not compose. Flags checked against codex exec --help and codex mcp add --help, v0.157.1.
Add the read-only endpoint to your global ~/.cursor/mcp.json, which stays out of the repository. GitHub’s Cursor installation guide uses this url + headers shape with a literal token; do not paste one into the file. Cursor’s MCP docs describe ${env:NAME} interpolation in mcp.json, so export the diagnostic identity’s read-only token as GITHUB_READONLY_TOKEN from your secret store before you start Cursor. Confirm the interpolation in your build: if it is not expanded, the server fails to authenticate, which is safe.
{ "mcpServers": { "github-actions-ro": { "url": "https://api.githubcopilot.com/mcp/x/actions/readonly", "headers": { "Authorization": "Bearer ${env:GITHUB_READONLY_TOKEN}" } } }}Restart Cursor, open the agent in Plan Mode, which plans before it writes any code, and paste the decision prompt. Cursor’s Run modes and Hooks control which shell commands the agent may run; the run modes are Auto-review, Allowlist and Run Everything, under Settings > Agents > Approvals & Execution (Cursor docs, checked 2026-10-12). Confirm the hook names and CLI flags in your build before you rely on them during an incident.
Keep the prompt in the repository (prompts/rollback-decision.txt) so every on-call engineer runs the same one, and so a change to it gets reviewed.
Replace RUN_ID with the ID that gh run list --workflow rollback.yml prints.
How do you rehearse a rollback honestly?
Section titled “How do you rehearse a rollback honestly?”A rehearsal proves the path only if it uses the real path. Run it on a schedule, in staging or a production-shaped environment, with the same workflow, the same identities and an environment gate configured like production’s.
- Deploy a known release to staging, then deploy a deliberately bad one that trips a recovery check, for example a synthetic that returns errors.
- Run the decision prompt and confirm that the agent names the right target and flags every precondition correctly.
- Dispatch
rollback.ymlwithtarget_env=staging(gh workflow run rollback.yml -f release_id=<known-good> -f target_env=staging) and have a second person approve it. Thestagingenvironment has a required reviewer too, exactly as in production. - Run the recovery verdict prompt and compare its verdict with the dashboards.
- Record the result: who ran it, the approver, start and end times, time to recover, the verdict, and what broke. The rehearsal job then opens a pull request that updates
last_rehearsalin the contract.
The rehearsal job is a scheduled workflow (on: schedule, for example weekly) that runs steps 1, 3 and 5 itself: it deploys both releases, dispatches the rollback, pauses at the staging reviewer and opens the pull request with the recorded result. The on-call pair runs the two prompts in steps 2 and 4.
How do you know the rollback worked without reading the script?
Section titled “How do you know the rollback worked without reading the script?”You verify behaviour, not code. Four pieces of evidence replace a line-by-line read:
- The dry run lists every precondition and its result before the approver clicks.
- The recovery checks in the contract return values, not opinions, and the verdict prompt reports them with timestamps.
- The rehearsal record shows the same job, identity and gate succeeded recently, with a measured time to recover.
- The audit log names who dispatched, who approved and which release ran.
The incident commander, not a model, declares recovery. After recovery, the gap that let the change through becomes a test or an eval, as When an agent causes an incident describes, and the incident re-enters the lifecycle through Maintain.
What breaks in a rollback pipeline, and how do you recover?
Section titled “What breaks in a rollback pipeline, and how do you recover?”The target artifact is gone. Retention policy deleted the previous image, and the script has nothing to restore. Recovery: roll forward with a fix now; afterwards, retain at least the last few production releases, and make the rehearsal fetch the target from the production registry.
The migration is not backward compatible. The old release starts and fails on the new schema. Recovery: abort the rollback, and roll forward. Prevent it with expand-and-contract migrations, and mark every contract migration in the release so the precondition catches it.
The rehearsal used different permissions. Staging ran with an admin token, and production OIDC refuses the job. Recovery: dispatch the job with the break-glass path the incident process defines, then fix the rehearsal to use the exact environment and trust policy, and re-run the rehearsal audit prompt.
Deploy and rollback raced. An auto-merge deployed a new release while the rollback ran, and production ended on neither. Recovery: pause automated deploys (gh workflow disable deploy.yml), confirm no deploy run is pending, re-run the rollback, and keep the shared concurrency group on both workflows, as step 3 explains.
The recovery checks pass, but users still fail. The checks watched service health, not the user journey. Recovery: keep the incident open, add a synthetic for the broken journey to recovery_checks, and treat inconclusive signals as not recovered.
The agent was given write access to speed things up. A false alarm rolled back a healthy release at peak traffic. Recovery: revoke the token, reconnect the read-only endpoint, and move automatic triggering to a versioned alert rule that the contract permits.
Is your rollback path ready?
Section titled “Is your rollback path ready?”-
deploy/rollback.ymlexists for the service, names an immutable target and was reviewed by its owner. - The rollback job uses the production environment gate and short-lived credentials, and no agent step runs in it.
- The
productionandstagingenvironments allow deployments from the default branch only, so an approval always runs the reviewed script. - Preconditions, abort conditions and recovery checks run automatically in the dry run.
- Agents have read-only deployment access and cannot dispatch the workflow.
- Automatic triggering is limited to services whose contract allows it, through a deterministic alert rule and a separate
production-autoenvironment that only the alerting app can reach. - A rehearsal within your policy window recorded actor, approver, times, time to recover and lessons.
- CI blocks a release when the last rehearsal is stale, except for an incident-ID deploy that opens a rehearsal ticket.
- The incident commander declares recovery.
Where to go next after the rollback pipeline
Section titled “Where to go next after the rollback pipeline”Official references: GitHub deployment environments, GitHub MCP server, and the Deploy stage of the method.
Frequently asked questions
How does a rehearsed rollback pipeline keep production authority with humans?
A versioned rollback contract defines the target release, preconditions, and recovery checks, and a protected deploy job with short-lived credentials executes it when triggered by a named human or a pre-approved deterministic alert rule.
What role do coding agents play during a production rollback?
Coding agents can draft the rollback decision record and verify post-rollback recovery checks using read-only evidence, but they never hold production credentials or trigger the rollback themselves.