pipetree
Scans a directory for Woodpecker CI pipeline
definitions and prints a tree of projects -> pipelines (with launch criteria) -> steps. Meant as a fast way to get an overview of a fleet of
repos' CI architecture, and as a foundation for later automated analysis
(inefficiency linting, an MCP wrapper) — the JSON output is the stable
machine-readable interface for that.
What it finds
Per project (nearest ancestor directory containing .git, or the scan root
if none):
.woodpecker.yml/.woodpecker.yaml- any
*.yml/*.yamldirectly under.woodpecker/
For each pipeline file it extracts: launch criteria (when, branches,
pipeline-level depends_on), matrix axes, and every step in declaration
order — image, commands, per-step when, depends_on, group, detach.
A pipeline or step with no when clause at all runs unconditionally in
Woodpecker — easy to write by accident, so pipetree flags it explicitly as
[always-run] (tree) / "always_run": true (JSON) rather than leaving it
indistinguishable from "no when line to show".
By default, projects whose git origin remote isn't git.oleks.space/oleks/*
are excluded from scans — this keeps vendored upstream mirrors' own CI test
fixtures (e.g. the woodpecker repo's own test-dag/test-when/... corpus)
out of fleet-wide results. Pass -all to include everything.
-analyze also flags two Woodpecker footguns that are statically visible in
a step's commands: text, both of which have cost a real pipeline run:
- a stray apostrophe inside a
-c bash -euxc '...'-style single-quoted shell block — including one inside a#comment — closes the quote early and hands the rest of the script to the outer shell, usually surfacing as a baffling "command not found" for a tool that IS installed (oleks/mempalace pipeline 218). - an unescaped
${VAR}in a step command — Woodpecker substitutes braced${VAR}in the raw pipeline text at parse time, before the shell runs, so a shell variable meant for the shell needs to be escaped as$${VAR}(Woodpecker's own${CI_*}builtins and a pipeline's ownmatrix:axis names are the legitimate unescaped exceptions and aren't flagged).
Build
go build ./...
Usage
pipetree # scan the current directory
pipetree -C ~/projects # scan a specific directory
pipetree -C ~/projects -json # machine-readable output
pipetree -show-commands # include each step's shell commands in the tree
pipetree -C ~/projects -filter 'building*' # only project names matching a glob
pipetree -C ~/projects -all # also include non-git.oleks.space remotes
pipetree -C ~/projects -summary # fleet-wide rollup counts, not the full tree
pipetree -C ~/projects -analyze # shared images, broken depends_on references
pipetree -C ~/projects -health # annotate pipelines with last run status/age
pipetree -C ~/projects -health -summary # rollup counts, incl. healthy/failing/other
-health needs WOODPECKER_SERVER/WOODPECKER_TOKEN in the environment
(or -server/-token) — same convention as woodpecker-cli. It's opt-in:
scans behave identically without it, no network access by default. A repo
that isn't registered in Woodpecker at all is skipped with an informational
message, not treated as an error.
Example:
2 project(s), 3 pipeline(s), 7 step(s)
├── forrest (/home/oleks/projects/forrest)
│ ├── .woodpecker.yml [pipeline: default]
│ │ ├── when: event=push,tag
│ │ ├── step: install (image: node:20)
│ │ ├── step: build (image: node:20) [when: event=push branch=main]
│ │ └── step: deploy (image: alpine) [depends_on: build]
│ └── .woodpecker/release.yml [pipeline: release]
│ ├── when: event=tag
│ └── step: publish (image: alpine)
└── gitea-mcp (/home/oleks/projects/gitea-mcp)
└── .woodpecker.yml [pipeline: default]
└── step: test (image: golang:1.22)
Status
v0.2/v0.3 shipped: scan + parse + tree/JSON/summary/analyze render, plus an
opt-in live-health layer against the Woodpecker API. See
SPEC.md for what's shipped, review feedback, and the roadmap
(v0.4: pipetree lint cross-checked against woodpecker-cli lint, and an
MCP wrapper).
Related tool
woodpecker-cli (the official CLI) validates a single file against
Woodpecker's real schema (woodpecker-cli lint <file>) and can query live
pipeline status/history from the server — useful as a correctness oracle
for this parser and as the source of the "is it actually healthy" data
pipetree doesn't have on its own. See SPEC.md for how the two fit together.