- Shell 83%
- Dockerfile 17%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
ci/woodpecker/push/build/3 Pipeline was successful
ci/woodpecker/push/build/1 Pipeline was successful
ci/woodpecker/push/build/6 Pipeline was successful
ci/woodpecker/push/build/2 Pipeline was successful
ci/woodpecker/push/build/5 Pipeline was successful
ci/woodpecker/push/build/4 Pipeline was successful
The rewrite to bearer-token POST reintroduced `pages-XXXXXX.tar.gz` as
the mktemp template — same Invalid-argument bug we fixed in
|
||
| .woodpecker | ||
| kaniko-build | ||
| mise | ||
| pages-deploy | ||
| renovate | ||
| semantic-release | ||
| trivy | ||
| README.md | ||
ci/plugins
Woodpecker CI plugins and runner images for the homelab. Each subdirectory builds and publishes one image to code.olsen.cloud/ci/<name>:<tag> (:latest and :<short-sha>). Other repos consume them in their own .woodpecker/*.yaml files.
Index
| Image | Type | Purpose |
|---|---|---|
| kaniko-build | plugin (settings:) |
Build and push OCI images. Wraps Kaniko — daemon-less, unprivileged. |
| mise | runner image (commands:) |
Debian-slim with mise preinstalled — pipelines bring their own .mise.toml. |
| pages-deploy | plugin (settings:) |
Publish a directory to the homelab pages server (host + optional sub-path). |
| renovate | plugin (settings:) |
Self-hosted Renovate scoped to the calling repo by default. |
| semantic-release | plugin (settings:) |
Conventional-commits version bump, CHANGELOG, tag, and npm publish for JS/TS packages. |
| trivy | plugin (settings:) |
Filesystem / image / config scanning for vulns, secrets, misconfig. |
Quick start (using a plugin from another repo)
Drop a workflow under .woodpecker/:
when:
- event: [push, manual]
branch: main
steps:
build:
image: code.olsen.cloud/ci/kaniko-build:latest
settings:
auth:
from_secret: forgejo_registry_auth
Then in Woodpecker:
- Add the repo (Repos → "+")
- Configure triggers (cron schedule, branch protection, etc.) in the repo's Settings.
- Ensure the org-level secrets the plugins need are present (see below).
Prerequisites — once per Woodpecker org
Each Woodpecker org that consumes these plugins needs the secrets the plugins reference. Set under <org> → Settings → Secrets:
| Secret | Value | Used by | Image filter |
|---|---|---|---|
forgejo_registry_auth |
base64 of user:PAT |
kaniko-build, semantic-release |
code.olsen.cloud/ci/kaniko-build, code.olsen.cloud/ci/semantic-release |
pages_admin_token |
bearer token (auto-generated by the pages chart) | pages-deploy |
code.olsen.cloud/ci/pages-deploy |
renovate_forgejo_token |
raw PAT | renovate |
code.olsen.cloud/ci/renovate |
Image filters engage because these are real plugin steps (settings:-only, no commands:/entrypoint:/environment:). Filtering locks each secret to the one plugin that should ever read it.
The Woodpecker agent in the cluster also has a kubernetes.io/dockerconfigjson pull-secret (woodpecker-pull-secret in prod ns) so kubelet can pull these private plugin images when scheduling pipeline pods. That's configured once in the Woodpecker chart (homelab/apps/apps/charts/woodpecker), not per repo or per org.
Layout
ci/plugins/
├── kaniko-build/ # plugin: build + push OCI images
├── mise/ # runner image: mise + base toolchain
├── renovate/ # plugin: self-hosted Renovate
├── semantic-release/ # plugin: conventional-commits release for JS/TS packages
├── trivy/ # plugin: vuln/secret/misconfig scanning
├── .woodpecker/
│ ├── build.yaml # event: push → builds all images via matrix
│ └── renovate.yaml # event: cron → self-renovates this repo
└── README.md # this file
How the build pipeline works
.woodpecker/build.yaml runs on every push to main (plus tags and manual triggers). It uses a Woodpecker matrix: over the IMAGE axis to build each top-level directory in parallel — adding a plugin is a one-line edit to the matrix list.
The bootstrap step uses raw Kaniko, not the kaniko-build plugin, to avoid a chicken-and-egg loop. Each image is pushed twice: :latest for consumers that want to track main, and :<short-sha> for reproducible pinning.
.woodpecker/renovate.yaml runs on cron schedules configured in the Woodpecker UI. It uses the renovate plugin (built from this same repo) to scan ci/plugins itself, opening PRs to bump upstream base images (Kaniko, Debian, renovate, Trivy, etc.). The bootstrap-vs-self-host distinction matters because if renovate:latest ever ships broken, you can still rebuild via raw Kaniko by pushing a fix.
Adding a new plugin
Each plugin is a directory with a Dockerfile, an entrypoint.sh that reads PLUGIN_* env vars, and a README.md documenting its settings.
my-plugin/
├── Dockerfile # FROM <upstream>; COPY entrypoint; ENTRYPOINT [...]
├── entrypoint.sh # reads PLUGIN_* env vars and execs the upstream tool
└── README.md # usage snippet + settings table
Then add the directory name to the matrix in .woodpecker/build.yaml:
matrix:
IMAGE:
- kaniko-build
- mise
- renovate
- semantic-release
- trivy
- my-plugin # ← add here
Push, build runs, image lands at code.olsen.cloud/ci/my-plugin:latest. That's it.
Plugin contract (settings-based)
Woodpecker auto-treats a step as a plugin when only image: and settings: are set — no commands:/entrypoint:/environment: overrides. Each settings.foo: bar becomes PLUGIN_FOO=bar in the container. Lists become comma-separated strings. from_secret: references resolve before the step runs.
The image's ENTRYPOINT runs with that env set. Read what you need, fall back to sensible defaults, fail loudly with a clear message when required input is missing — see kaniko-build/entrypoint.sh for the canonical pattern.
Why bother with the plugin form (vs. just commands:)? Two reasons:
- Image-filtered secrets work. Woodpecker only enforces secret image filters on plugin steps.
commands:steps bypass the filter, which is fine for fully-trusted internal repos but blunts defense-in-depth. - Per-repo
.woodpecker/*.yamlshrinks to the 6 lines a consumer cares about — no inline shell, no env-var wiring.
Runner-image contract (commands-based)
Some images aren't plugins — they're general-purpose toolchains consumed via commands:. mise is the canonical example. These don't need a PLUGIN_* entrypoint; they just need a sensible base shell on PATH so consumers can run their own commands.
Both kinds publish to the same code.olsen.cloud/ci/<name>:latest namespace. The distinction is purely how consumers invoke them.
Where related infra lives
| Question | Where |
|---|---|
| Woodpecker server + agent chart | homelab/apps/apps/charts/woodpecker/ |
| Pipeline-pod RBAC + agent env (k8s backend) | Same chart's templates/agent-*.yaml |
| Cluster-level imagePullSecret for these plugin images | Same chart's templates/pull-secret-sealed-secret.yaml |
| Forgejo (the forge that fires webhooks) + Forgejo OAuth for Woodpecker | homelab/apps/apps/charts/forgejo/ |
| End-to-end setup history | homelab/base/tickets/open/2026-04-30-add-woodpecker-ci.md |