BLOG/BY ROLE
THEME
BY ROLE

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.

Apr 19, 2026 OrchStep Team 6 minROLE: DevOps EngineerSCALE: Any
RUNNABLE DEMO
Full source for this post: blog/devops-pipelines
VIEW SOURCE

You 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 promote between environments is a tangle of if conditions you can't dry-run.

Before and after

The pipeline stops describing the work and starts delegating it:

GITHUB ACTIONS (BEFORE)
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.yml
ANY PIPELINE (AFTER)
jobs:
  ci:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: orchstep run ci

The 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.

orchstep.yml
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 ci

That'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 menu

Dry-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-run

You see the production branch take effect before any image moves. More in Previewing with Dry Run.

What you actually gained

ConcernBeforeOrchStep
Test a pipeline changepush and waitorchstep run ci locally
Multiple CI providersre-type steps per dialectone workflow, providers call it
Retry a flaky stepprovider-specific keyretry: next to the step
Promotion logicCI conditionals, untestedif:/then:/else:, dry-runnable
"Works in CI not locally"commonsame 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

Maintaining the same pipeline in two dialects? Move the steps into orchstep run ci and let both providers call it.

#DEVOPS#CI-CD#PIPELINES#AUTOMATION
Try it in two minutes — one binary, no signup.
curl -fsSL https://orchstep.dev/install.sh | sh

RELATED — BY ROLE