No description
  • Shell 83%
  • Dockerfile 17%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Morten Olsen ffcd1a39c8
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
fix(pages-deploy): mktemp template regressed in admin-API rewrite
The rewrite to bearer-token POST reintroduced `pages-XXXXXX.tar.gz` as
the mktemp template — same Invalid-argument bug we fixed in a128c6f
(X's must be at the end). Re-apply the mktemp -d + scratch-dir pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:48:05 +02:00
.woodpecker Revert "fix(build): escape $AUTH so Woodpecker doesn't eat it pre-shell" 2026-05-05 14:24:13 +02:00
kaniko-build Scaffold plugins repo with kaniko-build 2026-04-30 10:09:27 +02:00
mise fix(build): pull bases from non-Hub mirrors to dodge anon rate limits 2026-05-05 14:41:57 +02:00
pages-deploy fix(pages-deploy): mktemp template regressed in admin-API rewrite 2026-05-05 21:48:05 +02:00
renovate fix(build): pull bases from non-Hub mirrors to dodge anon rate limits 2026-05-05 14:41:57 +02:00
semantic-release fix(build): pull bases from non-Hub mirrors to dodge anon rate limits 2026-05-05 14:41:57 +02:00
trivy fix(build): pull bases from non-Hub mirrors to dodge anon rate limits 2026-05-05 14:41:57 +02:00
README.md pages-deploy: bearer token + POST /sites/{host}[/{path}] for new server 2026-05-05 21:42:29 +02:00

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:

  1. Add the repo (Repos → "+")
  2. Configure triggers (cron schedule, branch protection, etc.) in the repo's Settings.
  3. 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:

  1. 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.
  2. Per-repo .woodpecker/*.yaml shrinks 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.

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