Skip to content

Commit 67f2fa3

Browse files
committed
ci: test against latest stable BunkerWeb, add release workflow, bump to 1.11
1 parent e1b26b8 commit 67f2fa3

13 files changed

Lines changed: 156 additions & 30 deletions

File tree

.github/workflows/release.yml

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
name: Release
2+
3+
# Cut a GitHub release once per plugin version bump. Mirrors the release-creation
4+
# step of bunkerity/bunkerweb (softprops/action-gh-release, pinned SHA, draft for
5+
# human review) without the heavy build matrix the plugins repo does not need.
6+
#
7+
# Fires only after the "Tests" workflow succeeds on main, and only when the
8+
# version in plugin.json does not already have a release — so a normal push to
9+
# main that does not bump the version is a no-op.
10+
11+
on:
12+
workflow_run:
13+
workflows: [Tests]
14+
types: [completed]
15+
branches: [main]
16+
17+
permissions:
18+
contents: read
19+
20+
jobs:
21+
release:
22+
if: github.event.workflow_run.conclusion == 'success'
23+
runs-on: ubuntu-latest
24+
permissions:
25+
contents: write # create the tag + release
26+
steps:
27+
- name: Checkout the tested commit
28+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
29+
with:
30+
ref: ${{ github.event.workflow_run.head_sha }}
31+
fetch-depth: 0 # full history so generated release notes are complete
32+
33+
- name: Resolve plugin version
34+
id: version
35+
run: |
36+
# All plugin.json carry the same version (kept in lockstep by
37+
# misc/update_version.sh), so any one of them is the source of truth.
38+
version="$(jq -r .version clamav/plugin.json)"
39+
case "$version" in
40+
"" | null | *[!0-9.]*)
41+
echo "::error::unexpected plugin version '$version'"
42+
exit 1
43+
;;
44+
esac
45+
echo "version=$version" >> "$GITHUB_OUTPUT"
46+
echo "tag=v$version" >> "$GITHUB_OUTPUT"
47+
48+
- name: Skip if this version is already released
49+
id: guard
50+
env:
51+
GH_TOKEN: ${{ github.token }}
52+
TAG: ${{ steps.version.outputs.tag }}
53+
run: |
54+
# Match on tag_name across ALL releases, drafts included. A draft has
55+
# no git tag until it is published, so `gh release view <tag>` would
56+
# miss an unpublished draft and we'd create a duplicate on every push.
57+
# The assignment aborts the step if `gh api` fails (no fail-open to
58+
# "create" on an auth/network error).
59+
tags="$(gh api "repos/$GITHUB_REPOSITORY/releases" --paginate --jq '.[].tag_name')"
60+
if grep -Fxq "$TAG" <<<"$tags"; then
61+
echo "exists=true" >> "$GITHUB_OUTPUT"
62+
echo "Release $TAG already exists (including drafts) — nothing to do."
63+
else
64+
echo "exists=false" >> "$GITHUB_OUTPUT"
65+
echo "No release for $TAG yet — creating a draft."
66+
fi
67+
68+
- name: Create draft release
69+
if: steps.guard.outputs.exists == 'false'
70+
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
71+
with:
72+
tag_name: ${{ steps.version.outputs.tag }}
73+
target_commitish: ${{ github.event.workflow_run.head_sha }}
74+
name: ${{ steps.version.outputs.tag }}
75+
draft: true # published by a maintainer after a quick review
76+
generate_release_notes: true # auto-changelog from merged PRs/commits
77+
body: |
78+
BunkerWeb external plugins **${{ steps.version.outputs.tag }}**.
79+
80+
Install a plugin by mounting its directory into the scheduler's `/data/plugins`
81+
(see the README). Coraza ships its WAF API as `bunkerity/bunkerweb-coraza`.

