GitOps Pipelines - Push-Based and Pull-Based Delivery from Woodpecker

Status: Active
Last Updated: 2026-08-26
Category: CI/CD - Phase 2: Delivery
Prerequisites: gitops-principles, deployment-automation, k0s-deployments
Time: 3 hours
Tags: gitops, woodpecker, kubernetes, k0s, manifests, drift

Summary

Connect Woodpecker pipelines to a declarative desired-state repo so deployments are commits, not SSH commands. This article contrasts push-based GitOps (CI applies manifests directly) with pull-based GitOps (an in-cluster agent reconciles), and shows how to wire both from your Forgejo + Woodpecker stack.

๐ŸŽฏ What You'll Learn

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


Table of Contents

  1. Context / Why This Matters
  2. Implementation / Core Content
  3. Practical Examples
  4. Troubleshooting & Common Pitfalls
  5. Next Steps / Ops Actions
  6. Sources & Related

Context / Why This Matters

deployment-automation ships artifacts imperatively: run these commands on that host. GitOps instead treats a git repository as the single source of truth for what should be running. The conceptual foundation lives in gitops-principles and gitops; this article is the concrete Woodpecker wiring for it.

On a homelab running k0s (k0s-installation), GitOps buys you two things: every change to production is reviewable in Forgejo before it happens, and rollback is git revert โ€” no tribal shell history.

Implementation / Core Content

Push-based vs pull-based

Aspect Push-based Pull-based
Actor CI pipeline runs kubectl apply In-cluster controller watches the repo
Cluster credentials Live in CI secrets Never leave the cluster
Latency Immediate on push Poll/reconcile interval
Complexity Low โ€” one extra pipeline step Medium โ€” extra controller to run
Drift correction None (CI is not always watching) Continuous reconciliation
Failure mode CI outage blocks deploys Repo/agent mismatch surfaces as drift

Homelab guidance: start push-based (you already have Woodpecker), graduate to pull-based once you want continuous drift correction or multi-cluster sync.

Push-based: pipeline updates the manifests repo

Keep app code and manifests in separate repos (or separate directories). The app pipeline's deploy step clones the manifests repo, bumps the image tag, and pushes:

steps:
  update-manifests:
    image: alpine:3.20
    when:
      event: [tag]
    environment:
      MANIFESTS_TOKEN:
        from_secret: forgejo_manifests_token
    commands:
      - apk add --no-cache git
      - git clone https://oauth2:$$MANIFESTS_TOKEN@git.fogserv.cloud/homelab/manifests.git
      - cd manifests/apps/myapp
      - sed -i "s|image: registry.fogserv.cloud/homelab/myapp:.*|image: registry.fogserv.cloud/homelab/myapp:${CI_COMMIT_TAG}|" deployment.yaml
      - git config user.email ci@fogserv.cloud
      - git config user.name woodpecker-ci
      - git commit -am "myapp -> ${CI_COMMIT_TAG}" || exit 0   # nothing changed = ok
      - git push origin main

Notes:

Then apply:

  apply:
    image: bitnami/kubectl:1.30
    depends_on: [update-manifests]
    environment:
      KUBECONFIG_B64:
        from_secret: kubeconfig_b64
    commands:
      - echo "$$KUBECONFIG_B64" | base64 -d > /tmp/kubeconfig
      - export KUBECONFIG=/tmp/kubeconfig
      - kubectl -n myapp apply -f manifests/apps/myapp/
      - kubectl -n myapp rollout status deployment/myapp --timeout=120s

rollout status makes the pipeline fail if pods don't become ready โ€” your signal to roll back (rollback-procedures).

Pull-based: let the cluster reconcile

Instead of applying from CI, run an in-cluster agent (Flux or Argo CD) that polls the manifests repo; the Woodpecker step above still updates the repo but drops the apply step entirely.

Minimal Flux bootstrap against your Forgejo instance:

flux bootstrap git \
  --url=ssh://git@git.fogserv.cloud/homelab/manifests.git \
  --branch=main \
  --path=clusters/homelab \
  --interval=1m

Woodpecker then becomes purely a producer of commits: tests pass โ†’ build image โ†’ bump tag in manifests repo โ†’ Flux notices within ~1 minute and reconciles. The cluster holds its own read-only deploy key; no kubeconfig ever leaves the cluster.

Drift handling

Full detection playbook: drift-detection-runbook.

Repo layout recommendation

manifests/
โ”œโ”€โ”€ apps/
โ”‚   โ”œโ”€โ”€ myapp/deployment.yaml, service.yaml
โ”‚   โ””โ”€โ”€ otherapp/...
โ”œโ”€โ”€ clusters/
โ”‚   โ””โ”€โ”€ homelab/          # flux path / top-level kustomization
โ””โ”€โ”€ base/                 # shared labels, resource quotas

Practical Examples

Example: end-to-end tagged release, pull-based

  1. Tag v2.1.0 in myapp repo โ†’ Woodpecker tests + builds image, pushes v2.1.0.
  2. update-manifests step commits deployment.yaml image bump to manifests repo.
  3. Flux pulls within 60s, applies, waits for readiness; failure surfaces in Flux alerts.
  4. Rollback = revert the commit in Forgejo web UI; Flux converges back automatically.

Verify convergence from an admin host:

kubectl -n flux-system get kustomizations
kubectl -n myapp get deploy myapp -o jsonpath='{.spec.template.spec.containers[0].image}'
# expect: registry.fogserv.cloud/homelab/myapp:v2.1.0

Troubleshooting & Common Pitfalls

Problem Cause Fix
sed doesn't match the image line Manifest formatting differs from the pattern Normalize the manifest first, or use yq -i '.spec.template.spec.containers[0].image = "..."' deployment.yaml
Pipeline pushes empty commit and fails No changes for this tag (re-tag) Keep || exit 0 guard after git commit
kubectl apply hangs on CI runner Runner can't reach cluster API (firewall/VPN) Allow runner subnet to apiserver port; see network-segmentation
Flux keeps reverting manual fixes Working as intended โ€” drift correction Change the manifests repo, not the live cluster
Token expired, manifest bumps stop silently Forgejo token TTL Set expiry reminder; alert on missing CI commits (see below)

Next Steps / Ops Actions

Sources & Related

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