Monorepo for woodpecker plugins
  • Rust 74.2%
  • Nix 21.7%
  • Just 3.6%
  • Shell 0.5%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Martin Berg Alstad aefc757a59
All checks were successful
ci/woodpecker/push/verify-nix Pipeline was successful
ci/woodpecker/cron/flake-check Pipeline was successful
ci: Update verify nix workflow to run on pr and depend on global verify
2026-08-23 12:15:31 +00:00
.cargo openapi-gen-build (#10) 2026-07-25 12:53:14 +00: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 fix(forgejo-opencode-review): Test failures due to tempdir (#39) 2026-08-23 11:38:55 +00:00
scripts Initial commit 2026-06-14 11:32:34 +00:00
.gitignore feat: Replace codegen with my-clients (#30) 2026-08-15 19:36:49 +00:00
AGENTS.md feat: Replace codegen with my-clients (#30) 2026-08-15 19:36:49 +00:00
Cargo.lock feat: Replace codegen with my-clients (#30) 2026-08-15 19:36:49 +00:00
Cargo.toml feat: Replace codegen with my-clients (#30) 2026-08-15 19:36:49 +00:00
CODEOWNERS Migration: codeowners (#20) 2026-07-29 18:17:01 +00:00
flake.lock chore(deps): update dependency code.martials.no/personal/my-nix-lib to v0.2.0 (#36) 2026-08-21 09:39:10 +00:00
flake.nix fix(forgejo-opencode-review): Test failures due to tempdir (#39) 2026-08-23 11:38:55 +00:00
justfile feat: Replace codegen with my-clients (#30) 2026-08-15 19:36:49 +00:00
LICENSE Initial commit 2026-06-14 11:32:34 +00:00
README.md Remove example plugin 2026-07-27 19:50:10 +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
TODO.md openapi-gen-build (#10) 2026-07-25 12:53:14 +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

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.