- Shell 79.6%
- Dockerfile 20.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
ci/woodpecker/push/build/4 Pipeline was successful
ci/woodpecker/push/build/3 Pipeline was successful
ci/woodpecker/push/build/6 Pipeline was successful
ci/woodpecker/push/build/1 Pipeline was successful
ci/woodpecker/push/build/5 Pipeline was successful
ci/woodpecker/push/build/9 Pipeline was successful
ci/woodpecker/push/build/2 Pipeline was successful
ci/woodpecker/push/build/8 Pipeline was successful
ci/woodpecker/push/build/7 Pipeline was successful
A React Native (Expo) release build asks the SDK for the NDK, CMake and build tools, which the android runner doesn't have: every build downloaded about 1 GB into its fresh pod. android-react-native is the android runner with them installed (pinned for Expo SDK 57 / React Native 0.86). The skill gains the React Native APK variant, as validated on android/panalist: prebuild in the release task, injected signing, CI=true for Expo, step resources with a small CPU request, the key alias as plain config, and an app id per variant. |
||
| .woodpecker | ||
| plugins | ||
| skills/forgejo-woodpecker-ci | ||
| workflows | ||
| README.md | ||
ci/ci
Everything CI for the homelab Forgejo + Woodpecker stack, in one repo:
plugins/— Woodpecker plugin and runner images. Each subdirectory builds and publishes one image tocode.olsen.cloud/ci/<name>:<tag>(:latestand:<short-sha>). Other repos consume them in their own.woodpecker/*.yamlfiles.workflows/— shared Taskfiles that consumer repos pull in via remote includes (Task ≥ 3.53). A project's ownTaskfile.ymlis a stub that includes these; the definitions stay centralized here.skills/— agent skills for designing and bootstrapping pipelines on this stack.skills/forgejo-woodpecker-ciis the canonical "how to compose all of this" guide;~/.agents/skills/symlinks into this checkout so Claude Code picks it up.
This repo is public on the Forgejo so raw-URL Taskfile includes work without auth wiring. Nothing secret lives here — secrets are org-level Woodpecker secrets, documented below.
Plugin index
| Image | Type | Purpose |
|---|---|---|
| android | runner image (commands:) |
Android SDK 36 + JDK 17 + mise for reproducible Gradle builds. |
| draftsman | plugin (settings:) |
Release-drafter mechanics: keeps a draft release updated from conventional commits; publishing it cuts the tag. |
| 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. |
Workflow index
| Taskfile | Stack | Provides |
|---|---|---|
| android.yml | Gradle Android | build, test, lint, release:build, ci:quality |
| node.yml | pnpm + mise Node/TS | install, build, lint, format(:check), test(:watch,:coverage), license:check, publish:check, release:dry-run, ci:quality, ci:compliance |
Consume from a project:
# Taskfile.yml
version: '3'
includes:
node:
taskfile: https://code.olsen.cloud/ci/ci/raw/branch/main/workflows/node.yml
flatten: true
# .taskrc.yml — skip the remote-trust prompt (required in CI pods)
remote:
trusted-hosts:
- code.olsen.cloud
Pin raw/commit/<sha>/ instead of raw/branch/main/ to freeze a repo's CI behaviour. flatten: true merges tasks into the root namespace (task ci:quality, not task node:ci:quality) and errors on name collisions — add project-specific tasks under fresh names.
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).
The full pipeline-design guide (shapes, gotchas, first-push checklist) is the skill at skills/forgejo-woodpecker-ci.
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, draftsman |
code.olsen.cloud/ci/kaniko-build, code.olsen.cloud/ci/semantic-release, code.olsen.cloud/ci/draftsman |
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/ci/
├── plugins/
│ ├── android/ # runner image: Android SDK 36 + JDK 17 + mise
│ ├── draftsman/ # plugin: drafted releases from conventional commits
│ ├── kaniko-build/ # plugin: build + push OCI images
│ ├── mise/ # runner image: mise + base toolchain
│ ├── pages-deploy/ # plugin: publish static sites to the pages server
│ ├── renovate/ # plugin: self-hosted Renovate
│ ├── semantic-release/ # plugin: conventional-commits release for JS/TS packages
│ └── trivy/ # plugin: vuln/secret/misconfig scanning
├── workflows/
│ ├── android.yml # shared Taskfile for Gradle Android projects
│ └── node.yml # shared Taskfile for the pnpm+mise Node/TS stack
├── skills/
│ └── forgejo-woodpecker-ci/ # the pipeline-design skill (SKILL.md + assets + references)
├── .woodpecker/
│ ├── build.yaml # event: push → builds changed images via matrix (path-filtered)
│ └── renovate.yaml # event: cron → self-renovates this repo
└── README.md # this file
How the build pipeline works
.woodpecker/build.yaml runs on pushes to main (plus tags and manual triggers). It uses a Woodpecker matrix: over the IMAGE axis to build each directory under plugins/ in parallel — adding a plugin is a one-line edit to the matrix list. Push events are path-filtered per axis (plugins/${IMAGE}/**), so a commit touching only workflows/ or skills/ builds nothing, and a single-plugin change rebuilds only that image. Tag and manual runs rebuild everything.
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/ci 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 under plugins/ with a Dockerfile, an entrypoint.sh that reads PLUGIN_* env vars, and a README.md documenting its settings.
plugins/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
- pages-deploy
- 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 plugins/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.
Adding a new workflow Taskfile
A workflow is a self-contained Taskfile under workflows/ for one stack (e.g. go.yml, python.yml). Rules of thumb:
- Self-contained — no cross-file
deps:, since consumers include one file. - Overridable knobs as vars with
{{.VAR | default "..."}}so consumers can tune via the include'svars:. - Keep the
ci:quality/ci:complianceentry points — the skill's Woodpecker templates call those names for every stack.
Changes to main propagate to all consumers on their next uncached run; treat edits to existing task names/behaviour as breaking and prefer adding tasks over changing them.
Skill maintenance
skills/forgejo-woodpecker-ci documents this stack for agents. When a plugin's settings, a workflow's tasks, or an org secret changes, update the skill in the same commit — that co-location is the point of having it here.
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 |