GitHub Actions without the YAML sprawl
Your .github/workflows folder has six files and 400 lines you can't run locally. Move the logic into orchstep tasks and let the workflow do one job: call them.
blog/github-actions-tasksOpen .github/workflows/ on any repo older than a year. There's ci.yml, release.yml, nightly.yml, and three more, each 60-plus lines of steps with inline run: blocks, if: github.event_name == ... guards, and a matrix you copy-pasted between two of them. None of it runs on your laptop. All of it is GitHub-specific. When a step is wrong, you debug it by pushing commits.
The sprawl isn't GitHub's fault — it's that the workflow file is doing two jobs at once: triggering (on push, on schedule, on a matrix) and defining the work. GitHub is great at the first. It's a poor place to keep the second.
So split them. Triggering stays in the workflow. The work moves into orchstep tasks you can run anywhere.
The work lives in tasks
Here's the pipeline logic — setup, test, build — as plain tasks. The test task takes a suite variable so one definition covers the whole matrix:
name: app
# The GitHub workflow stays thin: it just calls these tasks.
# Logic lives here, where you can also run it on your laptop.
defaults:
suite: unit
tasks:
# `orchstep run setup`
setup:
steps:
- name: deps
func: shell
do: echo "restoring dependencies"
# `orchstep run test --var suite=integration` — one task, matrix-friendly
test:
steps:
- name: run
func: shell
do: echo "running {{ vars.suite }} suite"
- name: annotate
if: '{{ eq vars.suite "integration" }}'
then:
- name: note
func: shell
do: echo "::notice::integration suite finished"
# `orchstep run build`
build:
steps:
- name: compile
func: shell
do: echo "building release artifact"
outputs:
artifact: "dist/app.tar.gz"
- name: report
func: shell
do: echo "built {{ steps.compile.artifact }}"That annotate step prints a ::notice:: line — GitHub renders those as annotations on the run, so you get the nice UI markers without coupling the logic to GitHub. Run the task locally and you just see the line in your terminal.
The workflow gets thin
Now the GitHub side does one thing: trigger and call. The matrix that used to wrap inline shell steps now varies a single --var:
name: ci
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
suite: [unit, integration]
steps:
- uses: actions/checkout@v4
# The official action installs the CLI and runs the task in one step.
# (Prefer a manual install? Replace it with:
# - run: curl -fsSL https://orchstep.dev/install.sh | sh
# - run: orchstep run test --var suite=${{ matrix.suite }} )
- uses: orchstep/run-orchstep@v1
with:
workflow: orchstep.yml
task: test
vars: |
suite=${{ matrix.suite }}
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: orchstep/run-orchstep@v1
with:
workflow: orchstep.yml
task: buildThe matrix is still GitHub's — that's the part GitHub is genuinely good at. What changed is that the body of each job is now a one-liner. There's no inline shell to drift, no logic you can't reproduce locally. To add a contract suite, you add it to the matrix list; the task already handles any suite value.
Two official actions
There are two maintained actions, so you don't hand-roll the install:
orchstep/run-orchstep@v1— installs the CLI and runs a task (orlint/list-tasks) in one step. It acceptstask,vars,env,output, andjson-file, and adds inline::errorannotations on failure. That's the one used above.orchstep/setup-orchstep@v1— just installs (and caches) the binary, for a job that runs severalorchstepcommands:
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: orchstep/setup-orchstep@v1
with:
version: latest # or pin a semver
- run: orchstep lint
- run: orchstep validate release --strict
- run: orchstep run release --env productionPrefer no action at all? The manual curl -fsSL https://orchstep.dev/install.sh | sh still works everywhere — the binary is the binary. The actions just save you the install boilerplate and add the annotations.
Reproduce the matrix on your laptop
Each matrix cell is just a task call, so you run any cell directly:
orchstep run test --var suite=integrationNo act, no pushing a debug commit, no waiting for the runner. The thing that failed in CI is the same command in your shell.
What you actually gained
| Concern | Inline workflow steps | orchstep tasks |
|---|---|---|
| Run a job locally | install act, or push | orchstep run <task> |
| Matrix body | duplicated inline shell | one task, varies a --var |
| Annotations | GitHub-only ::notice:: strings | a step you can also run locally |
| Add a release pipeline | another 60-line YAML file | another task in the same file |
| Switch CI providers | rewrite every workflow | rewrite the thin glue |
The honest boundary: triggering, secrets injection, and the matrix fan-out belong in GitHub Actions, and OrchStep doesn't try to replace them. What it replaces is the 300 lines of pipeline logic that ended up in YAML because that's where the runner could see it.
Where to go next
- Quick Start — your first workflow in two minutes
- Variables & Outputs — how
--varand matrix values flow through - OrchStep in CI — the official actions, GitLab, exit codes, and annotations
- Branching —
if/then/elsefor per-suite branches
Got a .github/workflows folder you can't run locally? Move the bodies into tasks — curl -fsSL https://orchstep.dev/install.sh | sh.
curl -fsSL https://orchstep.dev/install.sh | sh