Testing Woodpecker Pipelines - Lint Locally, Debug on CI

Status: Active Last Updated: 2026-08-26 Category: CI/CD - Woodpecker Prerequisites: woodpecker-first-pipeline, woodpecker-installation Time: 2 hours Tags: woodpecker, ci, pipeline, linting, debugging, cli

Summary

Waiting for a push to find out your .woodpecker.yaml is broken wastes 5โ€“10 minutes per mistake. This article covers linting pipelines before commit, running steps locally with the woodpecker-cli exec, forcing re-runs without dummy commits, and a systematic method for debugging failures on the server.

๐ŸŽฏ What You'll Learn

By the end of this article, you'll be able to:


Context / Why This Matters

Woodpecker 3.x gives you nearly all of CI's execution model on your laptop. Teams that adopt local validation catch YAML syntax errors, missing images, and bad commands in seconds instead of per-push round trips โ€” and their commit history stays free of "fix CI" noise.


Implementation / Core Content

Install the CLI

# Linux binary (match your server's major version, 3.x)
curl -LO https://github.com/woodpecker-ci/woodpecker/releases/download/v3.6.0/woodpecker-cli_3.6.0_linux_amd64.tar.gz
tar xzf woodpecker-cli_*_linux_amd64.tar.gz
sudo install -m755 woodpecker-cli /usr/local/bin/
woodpecker-cli --version   # expect 3.x

Lint Before You Push

cd my-project
woodpecker-cli lint .woodpecker.yaml
# .woodpecker.yaml: ok

Lint catches: invalid YAML, unknown step fields, missing required image:, duplicate step names, malformed when: conditions. It does not run your commands โ€” pair it with exec below.

For editor integration, most setups also accept a JSON schema check; a minimal sanity check without the CLI:

python3 -c "import yaml,sys; yaml.safe_load(open('.woodpecker.yaml'))" && echo "yaml ok"

Run Steps Locally with exec

exec runs your real pipeline file through the same container backend locally:

woodpecker-cli exec .woodpecker.yaml

# Single step only:
woodpecker-cli exec --step-name test .woodpecker.yaml

What works out of the box in exec: image, commands, environment, secrets: from a local .env (--env-file), volumes. What doesn't: Forgejo status updates, matrix fan-out from webhooks, plugins that need instance credentials.

Example pipeline and local run:

steps:
  test:
    image: golang:1.22
    commands:
      - go vet ./...
      - go test -race ./...
$ woodpecker-cli exec .woodpecker.yaml
::group::test
go: downloading ...
ok      acme.dev/api/internal/api    0.412s
::endgroup::

Event Simulation Without Dummy Commits

Local files simulate events for filter logic:

# Simulate what a tag push would look like
woodpecker-cli exec --event tag \
  --metadata-file ci/metadata.json .woodpecker.yaml

On the server, to test a branch/tag path without new commits:

  1. Re-run an old pipeline: Woodpecker UI โ†’ Pipeline โ†’ โ‹ฎ โ†’ Restart. Restarted pipelines keep their original event/branch context.
  2. Or push an existing ref again: git push -f origin <sha>:refs/tags/v0.0-test then delete it.
  3. Or trigger via API with a token:
curl -s -X POST "https://ci.example.com/api/repos/$REPO_ID/pipelines" \
  -H "Authorization: Bearer $WP_TOKEN" \
  -d '{"branch":"main"}'

Debugging Techniques Inside Pipelines

Deliberate failure capture:

steps:
  flaky-step:
    image: alpine:3.20
    commands:
      - set -x            # echo each command with expanded variables
      - env | sort        # dump environment when hunting var issues

  always-cleanup:
    image: alpine:3.20
    when:
      status: [success, failure]     # runs even after failures
    commands:
      - docker system df || true

Useful step-level knobs while iterating:

Setting Purpose
failure: ignore Let a diagnostic step run to completion without failing the pipeline
detach: true Start a sidecar (DB, redis) and continue
backend_options limits Reproduce OOM by capping memory deliberately

Reading Failures on the Server

Pipeline view โ†’ click failed step, not just the red banner. Log reading order:

  1. Last ~30 lines first โ€” exit codes and error messages live at the bottom.
  2. Search the full log for exit code and ERROR.
  3. Compare against the local exec run of the same step.
  4. Check the agent logs if the step never started (docker logs woodpecker-agent) โ€” image pull failures and volume permission issues appear there, not in step logs.
# Agent-side triage on the runner host
docker logs --tail 100 woodpecker-agent 2>&1 | grep -iE 'error|pull'
docker system prune          # reclaim disk if pulls fail from ENOSPC

Isolating Problems with when

Temporarily narrow triggers while bisecting:

when:
  event: push
  branch: ci-experiments     # only run on this scratch branch during debugging

Push to ci-experiments freely; merge back once green. Delete the branch filter before merging.


Practical Examples

Example 1: Local-first workflow

# Pre-commit hook: never push broken YAML again
cat > .git/hooks/pre-push <<'EOF'
#!/bin/sh
command -v woodpecker-cli >/dev/null || exit 0
for f in .woodpecker*.yaml; do
  [ -e "$f" ] || continue
  woodpecker-cli lint "$f" || exit 1
done
EOF
chmod +x .git/hooks/pre-push

Example 2: Debug a failing build step end-to-end

# 1. Reproduce locally
woodpecker-cli exec --step-name build .woodpecker.yaml
# โ†’ fails: "npm: not found"

# 2. Fix the image in the pipeline (node:golang mismatch), lint, rerun
sed -i 's/image: golang:1.22/image: node:20/' .woodpecker.yaml
woodpecker-cli lint .woodpecker.yaml && woodpecker-cli exec .woodpecker.yaml

# 3. Commit the fix, verify remotely, watch the run
git add .woodpecker.yaml && git commit -m "ci: use node image for build"
git push
# open https://ci.example.com/acme-corp/api-service and confirm green

Common Pitfalls & Troubleshooting

Problem Cause Fix
lint passes but CI fails Differences: secrets, env, network, mounted volumes Reproduce with exec + --env-file; check agent-side logs
Step hangs forever at start Image pull blocked (registry unreachable) Check agent logs; pre-pull docker pull <image> on the runner
Pipeline skipped entirely when: didn't match actual event metadata Inspect pipeline's "Metadata" tab; compare with filters
exec can't reach localhost services Containers have their own network namespace Use service names/Docker networks, or host.docker.internal
Secrets empty in local exec No .env supplied Run with --env-file .env.secrets
Restart still fails identically Cached workspace from previous attempt Use "Restart from failure" vs clean restart appropriately; clear agent workspace volume if stale
Logs truncated in UI Large output exceeds log retention Increase WOODPECKER_LOG_STORE/log config or reduce verbosity

Next Steps / Ops Actions

Sources & Related Articles

External references consulted:

Related knowledge-base articles:

Change Log

2026-08-26

Choose Theme

Your selection is saved locally.

Neural Cacophony
Aperture v2
Flux v1
Mosaic Chaos
Nexus v1
Nexus Zest
Prism v2
Synapse