Dry-run everything before you run it
Stop guessing what your deploy script will do. --dry-run resolves every variable, renders every command, decides every branch it can, and executes nothing — in the terminal or as a self-contained HTML plan.
blog/dry-run-previewYou're about to run deploy production. Do you actually know what it will do? Which branch the if takes, what the version string resolves to, whether that variable came from the CLI or a stale default? With a bash script the honest answer is "mostly" — and "mostly" is how the wrong image ships to prod.
echo-ing every command before running it doesn't help: echo shows the line, not the decisions. It can't tell you the gate will skip, or that a variable you think is 2.0.0 is silently still 1.4.0.
OrchStep has a real answer: --dry-run. It builds the complete plan — every variable resolved with its winning source, every command rendered, every branch it can decide decided — and executes nothing. It's the difference between "I think canary triggers in prod" and seeing it on screen.
A pipeline worth previewing
This task has the three things that make a plan interesting: a step output another step depends on, a variable-decided gate, and a loop:
name: dryrun-demo
defaults:
app: checkout
version: "2.4.0"
target: staging
notify: ["alice", "bob"]
tasks:
ship:
steps:
- name: build
func: shell
do: echo "built {{ vars.app }}:{{ vars.version }}"
outputs:
image: '{{ result.output | regexFind "[a-z]+:[0-9.]+" }}'
- name: push
func: shell
do: echo "pushing {{ steps.build.image }}"
retry: { max_attempts: 3, interval: "1s" }
- name: gate
if: '{{ eq vars.target "production" }}'
then:
- name: canary
func: shell
do: echo "canary verify in {{ vars.target }}"
else:
- name: full
func: shell
do: echo "full rollout to {{ vars.target }}"
- name: notify_each
func: shell
loop:
items: "{{ vars.notify }}"
do: 'echo "notify {{ loop.item }}: {{ vars.app }} {{ vars.version }} live"'
finally:
- name: cleanup
func: shell
do: echo "cleanup"The plan, in the terminal
orchstep run ship --dry-runThe output is the whole machine, resolved:
VARIABLES (winning layer)
app = checkout (defaults)
notify = ["alice","bob"] (defaults)
target = staging (defaults)
version = 2.4.0 (defaults)
STEPS
1. build [shell] executes
| echo "built checkout:2.4.0"
outputs: image
2. push [shell] executes
| echo "pushing ⟨steps.build.image⟩"
retry: up to 3 attempts, interval 1s
3. gate [if] executes
condition: {{ eq vars.target "production" }} -> false
then [skipped]
1. canary [shell] skipped
else [-> taken]
1. full [shell] executes
| echo "full rollout to staging"
4. notify_each [shell] executes
loop: items: {{ vars.notify }} -> 2 iteration(s) [alice bob]Three things to read here:
- Variables show their source.
version = 2.4.0 (defaults)— not a--var, not an env file. The plan answers "which layer won?" before you've wondered. pushrenders a placeholder.⟨steps.build.image⟩can't exist without runningbuild, so dry-run marks exactly where runtime data flows in instead of inventing a value.gategets a real verdict. Its condition only readsvars.target, fully known at plan time:-> false, thecanarybranch dimmedskipped,fullmarked-> taken. You can see staging won't canary.
Flip an input, watch the verdict change
The whole point is the what-if. Change one variable and re-plan:
orchstep run ship --dry-run --var target=production 3. gate [if] executes
condition: {{ eq vars.target "production" }} -> true
then [-> taken]
1. canary [shell] executes
| echo "canary verify in production"
else [skipped]
1. full [shell] skippedThe verdict flips: canary now -> taken, full rollout skipped. You just confirmed the production path takes the canary branch — without touching production.
See it, then prove nothing ran
--open writes a self-contained HTML plan — the same decisions as a diagram, with placeholders as chips and skipped branches dimmed — and opens it in your browser:
orchstep run ship --dry-run --openIt has no external dependencies and makes no network requests, so it's safe to generate in CI and attach as an artifact. Then run it for real and compare:
orchstep run ship # the ⟨steps.build.image⟩ placeholder becomes checkout:2.4.0Because every step is a harmless echo, the real run lands exactly where the plan's placeholders were. Plan, then run, then diff — no surprises.
A deeper linter, for free
Dry-run renders every do: and args: template, so it catches mistakes a static read can't. Break a template on purpose and the plan reports that step as an error — before a real deploy explodes halfway through. Want a CI gate? --output json emits the structured plan; fail the PR if it contains errors, or diff two plans to catch unintended changes:
orchstep run ship --dry-run --output json | jq '.summary'What you gained
| Question | bash | OrchStep |
|---|---|---|
| Which branch runs? | read it and hope | branch verdict in the plan |
| Where did this var come from? | grep the whole pipeline | winning-layer column |
| What's known only at runtime? | unknowable up front | shown as ⟨placeholder⟩ |
| Will a template error? | find out mid-deploy | flagged as error at plan time |
| A shareable preview? | paste terminal output | --open HTML artifact |
Where to go next
- Previewing with Dry Run — the full hands-on tour, JSON plans, the visual page
- Dry Run reference — transform/prompt/module behavior, the plan's JSON schema
- Variables & Outputs — the layers the provenance column reports
The demo is echo-only, so plan it, run it, and compare: orchstep run ship --dry-run.
curl -fsSL https://orchstep.dev/install.sh | sh