.github/workflows/tests.yml

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,29 @@ jobs:
2121
outputs:
2222
bw_tag: ${{ steps.tag.outputs.bw_tag }}
2323
steps:
24-
- name: Compute BW tag
24+
- name: Resolve latest stable BunkerWeb tag
2525
id: tag
26+
env:
27+
GH_TOKEN: ${{ github.token }}
2628
run: |
27-
# NOTE: hardcoded on purpose and intentionally NOT read from
28-
# COMPATIBILITY.json. If you bump the pinned BunkerWeb version, change
29-
# it HERE only (single source of truth for the workflow).
30-
if [ "$GITHUB_REF" = "refs/heads/main" ] ; then
31-
echo "bw_tag=1.6.1" >> "$GITHUB_OUTPUT"
32-
else
33-
echo "bw_tag=dev" >> "$GITHUB_OUTPUT"
34-
fi
29+
# Always test against the latest STABLE BunkerWeb release, resolved at
30+
# runtime so it never goes stale. GitHub's releases/latest endpoint
31+
# excludes drafts and pre-releases (rc/beta), giving us "stable".
32+
# Same tag on every branch (dev and main) — there is no pinned version.
33+
# `|| true`: a 404 (no stable release yet) or rate-limit makes gh exit
34+
# non-zero, which under `bash -e` would abort the step here with a raw
35+
# error. Swallow it so the empty tag falls through to the case below
36+
# and reports our own diagnostic.
37+
tag="$(gh api repos/bunkerity/bunkerweb/releases/latest --jq .tag_name || true)"
38+
tag="${tag#v}" # release tag may be "v1.6.1"; Docker tags have no "v"
39+
case "$tag" in
40+
"" | *-*)
41+
echo "::error::could not resolve a stable BunkerWeb tag (got '${tag:-<empty>}')"
42+
exit 1
43+
;;
44+
esac
45+
echo "Resolved latest stable BunkerWeb tag: $tag"
46+
echo "bw_tag=$tag" >> "$GITHUB_OUTPUT"
3547
3648
lint:
3749
runs-on: ubuntu-latest

CLAUDE.md

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
44

55
## Repository purpose
66

