No description
  • Shell 79.6%
  • Dockerfile 20.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Morten Olsen 393e475883
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
feat(android-react-native): runner with the NDK, CMake and build tools preinstalled
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.
2026-10-16 10:15:28 +02:00
.woodpecker feat(android-react-native): runner with the NDK, CMake and build tools preinstalled 2026-10-16 10:15:28 +02:00
plugins feat(android-react-native): runner with the NDK, CMake and build tools preinstalled 2026-10-16 10:15:28 +02:00
skills/forgejo-woodpecker-ci feat(android-react-native): runner with the NDK, CMake and build tools preinstalled 2026-10-16 10:15:28 +02:00
workflows fix(android): bound Gradle resource usage 2026-09-15 22:58:34 +02:00
README.md fix(android): bound Gradle resource usage 2026-09-15 22:58:34 +02:00

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 to code.olsen.cloud/ci/<name>:<tag> (:latest and :<short-sha>). Other repos consume them in their own .woodpecker/*.yaml files.
  • workflows/ — shared Taskfiles that consumer repos pull in via remote includes (Task ≥ 3.53). A project's own Taskfile.yml is a stub that includes these; the definitions stay centralized here.
  • skills/ — agent skills for designing and bootstrapping pipelines on this stack. skills/forgejo-woodpecker-ci is 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:

  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).

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:

  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.

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's vars:.
  • Keep the ci:quality / ci:compliance entry 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.

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