Deployments & runs
A deployment is one workflow against a stack. It progresses through stages, each executed by a run on a runner. This page covers everything you do with a deployment from trigger to terminal state.
Modes
The stack page’s Trigger plan button runs a plan on click — the everyday case. The other modes live in its dropdown. Deployment detail pages stay focused on the workflow being inspected; use Retry there when you need to repeat that deployment’s mode and commit.
| Mode | What it does |
|---|---|
| Plan | Produce a plan, wait for confirmation, then apply. The default — the button’s main action. |
| Refresh | Reconcile state with the real world. In the dropdown. |
| Destroy | Tear down the stack’s resources. In the dropdown, and the one mode that asks you to confirm before it queues — though it too only plans the teardown, and still stops at the confirmation step before anything is applied. |
From the stacks list, each row’s run button does the same for plan and refresh; destroy stays a single-stack action on the full stack page.
A fourth mode, plan (unlocked), is created automatically for pull-request previews and never applies — see Pull requests.
The plan → confirm → apply flow
A plan deployment doesn’t pre-create the apply step. It does the plan first, then branches on the result:
- Plan has changes → the deployment pauses in a waiting state at a confirmation step. You review the rendered plan and either confirm (apply), replan with adjustments, or skip.
- Plan has no changes → the deployment finishes immediately. No confirmation, no apply — there’s nothing to do.
When you confirm, GantryCD creates the apply run, a runner executes it, and the
deployment ends in finished.
Plan analysis (the resource-by-resource diff and a summary of adds/changes/ destroys) is captured as structured data, so the UI can show you a clean breakdown rather than scraped log text.
Replanning with targets
At the confirmation step you can replan instead of applying — useful to narrow scope. A replan re-runs the plan carrying your selection:
- Targets or excludes (mutually exclusive — OpenTofu forbids combining them), plus
- an independent list of resources to replace.
The new plan loops back to the same confirmation step, so you can refine repeatedly before applying.
Seeing what a run was narrowed to
A narrowed plan measures only what it looked at, so its stage says so on its own row: a Narrowed · 2 targets · 1 replace marker, visible without expanding the stage. Open the stage and the marker becomes a line stating what it meant — “covered only the 2 resources below — everything else in the stack was left untouched and unexamined” — with the addresses themselves behind a click, in a scrolling, filterable list. The addresses are never printed inline: a select-all replan narrows to hundreds of them.
The apply that follows carries the same marker, tagged (from its plan). An apply run has no targeting of its own — the narrowing is baked into the planfile it replays — so without it a deployment that changed 3 of 300 resources would look exactly like one that covered the stack.
A run with only replaces is not narrowed and does not claim to be: it planned every resource, and just forced some of them to be recreated. Its marker reads Forced replace.
gantrycli deployments get <id> prints the same scope beneath the stage line,
truncated to the first few addresses with a (+N more) count; -o json carries
the full list.
Queueing: one active deployment per stack
A stack can have only one state-changing deployment in flight. The active one
holds the stack lock; newer ones triggered on the same stack sit in queued and
are promoted automatically — in order — when the lock frees (because the active
one finished, failed, was skipped, or cancelled).
Pull-request preview plans are the exception: they never take the lock and run alongside the active deployment.
This means triggering a second deployment is always safe — it won’t clobber the first, it’ll wait its turn.
Prioritizing a waiting deployment
When the queue backs up — many stacks competing for a few runner slots, or a
deep queue on one stack — you can prioritize a deployment to make it next
in line. Prioritized deployments jump ahead of normal FIFO order at both
queues: their stack’s deployment lane and the wait for a free runner. The
button lives on the deployment detail page (also gantrycli deployments prioritize <id>); it needs the prioritize permission on the stack, bundled
into the Stack Triggerer role.
The Queue page in the sidebar (also gantrycli queue) shows work across the
stacks you can see, in execution order, in three tiers: runs executing on a
runner (longest-running first), then runs waiting for a free runner slot, then
deployments queued behind their stack’s lane. The last two carry the prioritize
toggle inline; the executing tier does not, because a bump only reorders work
that is still waiting.
One thing the page deliberately does not list: a deployment parked at a
confirmation gate. It has no run — it is waiting on a person, not on capacity —
so it appears in no tier even though it holds its stack’s lane. Rows queued
behind one still name it, and the Stacks page’s locked filter finds it. A PR
preview is the mirror image: it shows up in the executing tier but holds no
lane, because previews run alongside the lane rather than in it.
The waiting order is a prediction: a run is passed over while every runner group matching its selector is at capacity, letting later runs go first.
What a bump does and doesn’t do:
- It reorders waiting work only. The deployment currently running is never interrupted, and a bump can’t conjure runner capacity — it decides who gets the next free slot, among work competing for the same runner groups.
- Every bump records who made it; the “Prioritized” badge shows the bumper.
- If several deployments are prioritized, they run in the order they were bumped. Re-clicking doesn’t jump ahead of someone else’s bump.
- Future runs of the deployment inherit it: prioritizing a deployment that’s waiting for plan confirmation pre-bumps its apply run.
- A retry is a new deployment and starts unprioritized.
- “Remove priority” (or
--remove) returns it to normal FIFO order.
Clearing queued work
A stack’s Deployments tab can discard work that hasn’t started yet. Only
queued, pending, and deployments waiting at a confirmation gate can be
skipped — a deployment that is already planning or applying is cancelled, not
skipped — and a pull-request preview held at its gate is excluded, because
skipping it would leave the PR unpromotable.
Two shapes, both needing the confirm permission on the stack:
- Skip all queued — one action that discards every skippable deployment on the stack, including ones further down the queue than the list has loaded.
- Skip N selected — tick individual deployments in the timeline, then skip just those. Use it to drop a few superseded commits while letting the rest of the queue proceed. Ticking anything moves Skip all queued into the button’s dropdown.
The selective skip sends one request per deployment, so it reports each one’s outcome separately: if a deployment starts applying while the confirmation is open, that row fails and says so while the others still go through. Failures stay ticked so you can retry them. Skipping frees the stack’s lane, so the next queued deployment is promoted automatically as usual.
Who did what
Three different people can appear on one deployment, and they are not interchangeable:
- Authored — the git identity on the commit. Often a bot, and never proof of anything: anyone can set a commit author locally.
- Ran — the person (or service account) who triggered the deployment.
- Approved — the person who confirmed the apply. On a stack with a review gate this is deliberately somebody else.
The timeline card names all three with their verb. A deployment’s Overview names who triggered it, and its Activity tab tells the whole story: every action anyone took, in order, including the ones that don’t produce a pipeline stage — skips, queue bumps, and cancellation requests.
The Activity tab is a tab of its own precisely because it holds nothing back. Each entry carries what was actually recorded: how many resources a replan narrowed to (the addresses themselves live on the run stage it produced, which is where you compare them against the plan); the run a cancellation was aimed at; the destroy and replace counts behind an automatic apply; the deployment a retry came from, as a link. Creating the deployment is the trail’s first entry — triggering is an action like any other. Outcomes are not: nobody finishes a deployment, so Finished, Failed, and Cancelled are derived from its status and carry no name.
Its URL ends in /activity, so you can hand someone a link straight to the
trail.
Some deployments have no person at all: a pull-request preview plan and a dependency-triggered deployment are created by gantrycd itself, and a push whose commit-author email doesn’t match a verified account on your gantrycd user stays unattributed. The page says so rather than blaming the committer. Add the email to your account to have your pushes attributed to you.
A preview plan that someone approved is the exception — the person who approved it is named on the plan run itself, because that run exists only because they said yes. See Pull requests.
Service accounts appear with a square avatar and a service account badge, so a
machine never reads as a colleague.
Cancelling
Cancellation is cooperative and happens in two visible steps:
- You request cancellation; the deployment moves to
requested_cancellation. - The runner notices on its next status check, stops its OpenTofu process,
acknowledges, and reports the run
cancelled.
If a runner dies without acknowledging, a background cleanup job releases the work so the stack doesn’t stay locked.
The Activity trail records the request and who made it — not the cancellation. Nobody cancels a run; the runner does, once it sees the request.
Runs can’t run forever
Every run carries a hard duration cap tied to its credential lifetime (default
one hour, configurable). As the deadline approaches the runner gracefully
interrupts OpenTofu, and force-kills it shortly after if needed — always while
credentials are still valid, so the final state write and log upload succeed.
You’ll see these end as cancelled with a credential-expiry reason. Operators
tune this via RUN_TTL; see Configuration.
Retrying
Any terminal deployment can be retried. Retry re-runs it as a brand-new
deployment against the same commit — it doesn’t mutate the original, which stays
in the history. Rapid double-clicks are debounced server-side, so you won’t
accidentally launch two. The one exception is a local plan:
its uploaded source is deleted once the deployment finishes, so re-run
gantrycli local-plan instead.
Run Command (the escape hatch)
Run Command is a special deployment that runs a free-text bash script on a
runner, in the stack’s cloned, tofu init-ed working directory, with the stack’s
cloud and state-backend credentials in the environment. There is no plan or
confirmation — it just runs. Use it for the things the standard modes don’t
cover: OpenTofu state surgery (tofu state rm, tofu import, tofu state list)
and other one-off operations.
Because it executes arbitrary commands, it is gated by its own dangerous
permission, run_command (the Stack Command Runner role), kept separate from
the deploy permissions on purpose. It sits at the bottom of the stack page’s
trigger dropdown, below the standard plan, refresh, and destroy modes — and
because its permission is separate, it stays reachable there even for an operator
who may run commands but may not deploy.
Like any other deployment it is pinned to the stack’s synced commit — the dialog
names the commit before you run, and the command’s page records the commit it ran
against and who ran it. Every Run Command also lands on the deployment-audit:
log channel. The script itself is never logged: operators paste secrets into it.
For a bring-your-own (unmanaged) backend, Run Command is also how you
force-unlock or push state — tofu force-unlock <LOCK_ID> and tofu state push <file> — since the runner initializes your backend. Managed-backend
stacks have dedicated, more narrowly-permissioned buttons for the same
recovery; see State administration.
Reading the status
Deployments and runs each carry a status. The ones you’ll see most:
- Deployment:
queued→pending→planning/applying→waiting(confirmation) →finished. Terminal:finished,failed,cancelled,skipped. - Run:
queued→pending→assigned→running→ terminal (success,failure,cancelled,timed_out, …).
The full state machine — including how startup failures and provider problems fail a queue cleanly — is documented for contributors in Deployment State Machine.