OrchStep for DevOps engineers
Pipeline logic lives in YAML you can only test by pushing. Here's how to move the steps into one OrchStep workflow your laptop and your pipeline both run — so CI YAML shrinks to one line and green-locally means green-in-CI.
blog/devops-pipelinesYou maintain the pipeline. Which means you maintain a YAML file you can only really test by pushing a commit and watching it run — a feedback loop measured in minutes, multiplied across GitHub Actions, GitLab CI, and whatever the data team uses. The same five steps — lint, test, build, package — are rewritten in three dialects, drift apart, and "works in CI but not locally" becomes a sentence you say out loud.
This post moves the logic out of pipeline YAML and into one OrchStep workflow. The pipeline's job shrinks to a single line: orchstep run ci. The steps live in a file you can run on your laptop, so reproducing a CI failure stops requiring a push.
The pain, concretely
- The only way to test a pipeline change is to push and wait.
- Each CI provider's YAML reimplements the same steps with different syntax.
- A
promotebetween environments is a tangle ofifconditions you can't dry-run.
Before and after
The pipeline stops describing the work and starts delegating it:
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: make lint
- run: make test
- run: make build
- run: |
echo "${TOKEN}" | docker login -u ci --password-stdin
docker push ghcr.io/acme/api:dev
# ...and the same steps, re-typed, in .gitlab-ci.ymljobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: orchstep run ciThe steps didn't disappear — they moved into a workflow file that every provider, and your laptop, run identically.
The workflow
ci is the pipeline body: lint, test (with a retry for the flaky one), build, package. promote branches on the tag — release tags go to production, everything else to staging — with the branch written as if:/then:/else: instead of provider-specific conditionals.
name: service-ci
defaults:
image: "ghcr.io/acme/api"
tag: "dev"
tasks:
# `orchstep run ci` — the whole pipeline body
ci:
steps:
- name: lint
func: shell
do: echo "linting service-ci"
- name: test
func: shell
do: echo "running tests"
retry:
max_attempts: 2
interval: "1s"
backoff_rate: 2.0
- name: build
func: shell
do: echo "building {{ vars.image }}:{{ vars.tag }}"
- name: package
func: shell
do: echo "pushing {{ vars.image }}:{{ vars.tag }}"
# `orchstep run promote --var tag=release`
promote:
steps:
- name: gate
if: '{{ eq vars.tag "release" }}'
then:
- name: prod
func: shell
do: echo "promoting {{ vars.image }} to production"
else:
- name: stg
func: shell
do: echo "promoting {{ vars.image }} to staging"The retry on test lives next to the step it protects, in one place, instead of a provider-specific retries: key you re-learn per platform. The promote gate is readable branching you can preview — no more reasoning about a CI conditional by pushing a tag and praying.
Run the pipeline locally
orchstep run ciThat's the same command your pipeline runs. Wiring it into a provider is one step — full guidance in Running in CI. And because the picker is non-interactive-safe, ad-hoc tasks are callable from automation too:
orchstep menuDry-run the promotion gate
The promotion logic is exactly the thing you don't want to debug in production. A dry-run resolves the tag, evaluates the if, and shows which branch fires — without promoting anything:
orchstep run promote --var tag=release --dry-runYou see the production branch take effect before any image moves. More in Previewing with Dry Run.
What you actually gained
| Concern | Before | OrchStep |
|---|---|---|
| Test a pipeline change | push and wait | orchstep run ci locally |
| Multiple CI providers | re-type steps per dialect | one workflow, providers call it |
| Retry a flaky step | provider-specific key | retry: next to the step |
| Promotion logic | CI conditionals, untested | if:/then:/else:, dry-runnable |
| "Works in CI not locally" | common | same command both places |
This isn't a CI platform — keep GitHub Actions or GitLab for triggers, runners, secrets, and the merge gate. OrchStep is the body of the job, factored out so it stops being three copies. If your pipeline is genuinely five lines that never change, leave it. The ones that have grown matrices and per-provider quirks are the ones worth consolidating.
Where to go next
- Running in CI — the same file on every provider and your laptop
- Branching —
if/then/elif/elseacross steps - Error Handling — retry, catch, finally, timeouts
- Previewing with Dry Run — see the plan before it runs
Maintaining the same pipeline in two dialects? Move the steps into orchstep run ci and let both providers call it.
curl -fsSL https://orchstep.dev/install.sh | sh