7-
Official external plugins for [BunkerWeb](https://github.com/bunkerity/bunkerweb). Each top-level directory (`clamav/`, `coraza/`, `discord/`, `slack/`, `virustotal/`, `webhook/`) is an independently-shipped plugin. There is no monorepo build — plugins are consumed by BunkerWeb at runtime by mounting the plugin directory into `/data/plugins` of the `bunkerweb-scheduler` container.
7+
Official external plugins for [BunkerWeb](https://github.com/bunkerity/bunkerweb). Each top-level directory (`authentik/`, `clamav/`, `coraza/`, `discord/`, `matrix/`, `slack/`, `virustotal/`, `webhook/`) is an independently-shipped plugin. There is no monorepo build — plugins are consumed by BunkerWeb at runtime by mounting the plugin directory into `/data/plugins` of the `bunkerweb-scheduler` container.
88

99
## Plugin anatomy
1010

@@ -22,26 +22,46 @@ Coraza is special: it also ships `coraza/api/` — a standalone Go HTTP service
2222

2323
There are **two unrelated version streams**, easy to confuse:
2424

25-
1. **Individual plugin version** in each `plugin.json` (currently `1.10`). Bump with `./misc/update_version.sh <new_version>` from the repo root — it rewrites every `plugin.json` and the README badge in place.
26-
2. **Plugins-collection version** in `COMPATIBILITY.json`, which maps a collection version to the BunkerWeb versions it supports (e.g. `"1.8": ["1.6.0", ...]`). This is the version shown in the README badge and controls compatibility gates.
25+
1. **Individual plugin version** in each `plugin.json` (currently `1.11`). Bump with `./misc/update_version.sh <new_version>` **run from the repo root** (it uses `find .` + `README.md` relative paths) — it rewrites every `plugin.json` **and the README badge** to that plugin version (see `misc/update_version.sh:17-18`).
26+
2. **Plugins-collection version** in `COMPATIBILITY.json`, which maps a collection version (currently `1.8`) to the BunkerWeb versions it supports (e.g. `"1.8": ["1.6.0", ...]`). Controls compatibility gates.
2727

28-
When bumping, check both — the README badge is tied to (2), while `plugin.json` uses (1).
28+
These two streams are independent and `update_version.sh` only touches (1) — it now updates the README badge too (the badge's sed pattern was anchored on a leading `"` that never matched the shields.io URL, so the badge silently froze; fixed to anchor on `/badge/`, so it tracks the plugin version automatically). When bumping: run the script for (1), and edit `COMPATIBILITY.json` by hand for (2).
2929

3030
## Testing
3131

32-
There is no unit-test framework. Tests are end-to-end integration tests under `.tests/`:
32+
Two layers: fast **unit tests** (no Docker) and **end-to-end integration tests** under `.tests/`.
33+
34+
### Unit tests (no Docker, run locally + in CI)
35+
36+
- **Go**`coraza/api/main_test.go` covers the Go WAF service. Run `cd coraza/api && go test ./...`.
37+
- **Python (pytest)**`tests/test_ui_actions.py` parametrizes over every plugin's `ui/actions.py`; `tests/conftest.py` provides a `FakePingUtils` mock for `bw_instances_utils`. Run `pytest tests/ -q`.
38+
- **Lua (busted)**`spec/*_helpers_spec.lua` exercises the pure-logic helper modules (`authentik`, `clamav`, `discord`, `matrix`, `virustotal`) against `spec/helpers/fake_ngx.lua`. Run `busted`. This is why upstream-specific logic is factored into `<plugin>_helpers.lua` — so it's testable outside OpenResty.
39+
40+
### Integration tests (Docker, e2e)
3341

3442
- `./.tests/bw.sh <bw_tag>` pulls `bunkerity/bunkerweb:<tag>` + `bunkerweb-scheduler:<tag>` and retags them as `bunkerweb:tests` / `bunkerweb-scheduler:tests`. Must run first.
35-
- `./.tests/clamav.sh`, `./.tests/coraza.sh`, `./.tests/virustotal.sh` each: copy the plugin into `/tmp/bunkerweb-plugins/<plugin>/bw-data/plugins` (owned `101:101`), copy `.tests/<plugin>/docker-compose.yml`, `sed` the compose file to use the `:tests` tagged images, then `docker compose up --build -d` and poll with `curl`. EICAR file is downloaded for ClamAV; VirusTotal requires `VIRUSTOTAL_API_KEY` env var.
43+
- Per-plugin scripts: `./.tests/clamav.sh`, `coraza.sh`, `virustotal.sh`, `authentik.sh`, and `notifier.sh` (the last covers discord/slack/webhook/matrix together in one multisite stack). Each copies the plugin into `/tmp/bunkerweb-plugins/<plugin>/bw-data/plugins` (owned `101:101`), copies `.tests/<plugin>/docker-compose.yml`, `sed`s it to the `:tests` tagged images, then `docker compose up --build -d` and polls with `curl`. EICAR file is downloaded for ClamAV; VirusTotal requires `VIRUSTOTAL_API_KEY`. Several use mock upstreams instead of the real service: `.tests/virustotal/vt-mock.conf`, `.tests/authentik/mock-outpost.conf`, `.tests/notifier/ratelimit.conf`.
3644
- `.tests/utils.sh` provides `do_and_check_cmd` (runs a command, echoes output on failure, exits on non-zero) and `git_secure_clone` (pinned-commit clone helper). Source it with `. .tests/utils.sh`.
37-
- Run a single plugin's tests: `./.tests/bw.sh dev && ./.tests/clamav.sh` (or `coraza`/`virustotal`). Pass `verbose` as `$1` to dump compose logs on success.
45+
- Run a single plugin's e2e: `./.tests/bw.sh <bw_tag> && ./.tests/clamav.sh` (or `coraza`/`virustotal`/`authentik`/`notifier`). CI resolves `<bw_tag>` to the latest stable release; locally any pulled tag works (e.g. a stable `1.6.1`, or `dev` for the upcoming build). Pass `verbose` as `$1` to dump compose logs on success.
3846
- Tests need `sudo` (for chowning to BW's uid 101) and a working Docker daemon. They leave state in `/tmp/bunkerweb-plugins/` — the scripts clean it at start, but `docker compose down -v` is the safe manual reset.
3947

40-
Discord, Slack, and Webhook have no automated tests — they're exercised by manually pointing a real BunkerWeb instance at a webhook URL.
41-
4248
## CI/CD (`.github/workflows/tests.yml`)
4349

44-
Runs on push to `dev` and `main`. Picks BW tag from branch: `main``1.6.1`, `dev``dev`. Runs CodeQL, then `bw.sh``clamav.sh``coraza.sh``virustotal.sh` sequentially. On `main` only, `./.tests/build-push.sh 1.6.1` builds and pushes the `bunkerweb-coraza` image. **If you update the pinned BW version, update the hardcoded `1.6.1` in `tests.yml` too** — it is not read from `COMPATIBILITY.json`.
50+
Runs on push to `dev` and `main`. A `tag` job resolves the **latest stable BunkerWeb release** at runtime via the GitHub `releases/latest` API (`gh api repos/bunkerity/bunkerweb/releases/latest`, which excludes drafts and pre-releases; a leading `v` is stripped) and feeds that tag to every downstream job — same version on both branches, never pinned. Pipeline:
51+
52+
1. **codeql**`.github/workflows/codeql.yml` (also runs weekly on a cron), matrix `[python, go]`.
53+
2. **lint**`pre-commit run --all-files`.
54+
3. **unit** — matrix `[go, python, lua]` (the unit tests above).
55+
4. **integration**`needs: [tag, lint, unit]`, matrix `plugin: [clamav, coraza, virustotal, authentik, notifier]`; each runs `.tests/bw.sh <tag>` then `.tests/<plugin>.sh`.
56+
5. **build-push**`main` only: `./.tests/build-push.sh <tag>` builds and pushes the `bunkerweb-coraza` image.
57+
58+
There is **no pinned BW version** — the `tag` job always resolves the latest stable release, so the tests track upstream automatically (`COMPATIBILITY.json` is not consulted here). The resolved tag flows into `bw.sh` (pulls `bunkerity/bunkerweb[-scheduler]:<tag>`) and, on `main`, into `build-push.sh` (which also tags the pushed `bunkerweb-coraza` image with it). The job fails fast if the API returns an empty or pre-release (hyphenated) tag.
59+
60+
Two tradeoffs of tracking upstream: (1) the `dev` branch no longer tests against BunkerWeb's `dev` build, so a plugin change that relies on an unreleased BW feature gets no CI coverage until BW ships a stable release; (2) every `main` push republishes `bunkerweb-coraza:latest` (and `:<stable>`) — harmless here because that image is a self-contained Go binary + vendored CRS, independent of the BW base tag, so only the extra tag's value moves.
61+
62+
## Releasing (`.github/workflows/release.yml`)
63+
64+
Releases are cut automatically from `main`. After `Tests` succeeds there, a `Release` workflow (a `workflow_run` trigger on the `Tests` workflow) reads the plugin version from `plugin.json` and, if no release `v<version>` exists yet (**drafts included** — it matches `tag_name` via `gh api`, since a draft has no git tag), opens a **draft** GitHub release with `softprops/action-gh-release` and auto-generated notes. A maintainer reviews and publishes it; a push that doesn't bump the version is a no-op. Two consequences of the `workflow_run` model: the file only fires once it is on the default branch (`main`), and it can't be tested from `dev`. So **cutting a release = `./misc/update_version.sh <ver>` → merge to `main` → publish the draft** the workflow creates.
4565

4666
## Linting — pre-commit is the source of truth
4767

@@ -61,7 +81,17 @@ Runs on push to `dev` and `main`. Picks BW tag from branch: `main` → `1.6.1`,
6181
- Every hook method returns `self:ret(ok_bool, msg, [http_status])`. To deny a request, return `self:ret(true, "reason", utils.get_deny_status())`.
6282
- Gate expensive work at `init_worker` with `utils.has_variable("USE_<PLUGIN>", "yes")` — avoids connecting to upstream services when the plugin is globally disabled. Skip when `self.is_loading` is true.
6383
- Use `ngx.socket` for TCP (see `clamav.lua` INSTREAM protocol) and `resty.http` for HTTP upstreams. Prefer `resty.upload` for streaming request bodies (`clamav.lua` is the reference).
64-
- Cache API responses (SHA-512 of body for file scans) — `virustotal.lua` is the pattern.
84+
- Cache scan results keyed by the file's hash so identical uploads skip the upstream. `clamav.lua` hashes the body with **SHA-512** (`resty.sha512`); `virustotal.lua` uses **SHA-256** (`resty.sha256`, matching VT's file-id) with a 24h TTL — it's the reference for cached HTTP-API lookups.
85+
86+
## Plugin-specific notes
87+
88+
The "Plugin anatomy" layout and the Lua conventions above are shared by all plugins. The non-obvious, per-plugin logic lives in code — these pointers save a re-read:
89+
90+
- **coraza** — the only plugin with an external sidecar. `coraza.lua` talks HTTP to the Go service in `coraza/api/` (`/ping` health check, `/request` for the verdict; the service returns deny/msg). CRS rules are vendored at build time by `coraza/api/crs.sh`, **pinned to a commit hash** with `.git` stripped; the two-stage `coraza/api/Dockerfile` builds the Go binary (multiphase-evaluation build tag) and bakes the rules in. Bumping CRS = bump the hash in `crs.sh`. Image: `bunkerity/bunkerweb-coraza`.
91+
- **clamav** — speaks ClamAV's **binary INSTREAM protocol** over `ngx.socket.tcp` (each chunk framed by a 4-byte big-endian length, terminated by a zero-length frame), not HTTP. Streams the request body via `resty.upload`, scanning only multipart parts that have a real filename (`Content-Disposition` parsing handles quoted, unquoted, and RFC 5987 `filename*`). SHA-512 cache (see above).
92+
- **virustotal** — HTTP to VT API v3 with `VIRUSTOTAL_API_KEY`; scans files and/or IPs. Verdict logic is in `virustotal_helpers.lua` (`evaluate()` compares VT's suspicious/malicious counts to configurable thresholds) — unit-tested in `spec/virustotal_helpers_spec.lua`. SHA-256 cache, 24h TTL. No usable ping endpoint, so `init_worker` does not pre-connect.
93+
- **authentik** — forward-auth: `confs/` ships the nginx snippet; the Lua access handler whitelists outpost paths and forwards/extracts auth headers (needs enlarged proxy buffers for big JWTs).
94+
- **discord / slack / webhook / matrix** — notifiers: build a JSON payload and POST it to an external URL from an `ngx.timer.at` async timer on the `log` hook (denials only), so request latency is unaffected. The generic case; little plugin-specific state.
6595

6696
## Pull requests & commits
6797

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
</p>
44

55
<p align="center">
6-
<img src="https://img.shields.io/badge/bunkerweb_plugins-1.6-blue" />
6+
<img src="https://img.shields.io/badge/bunkerweb_plugins-1.11-blue" />
77
<img src="https://img.shields.io/github/last-commit/bunkerity/bunkerweb-plugins" />
88
<img src="https://img.shields.io/github/actions/workflow/status/bunkerity/bunkerweb-plugins/tests.yml?branch=dev&label=CI%2FCD%20dev" />
99
<img src="https://img.shields.io/github/actions/workflow/status/bunkerity/bunkerweb-plugins/tests.yml?branch=main&label=CI%2FCD%20main" />

authentik/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"id": "authentik",
33
"name": "Authentik",
44
"description": "Protect any site (reverse proxy, served files, ...) with Authentik forward authentication (auth_request).",
5-
"version": "1.10",
5+
"version": "1.11",
66
"stream": "no",
77
"settings": {
88
"USE_AUTHENTIK": {

clamav/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"id": "clamav",
33
"name": "ClamAV",
44
"description": "Automatic scan of uploaded files with ClamAV antivirus engine.",
5-
"version": "1.10",
5+
"version": "1.11",
66
"stream": "no",
77
"settings": {
88
"USE_CLAMAV": {

coraza/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"id": "coraza",
33
"name": "Coraza",
44
"description": "Use Coraza as a library to inspect client request.",
5-
"version": "1.10",
5+
"version": "1.11",
66
"stream": "no",
77
"settings": {
88
"USE_CORAZA": {

discord/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"id": "discord",
33
"name": "Discord",
44
"description": "Send alerts to a Discord channel (using webhooks).",
5-
"version": "1.10",
5+
"version": "1.11",
66
"stream": "yes",
77
"settings": {
88
"USE_DISCORD": {

matrix/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"id": "matrix",
33
"name": "Matrix",
44
"description": "Send alerts to a Matrix room via the Matrix API.",
5-
"version": "1.10",
5+
"version": "1.11",
66
"stream": "yes",
77
"settings": {
88
"USE_MATRIX": {

misc/update_version.sh

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,7 @@ fi
1515
echo "Updating version of plugins to \"$1\""
1616

1717
find . -type f -name "plugin.json" -exec sed -i 's@"version": "[0-9].*"@"version": "'"$1"'"@' {} \;
18-
sed -i 's@"bunkerweb_plugins\-[0-9].*\-blue"@"bunkerweb_plugins-'"$1"'-blue@' README.md
18+
# Anchor on the shields.io URL path (/badge/), not a leading quote — the badge
19+
# is inside src="…/badge/bunkerweb_plugins-<ver>-blue", so the old `"bunkerweb…`
20+
# pattern never matched and the badge silently went stale.
21+
sed -i 's@/badge/bunkerweb_plugins-[0-9][0-9.]*-blue@/badge/bunkerweb_plugins-'"$1"'-blue@' README.md

0 commit comments

Comments
 (0)