Monorepo for woodpecker plugins
  • Rust 71.9%
  • Nix 18.1%
  • Shell 6.1%
  • Just 3.9%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
martin b71fec2bc8
All checks were successful
ci/woodpecker/cron/flake-check Pipeline was successful
Delete .beads/issues.jsonl
2026-10-04 12:32:34 +00:00
.beads Delete .beads/issues.jsonl 2026-10-04 12:32:34 +00:00
.cargo openapi-gen-build (#10) 2026-07-25 12:53:14 +00:00
.claude bd init: initialize beads issue tracking 2026-10-04 13:51:31 +02:00
.woodpecker ci: Update verify nix workflow to run on pr and depend on global verify 2026-08-23 12:15:31 +00:00
core fix(forgejo-opencode-review): Test failures due to tempdir (#39) 2026-08-23 11:38:55 +00:00
nix fix(cargo): Add missing cc linking 2026-08-16 09:48:35 +00:00
plugins chore(forgejo-opencode-review): Bump version 2026-09-29 19:04:39 +00:00
scripts feat: Add integration tests for flake-checker (#47) 2026-09-12 17:53:19 +00:00
.gitignore feat(beads): Init beads 2026-10-03 21:42:24 +00:00
AGENTS.md feat(beads): Init beads 2026-10-03 21:42:24 +00:00
Cargo.lock chore(forgejo-opencode-review): Bump version 2026-09-29 19:04:39 +00:00
Cargo.toml feat(forgejo-release): Support multiple tags per repo 2026-08-29 21:22:09 +00:00
CLAUDE.md bd init: initialize beads issue tracking 2026-10-04 13:51:31 +02:00
CODEOWNERS Migration: codeowners (#20) 2026-07-29 18:17:01 +00:00
flake.lock feat(beads): Init beads 2026-10-03 21:42:24 +00:00
flake.nix feat(beads): Init beads 2026-10-03 21:42:24 +00:00
justfile Add gates before release to avoid invalid releases 2026-09-18 17:04:11 +02:00
LICENSE Initial commit 2026-06-14 11:32:34 +00:00
potential-plugins.md docs: Update table of potential plugins 2026-09-26 21:47:10 +00:00
README.md feat: Add integration tests for flake-checker (#47) 2026-09-12 17:53:19 +00:00
renovate.json chore(renovate): Use flake preset 2026-08-08 12:34:46 +02:00
rust-toolchain.toml Initial commit 2026-06-14 11:32:34 +00:00
treefmt.nix Initial commit 2026-06-14 11:32:34 +00:00

woodpecker-plugins

A Rust workspace of Woodpecker CI plugins. Each plugin is an independently versioned binary that ships as its own OCI image, built reproducibly with Nix via nix2container.

Layout

woodpecker-plugins/
├── Cargo.toml              # workspace root (Rust plugins only)
├── flake.nix               # discovers plugins, exposes binary + image per plugin
├── nix/
│   └── mk-rust-plugin.nix  # shared Rust build helper
├── core/                   # shared library: Plugin trait, env-var settings loader, tracing
└── plugins/
    └── flake-lock-checker/ # non-Rust plugin wrapping pkgs.flake-checker
        └── default.nix

Every plugin owns a plugins/<name>/default.nix that returns a derivation. The flake auto-discovers any directory under plugins/ with that file, so adding a plugin requires no flake edits — Rust or otherwise.

Rust plugins additionally depend on woodpecker-plugins-core for the Plugin trait, env-var settings loading, tracing initialisation, and a unified error type, so main.rs stays tiny.

Build & test (Cargo)

cargo build                                 # whole workspace
cargo test  --workspace
cargo clippy --workspace -- -D warnings
cargo fmt   --all

Or via just:

just build
just test
just lint
just fmt

Plugin integration tests

Integration pipelines live beside the plugin they test as plugins/<name>/integration.yaml. They are real Woodpecker pipelines: the plugin OCI image is built with Nix and then started by woodpecker-cli exec through a Docker-compatible container backend.

The runner requires:

  • fish
  • nix
  • woodpecker-cli (provided by the Nix flake/dev shell)
  • Docker, or Podman with an active Docker-compatible API socket

Run all discovered integration pipelines with:

scripts/integration-test.fish

Run one plugin by passing its directory name:

scripts/integration-test.fish flake-checker

If the container socket is only accessible to root, run the command through sudo:

sudo just integration flake-checker

The equivalent direct command is:

sudo scripts/integration-test.fish flake-checker

The runner prefers Docker when it is available. Otherwise it looks for the rootless Podman socket at $XDG_RUNTIME_DIR/podman/podman.sock, then the rootful socket at /run/podman/podman.sock. An explicit DOCKER_HOST may be used when the backend is exposed at another location.

For each selected plugin, the runner:

  1. Evaluates the plugin version from the Nix flake.
  2. Builds .#<plugin>-image.
  3. Loads the image into Docker or Podman.
  4. Adds the local woodpecker-plugin-<plugin>:integration tag.
  5. Executes plugins/<plugin>/integration.yaml with woodpecker-cli exec.

To add an integration test for another plugin, create an integration.yaml beside its default.nix. The runner discovers it automatically; no central registry or script change is needed. The workflow should use the stable integration image tag:

steps:
  - name: run plugin
    image: woodpecker-plugin-example:integration
    settings:
      example: value

The equivalent just recipe is:

just integration
just integration flake-checker

Build a plugin OCI image (Nix)

The flake auto-discovers plugins/<name> directories that contain a default.nix and exposes:

Attribute What it produces
.#<name> release binary for the plugin
.#<name>-image OCI image (nix2container manifest) tagged with the derivation's version
.#default symlinkJoin of every plugin binary

Adding a new plugin

A plugin is whichever directory you create under plugins/ with a default.nix that returns a derivation. The flake picks it up automatically.

Rust plugin

cargo new --bin plugins/my-plugin

In plugins/my-plugin/Cargo.toml:

[package]
name = "my-plugin"
version = "0.1.0"
edition.workspace = true
license.workspace = true

[dependencies]
woodpecker-plugins-core = { path = "../../core" }
serde = { workspace = true }
tokio = { workspace = true }

In plugins/my-plugin/default.nix:

{ mkRustPlugin }:
mkRustPlugin { name = "my-plugin"; }

Implement Plugin for your type and let the helper drive it. The trait uses RPITIT, so write a bare async fn run — no #[async_trait]:

use serde::Deserialize;
use woodpecker_plugins_core::{Plugin, PluginError, run_plugin};

#[derive(Debug, Deserialize)]
struct Settings {
    target: String,
}

struct MyPlugin;

impl Plugin for MyPlugin {
    type Settings = Settings;

    async fn run(settings: Self::Settings) -> Result<(), PluginError> {
        tracing::info!(target = %settings.target, "running");
        Ok(())
    }
}

#[tokio::main]
async fn main() -> Result<(), PluginError> {
    run_plugin::<MyPlugin>().await
}

Plugin wrapping a nixpkgs package

When the work is already done by something in nixpkgs, skip the Rust crate entirely and hand the flake a derivation directly. plugins/flake-lock-checker/default.nix is the reference example: it wraps pkgs.flake-checker with writeShellApplication, translates PLUGIN_* env vars to CLI flags, and pins the version literally so the release tag and image tag agree.

{ lib, writeShellApplication, jq }:
let version = "1.7.1"; in
assert lib.assertMsg (jq.version == version)
  "plugin pin (${version}) != nixpkgs (${jq.version}); bump or override src";
(writeShellApplication {
  name = "jq-plugin";
  runtimeInputs = [ jq ];
  text = ''jq "$PLUGIN_FILTER" "$PLUGIN_INPUT"'';
}).overrideAttrs (old: {
  inherit version;
  meta = (old.meta or {}) // { mainProgram = "jq-plugin"; };
})

The image tag and release-tag check both read drv.version, so any non-Rust plugin must expose a version attribute on its returned derivation.

Configuration

Plugins receive their configuration from environment variables prefixed with PLUGIN_, the Woodpecker plugin convention. Field names are matched case-insensitively against the prefixed env vars:

Settings field Env var
message: String PLUGIN_MESSAGE
uppercase: bool PLUGIN_UPPERCASE

Use #[serde(default)] on Settings fields that are optional.

Releasing

Tag the commit with <plugin-name>-v<version>. The .woodpecker/release.yaml pipeline parses the tag, builds .#<plugin-name>-image, and pushes the image to the configured registry. Before pushing it asserts nix eval --raw .#<plugin-name>.version equals <version> from the tag, so a tag that disagrees with the derivation fails fast. For Rust plugins the version comes from plugins/<name>/Cargo.toml; for nixpkgs-wrapped plugins it comes from the literal version in their default.nix. Bump the relevant source before tagging.

License

MIT — see LICENSE.