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:
- โ Explain push-based vs pull-based GitOps trade-offs
- โ Have a pipeline update image tags in a manifests repo
- โ Apply manifests from CI against a k0s cluster safely
- โ Detect and correct configuration drift
Table of Contents
- Context / Why This Matters
- Implementation / Core Content
- Practical Examples
- Troubleshooting & Common Pitfalls
- Next Steps / Ops Actions
- 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:
- Use a fine-grained Forgejo token scoped to only the manifests repo with write access; store it via ci-secrets-management.
- Committing straight to
mainworks when the manifests repo is CI-owned. If humans also edit it, open a PR instead and require review per branch-protection.
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
- Push-based: schedule a nightly read-only check (
kubectl diff -f manifests/apps/myapp/) and alert on non-empty output. - Pull-based: automatic โ the agent reverts out-of-band changes at each reconcile. Alert on repeated reconcile failures instead.
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
- Tag
v2.1.0inmyapprepo โ Woodpecker tests + builds image, pushesv2.1.0. update-manifestsstep commitsdeployment.yamlimage bump tomanifestsrepo.- Flux pulls within 60s, applies, waits for readiness; failure surfaces in Flux alerts.
- 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
- Plan rollback for GitOps deploys (it's just git): rollback-procedures
- Alert when reconciles fail or drift persists: ci-monitoring
- Enforce policy on manifests before they merge: policy-as-code
Sources & Related
External references consulted:
Related knowledge-base articles:
Change Log
2026-08-26
- Initial creation by KB writing session.