Monorepo for woodpecker plugins
  • Rust 46.6%
  • Nix 43.7%
  • Just 7.5%
  • Shell 2.2%
Find a file
2026-06-20 16:25:19 +02:00
.woodpecker ci: Add daily flake check 2026-06-20 16:19:11 +02:00
core Initial commit 2026-06-14 11:32:34 +00:00
nix feat: Support non-rust apps 2026-06-14 19:54:58 +00:00
plugins feat: Support non-rust apps 2026-06-14 19:54:58 +00:00
scripts Initial commit 2026-06-14 11:32:34 +00:00
.gitignore Initial commit 2026-06-14 11:32:34 +00:00
AGENTS.md feat: Support non-rust apps 2026-06-14 19:54:58 +00:00
Cargo.lock Initial commit 2026-06-14 11:32:34 +00:00
Cargo.toml feat: Support non-rust apps 2026-06-14 19:54:58 +00:00
flake.lock Initial commit 2026-06-14 11:32:34 +00:00
flake.nix ci: Replace Docker CLI with Skopeo 2026-06-20 16:00:30 +02:00
justfile ci: Add daily flake check 2026-06-20 16:19:11 +02:00
LICENSE Initial commit 2026-06-14 11:32:34 +00:00
README.md docs(README): Add CI badge 2026-06-20 16:25:19 +02:00
renovate.json Add renovate.json 2026-06-15 00:02:37 +00:00
rust-toolchain.toml Initial commit 2026-06-14 11:32:34 +00:00
TODO.md feat: Cachix on flake in verify pipeline 2026-06-19 17:08:23 +02: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 dockerTools.

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/
    ├── example-plugin/     # Rust plugin (uses mkRustPlugin)
    │   ├── Cargo.toml
    │   ├── default.nix     # build entry point
    │   └── src/main.rs
    └── 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

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 tarball tagged with the derivation's version
.#default symlinkJoin of every plugin binary
# Build the binary
nix build .#example-plugin

# Build the image
nix build .#example-plugin-image
docker load < result
docker run --rm -e PLUGIN_MESSAGE=hello woodpecker-plugin-example-plugin:0.1.0

First-build cargoHash bootstrap

buildRustPackage requires cargoHash to be pinned. The flake ships with a placeholder; the first nix build will fail with something like:

error: hash mismatch in fixed-output derivation '/nix/store/...'
  specified: sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
       got: sha256-<real-hash>=

Copy <real-hash> into flake.nix (the cargoHash attr) and rebuild. The same hash works for every plugin because the workspace shares one Cargo.lock.

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>, e.g. example-plugin-v0.1.0. 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.