just release-run TICKETandjust release-revert TICKETare the only two deploy verbs you need. Everything else is a sub-script. The orchestrator owns its own git state, takes per-service backups, runs the migration, verifies — halt-on-failure between every phase. Rollback restores the backups and reverts the merge commit.
Level: 6 · Reading time: 25 min
prod as the alias; the orchestrator runs there, not on your laptop).Already know this rung? Skip to Level 7 — deploy to prod (planned) where you'll set up a new prod host from scratch and wire it into the release flow.
PMA's release model is atomic over four phases. Each phase
either succeeds entirely or fails, and the orchestrator stops
between phases. If anything goes wrong, release-revert restores
the world.
The four phases:
Per Golden Rule 13: release-run and release-revert are the
only operator entry points. The sub-scripts in
scripts/release/ (prepare.sh, revert.sh, per-ticket
scripts in releases/) are debug-only — never invoke them
directly as the operator path.
In this rung we'll do (a) a normal release, (b) a deliberate
failure → revert, (c) a partial-failure resume with
--from=PHASE.
Assume you have a Redmine ticket #1234 for a manifest change to
Redmine (bumped the image tag). You've committed it on a feature
branch + opened a PR + got it merged to main. Now deploy.
ssh prod 'cd /opt/asd-pma && just release-run 1234'
That's the whole operator action. The orchestrator does:
Phase 1 — prepare:
git fetch originBASE_SHA = HEAD (pre-pull)git pull --ff-only origin mainMERGE_SHA = HEAD (post-pull).asd/workspace/releases/1234.envevent=done to 1234.logPhase 2 — migrate:
bun scripts/release/release-check.ts --diff $BASE_SHA to regenerate .asd/workspace/release/migrate.sh from the actual diffbash .asd/workspace/release/migrate.sh all --no-backupevent=donePhase 3 — ticket-script:
releases/1234-*.sh. If absent: logs skipped-no-script and continues.bash releases/1234-*.sh --yesPhase 4 — verify:
HEAD == MERGE_SHAjust release --service=<name> for each changed service (drift check; must show 0 actions)event=done rc=0Output ends with:
[release-run] release 1234 complete
To practise the revert path safely (do this on dev, not prod):
# 1. Make a change that will fail verify — e.g. break a service's
# health endpoint by editing its manifest health.endpoint to /nonexistent
git checkout -b feat/will-fail
$EDITOR packages/redmine/manifest.yaml # set health.endpoint: /nonexistent
git commit -am "deliberate fail for revert drill"
# (would normally merge to main; for the drill just simulate by ff-merging locally)
# 2. Run the release
just release-run 9999
# Phase 4 (verify) fails because the health probe gets 404
# Output ends with:
# release-run failed in phase verify. To roll back:
# just release-revert 9999
# 3. Revert
just release-revert 9999
# Restores the redmine backup taken in phase 1
# Runs git revert -m 1 MERGE_SHA (creates a revert commit)
# Service is back to pre-release state
The state file .asd/workspace/releases/9999.env was written in
phase 1; release-revert reads it for the BASE_SHA, MERGE_SHA,
and BACKUPS list. If the file is gone, revert can't proceed
cleanly — that's why phase 1 writes it BEFORE any mutation.
--from=PHASESometimes a phase fails for a reason outside the release content
itself — a flaky network, a wedged container, a stale runner.
After fixing the underlying issue you don't want to redo
everything. The --from=PHASE flag skips earlier phases:
just release-run 1234 --from=migrate
# Skips prepare (uses the existing state file's BASE_SHA + MERGE_SHA + BACKUPS)
# Starts at phase 2
Phase values: prepare (skip nothing), migrate (skip prepare),
ticket (skip prepare + migrate), verify (skip everything but
verify).
When --from=ticket is the right call: the migrate phase
succeeded but the ticket script failed for a transient reason
(e.g. a one-off race). After fixing the root cause, resume from
ticket so you don't re-do the migrate work.
When --from=verify is the right call: rare — only when
you're confident every phase ran correctly but the verifier
flaked.
Per Golden Rule 13: do not invoke prepare.sh / revert.sh /
the ticket script directly. Always go through release-run
with --from=.
$ cat .asd/workspace/releases/1234.env
TICKET=1234
BRANCH=main
BASE_SHA=a1b2c3d4...
MERGE_SHA=e5f6a7b8...
SERVICES="redmine"
SKIPPED_METADATA=""
BACKUPS="redmine=20260518_153012"
$ tail .asd/workspace/releases/1234.log
2026-05-18T15:30:12+00:00 ticket=1234 phase=prepare event=done rc=0
2026-05-18T15:30:45+00:00 ticket=1234 phase=migrate event=done rc=0
2026-05-18T15:30:45+00:00 ticket=1234 phase=ticket event=skipped-no-script
2026-05-18T15:30:48+00:00 ticket=1234 phase=verify event=done rc=0
2026-05-18T15:30:48+00:00 ticket=1234 phase=orchestrator event=done rc=0
Service is live with the new code. State file + audit log
preserved for post-deploy review or release-revert if needed.
Three things to internalise:
The orchestrator owns its own git state. Don't pre-pull
on prod before running. Don't git checkout. Don't git reset. The script captures BASE_SHA, pulls to MERGE_SHA,
uses the diff. Pre-pulling collapses BASE_SHA == MERGE_SHA
→ "nothing to release" error. This is the most common
self-inflicted release failure; Golden Rule 13 codifies the
anti-pattern.
Phase 1 takes backups before any mutation. This is
what makes revert safe. If anything in phase 2/3/4 fails,
the backup from phase 1 is the rollback target. The state
file (written at end of phase 1) records the backup
timestamps so revert can find them.
migrate.sh is regenerated from the diff every release.
It's not a hand-maintained file. release-check.ts reads
the diff between BASE_SHA and MERGE_SHA, classifies each
changed file (manifest change? compose change? script change?
src change?), and emits the right set of migrate actions.
This is why "edit a manifest, commit, release-run" works
without writing per-release migration code.
For the recovery playbooks side (what to do when phase X fails
for known-cause Y), see recovery/playbooks/release-*.yaml. Three
real entries shipped this month: release-verify-race
(post-migration health check too eager, fixed in #3918),
release-run-exit-5-with-drift (src/ changes not classified, fixed
in #3851), release-orchestrator-source-drift.
Reference: /pma/internals/release-orchestrator (planned)
covers the four phases in source-code detail.
recovery/playbooks/release-*.yaml. If none match, file a new entry as part of the fix (Golden Rule 11).just release-run TICKET --dry-run. Runs every phase in preview mode, no mutations./pma/reference/cli/release (planned) — every flag + override for release-run and release-revert.