- Shell 40.1%
- Python 40.1%
- Go 10.8%
- Dockerfile 8.9%
- C 0.1%
|
|
||
|---|---|---|
| .woodpecker | ||
| docs/plans | ||
| images/rust-tauri | ||
| plugins | ||
| .gitignore | ||
| README.md | ||
ci-plugins
Versioned Woodpecker CI plugin catalog for the nastoupil estate.
Woodpecker has no include, extends, import, or remote-config keyword,
and YAML anchors are file-local only (verified upstream 2026-08-18). Plugins and
extensions are the entire reuse surface. This repo is that surface — ADR-0028
Decision #3, "Shared pipeline logic is packaged as versioned, privately-hosted plugin images" made concrete.
Built by W10.3. One image, referenced from many repos; never the reverse.
Catalog
| Plugin | Image | Status |
|---|---|---|
notify |
git.nastoupil.org/brandon/notify |
v0.2.5 |
cache-restore |
git.nastoupil.org/brandon/cache-restore |
v0.3.2 |
doc-gates |
git.nastoupil.org/brandon/doc-gates |
v0.1.0 |
pr-review |
git.nastoupil.org/brandon/pr-review |
v0.1.4 |
npm-publish |
git.nastoupil.org/brandon/npm-publish |
v0.1.0 |
buildkit-build-push |
git.nastoupil.org/brandon/buildkit-build-push |
v0.1.5 |
trivy-gate |
git.nastoupil.org/brandon/trivy-gate |
v0.1.2 |
promote-by-digest |
git.nastoupil.org/brandon/promote-by-digest |
v0.1.3 |
cosign-sign |
git.nastoupil.org/brandon/cosign-sign |
v0.1.4 |
ghcr-promote-drift |
git.nastoupil.org/brandon/ghcr-promote-drift |
unreleased |
forgejo-sticky-comment |
git.nastoupil.org/brandon/forgejo-sticky-comment |
unreleased |
sops-no-duplicates |
git.nastoupil.org/brandon/sops-no-duplicates |
v0.3.0 |
vps-ssh |
git.nastoupil.org/brandon/vps-ssh |
v0.2.0 (v0.3.0 reserved) |
terraform-stack |
git.nastoupil.org/brandon/terraform-stack |
v0.2.2 |
tailscale-acl |
git.nastoupil.org/brandon/tailscale-acl |
unreleased |
governor-gate |
git.nastoupil.org/brandon/governor-gate |
unreleased |
repo-gates was retired before merge after three review rounds found five
independent fail-open paths. The existing repository-local validation jobs stay
authoritative; duplication is cheaper than a reusable gate that can report a
false PASS.
buildkit-build-push, trivy-gate, promote-by-digest, and the deliberately
narrow cosign-sign signer are W10.6. cosign-sign exists separately so
the image-restricted signing key is unreachable from build, scan, and promote
steps. The wave plan's plugin list is the baseline catalog; this signer is the
secret-isolation boundary required by the approved signing design.
Golden pipeline archetypes
Archetype A is the repository-local validation shape documented by its first
consumers. W10.5 adds two deliberately small variants: B is Python lint,
typecheck, and a supported-version test matrix; D is Node package gates plus
a main-only registry publish. Gates remain repository-local scripts/package
commands; npm-publish is only a credential-constrained publisher, not a gate
or a replacement for retired repo-gates.
Base images
Everything under plugins/ is a Woodpecker plugin: an ENTRYPOINT-only
container driven by settings:, constrained by Container.IsPlugin(), and
eligible for image-filtered secrets. A base image is the opposite — a
commands: step runs in it, it consumes no secrets, and it has no entrypoint
contract. They live under images/ so they are not in the same namespace the
secret extension matches against.
| Image | Image path | Status |
|---|---|---|
rust-tauri |
git.nastoupil.org/brandon/rust-tauri |
v0.1.0 |
The tag discipline below applies to base images unchanged — never re-point a
published tag, never publish latest, record the digest.
⛔ images/ has NO CI coverage, and that is a real gap rather than an
oversight. Both steps in .woodpecker/ci.yaml enumerate explicit plugins/…
paths — there is no Dockerfile lint, no image build, and no glob that would pick
up a new tree. So a green pipeline on a change under images/ is evidence of
nothing about that change, and everything asserted about a base image here has
been verified by hand. Stated plainly because the alternative is a second
top-level artifact tree that looks gated and is not. Closing it needs a lint or
a build step with its own tuning, which is not this change's scope.
⚠ rust-tauri is amd64-only, a deliberate deviation from rule 5 below.
Rule 5's own text concedes the multi-arch requirement is "a cheap hedge against
a future arm64 node" rather than a fix for a live scheduling problem — there is
no arm64 node in the cluster. The hedge is cheap for a ~20 MB Go plugin. For a
Rust/Tauri toolchain image two orders of magnitude larger it means a
QEMU-emulated build of the entire dependency set, and the whole point of that
image is to remove build cost. Recorded here rather than dropped silently, so
that adding an arm64 node surfaces this as a known gap instead of a mystery.
Every plugin under plugins/ remains multi-arch.
⛔ Tag discipline — read before publishing anything
Plugin versioning here is plain container image tags. There is no manifest, no lockfile, and no registry-side resolution. Whatever discipline exists, this section is it, and nothing enforces it but review.
-
Semver,
vMAJOR.MINOR.PATCH, and tags are immutable. Never re-point a published tag at a new build. A consumer that pinnedv0.1.0and gets different behaviour tomorrow has no way to discover that happened — there is no digest pin in the consumer's pipeline file to contradict the tag. -
Publish the digest alongside the tag, and record it here. The digest is the only identifier in this system that cannot lie.
-
Never publish
latest. A floating tag turns "which version broke this" into an unanswerable question, and Woodpecker gives no way to see what a step actually resolved to after the fact. -
Bump the MINOR on any behaviour change, including a defaults change. Consumers have no changelog to read except this repo's git log. ⚠ Behaviour means the contract a consumer depends on: inputs accepted, outputs produced, exit codes, defaults, delivery semantics. Diagnostic text is not behaviour — a reworded error message on a failure path gets a PATCH. Worked examples on this repo: v0.1.6 → v0.2.0 accepted a new input spelling and defined precedence between two, so it was MINOR (and was renumbered from v0.1.7 for exactly that reason). v0.2.0 → v0.2.1 → v0.2.2 only changed what two gates print; same inputs, same exits, same delivery — PATCH. Stated here because the distinction was left implicit and a reviewer correctly flagged the ambiguity rather than the choice.
-
Every image is multi-arch:
linux/amd64ANDlinux/arm64. ⛔ The rationale originally given here is FALSE against live cluster state, and the rule is kept on different grounds. It read: "echo(192.168.20.23) is the estate's only arm64 node and carries the GB10 GPU. An amd64-only plugin … silently constrains scheduling to four of five nodes." Measured 2026-08-25:NAME ARCH IP alpha amd64 192.168.20.13 bravo amd64 192.168.20.14 charlie amd64 192.168.20.12 delta amd64 192.168.20.17Four nodes, all amd64.
echois not a cluster node at all, and there is no arm64 node to schedule onto — so the "four of five" figure was wrong in both its numerator and its premise.The rule stands anyway, because multi-arch costs one
--platformflag and an amd64-only image is the kind of constraint that surfaces months later as capacity that mysteriously never gets used. But it is now a cheap hedge against a future arm64 node, not a fix for a live scheduling problem — and if an arm64 node is never added, this rule is worth revisiting rather than obeyed on a rationale that was never true. Assert it withdocker buildx imagetools inspect, never by trusting the build command's flags.
Published digests
⚠ cache-restore v0.3.1 was published and immediately superseded by v0.3.2,
and the row is kept rather than deleted. v0.3.1 carried the correct fix but a
code comment quoting build-freshness numbers that had not been reproduced
first-hand. Correcting the comment changed the binary — Go's build ID
incorporates source content, so the change is not a no-op even under
-trimpath -ldflags='-s -w' (verified: two builds, differing sha256). Reusing
the tag is forbidden by the discipline above, and shipping a tag whose source
no longer matches is worse than one extra version. No consumer ever pinned
v0.3.1.
terraform-stack follows the same immutable supersession rule. v0.2.0
exported the wrong Cloudflare variable name, v0.2.1 corrected the runtime,
and v0.2.2 additionally emits and validates the saved-plan SHA-256 needed for
W10.9 acceptance. Consumers must use v0.2.2 by its index digest; none of the
older tags are moved or overwritten.
| Image | Tag | Index digest | Published |
|---|---|---|---|
doc-gates |
v0.1.0 |
sha256:7c398c37b78d04ad2e792fdd4d138b38566dae419e5a0ba66383299f10c05f8e |
2026-08-26 |
notify |
v0.2.5 |
sha256:90a891d057f2c90274efa53778c663fa7b5a93312ca59f2f28028cdf0c51b544 |
2026-08-25 |
notify |
v0.2.4 |
sha256:571bc3bccaf711d671fd20c29412ace9f7db359ebca0a0ff9922d62e6f5840e5 |
2026-08-25 |
notify |
v0.2.3 |
sha256:c32a14b7310329473a538f13095667332b0142d4915b811c5631ba9fc9c43e71 |
2026-08-25 |
notify |
v0.2.2 |
sha256:8c9b1e12f72b479732f4f2c5cc61a76a71ab6c918b6c7c6d0dd8343fab282f45 |
2026-08-25 |
notify |
v0.2.1 |
sha256:6219a93b22b17010def03e87963427e6a1416cf1dafc64caf6102c42d051638f |
2026-08-25 |
notify |
v0.2.0 |
sha256:07978e7ec2f0193100ca59f065eaa85efbebbb753e666cd030a97d4cdca046b7 |
2026-08-25 |
notify |
v0.1.6 |
sha256:9e39806bbc7b00ba74d8777e712b338c294cb74c0e2c856061d4f9a8ffb8d8cc |
2026-08-24 |
cache-restore |
v0.3.2 |
sha256:0d136d411bd9e843958024b4fdd7ceb6deeae39f4dc55b4756338329d245e187 |
2026-08-28 |
cache-restore |
v0.3.1 |
sha256:2024553fe051f277ef8b40542137fd8fdfe7e9b4d1917834a23c46518dce2a6f |
2026-08-28 |
cache-restore |
v0.3.0 |
sha256:369a5cd387af669c16c4cc06596b45a263bd7cd7d7f7bce06e5ea0055e1dde43 |
2026-08-24 |
pr-review |
v0.1.0 |
sha256:42c5094e9add73e8a50d84a718b5deeb83855b85700db4130b7d8d6c47e4c526 |
2026-08-25 |
pr-review |
v0.1.1 |
sha256:eae8c96bcfb71f1f812e6bd31eeb1854aeb9aed9f63a879704ab1b8bbabbef99 |
2026-08-25 |
pr-review |
v0.1.2 |
sha256:929bac3e7e4a3b5d7bdd8e16c478d8da5a045d8b303545ee95528392f81db72d |
2026-08-25 |
pr-review |
v0.1.3 |
sha256:a5706105f2de8d72945c6a1fef9f053363423f6232e9ac28257b2b0b5ad2db73 |
2026-08-26 |
pr-review |
v0.1.4 |
sha256:162c072ebc4f2876515f870a827033b0546282f81f3dc6fee454d646907da128 |
2026-08-26 |
npm-publish |
v0.1.0 |
sha256:cd575d0d8387fbb798c9e75a9aee5a6fd540c716c34ec85adccebf9a4593c727 |
2026-08-27 |
npm-publish |
v0.1.1 |
sha256:0ec8786d615bd8860a2c3abc4d505d034d7971e35cd24019f1bc4431e23e494e |
2026-08-27 |
buildkit-build-push |
v0.1.1 |
sha256:ef734a337ec6c7b42236e8fd567e60744ccfe3425975f9944cca8be9c34a292f |
2026-08-27 |
buildkit-build-push |
v0.1.2 |
sha256:dd4d4e60bb6bb7eda950696010a5b7f5260e628d9ea766f2b13642faf91937c4 |
2026-08-27 |
buildkit-build-push |
v0.1.3 |
sha256:a32f5006319ad92d4f3aaf03e2ccbb4a4576782bbfcc6862b9fa986e02a4e5c3 |
2026-08-27 |
buildkit-build-push |
v0.1.4 |
sha256:a0c15723220a0b2c727064ec4a5884a9ffbfbb596cedac6f4ec316a75204f087 |
2026-08-27 |
buildkit-build-push |
v0.1.5 |
sha256:bb8e2f52ef1fa5130c989154eec0cf31c3ee114e5debe80763f41d27469faa46 |
2026-08-28 |
trivy-gate |
v0.1.1 |
sha256:82d7d57b069842e72ac546c6b34a97f1311057d1cbdb3ce7f0bb10d6e1a97d79 |
2026-08-27 |
trivy-gate |
v0.1.2 |
sha256:c700133f3f376042eb07bbb9f6ac4823cd64081e42abf8ac895707305a0280ac |
2026-08-27 |
promote-by-digest |
v0.1.1 |
sha256:f9510c35522f03a75e487a4d68f286e865e7bf81f7a757b4073feb327a2fb58a |
2026-08-27 |
promote-by-digest |
v0.1.2 |
sha256:44827caa0a53d5671f4642648a60ad1bc10e7974078826307d3eb9164ef06087 |
2026-08-27 |
promote-by-digest |
v0.1.3 |
sha256:6a765f8e19a9cdb8a9139048dd9cd499ce064c0643072cec57af96d4a9d83547 |
2026-08-28 |
ghcr-promote-drift |
v0.1.0 |
sha256:d9aae110f1dad541adf844ecb108c75d27b310e0d81450312c94a34d1810b2e5 |
2026-08-28 |
forgejo-sticky-comment |
v0.1.0 |
sha256:593b02522b10e66d996192ecab984e3c43db773b5bfc6dd695a6cdad9aac4ac7 |
2026-08-28 |
cosign-sign |
v0.1.1 |
sha256:b6be5ff6601d7044553451dc9166b4b2c4965a03158074b30b5141dc343b9a5b |
2026-08-27 |
sops-no-duplicates |
v0.1.0 |
sha256:76e36e92c0f5090c1d05b700ffc4991219700700952fef5ca707782f11932784 |
2026-08-28 |
sops-no-duplicates |
v0.2.0 |
sha256:0629e2be7c4a6fb395f8de1f27dc49ae8821ff06d734fac1d9b1870190c0ff1d |
2026-08-28 |
sops-no-duplicates |
v0.3.0 |
sha256:87f406532c7b1500303726640a6939dc4d128a2b38ffc0a3f02e68f00639f6d9 |
2026-08-28 |
vps-ssh |
v0.1.0 |
sha256:d40bade05756a7da678abba6f52c0dc373b9479b59bab54c2d094510b296b550 |
2026-08-28 |
vps-ssh |
v0.2.0 |
sha256:4d4f0c16c1d262b947c8d8e2daf13c1b22ef22813fe33a4003479e881160f6b2 |
2026-08-28 |
terraform-stack |
v0.1.0 |
sha256:cb6638745eb14f6bd8164de62b77255e293719f0c291a06c8fad71e7678c2f02 |
2026-08-28 |
terraform-stack |
v0.2.0 |
sha256:42a10950db6d05c1dd7ed7c911f16c0f3715eb09a114846c3650445ce0a5a542 |
2026-08-29 |
terraform-stack |
v0.2.1 |
sha256:12553def1b3d91777c93207e6bbdcf7f72dd4e51d9e572cbcde57d54fa141471 |
2026-08-29 |
terraform-stack |
v0.2.2 |
sha256:074efffd871ae73d8fb75e1b793e5f7fb63998797e3d231b49bc32653f5d8e83 |
2026-08-29 |
rust-tauri (base image) |
v0.1.0 |
sha256:f8710bb56e1ffe0c55d603e0d5427c45c3b028221402267b0ced92eed788af1b |
2026-08-28 |
✅ The eight defective named versions are deleted:
repo-gates:v0.1.0–v0.1.2 and v0.2.0, plus
cache-restore:v0.1.0–v0.1.2 and v0.2.0. Verified 2026-08-24 in both
directions: the public Forgejo package API lists none of those versions, and
anonymous manifest inspection fails for all eight while notify:v0.1.6 still
resolves. Untagged child and attestation digest records remain; those are not
named versions and are left for registry garbage collection.
repo-gates:v0.1.0refused to run in any container (gitsafe.directoryrejects a differently-owned workspace). Failed closed, but unusable.repo-gates:v0.1.1and later versions fail OPEN. Reviews found unquoted expansions undergoing pathname expansion against the gated repo, so an ordinary untracked file — or a committed file named after a settings-table row — silently disabled the gate over a cleartext secret. This is the one that matters.cache-restore:v0.1.0allowed root command execution viataroption injection, and its repo slug was not injective.cache-restore:v0.1.1fixed those but shipped amktemptemplate busybox rejects, so every save failed in the real image while the host suite was green.cache-restore:v0.2.0still permits inheritedLD_PRELOADto execute before its shell entrypoint and has an untested load-bearing tar operand guard.
✅ notify's v0.1.0–v0.1.5 and archtest-amd64only are DELETED (2026-08-24, verified by anonymous pull returning not-found for
each, not by the API's 204). At the time of that sweep v0.1.6 was notify's only published tag (later tags came after -- see the digest table above, and see the "Do not quote a tag list here" note for why none is quoted); the other plugins' superseded tags
were deleted in the same sweep — see the ✅ line above (the "eight defective
named versions" block), which this clause contradicted until now.
⚠ Do not quote a tag list here. Two successive attempts to state one were
wrong within minutes: the first said the superseded tags were not deleted,
the second omitted v0.2.2 — the tag the very same commit published and added
to the digest table above. A list of live registry state, hand-maintained in a
file that does not gate on it, is stale the next time anything is published.
Read it from the registry:
skopeo list-tags --no-creds docker://git.nastoupil.org/brandon/notify
Kept as a record because the sequence is the tag-discipline rule working rather
than a mess: v0.1.0 shipped with gateway_url defaulting to the public
atlas.nastoupil.org, which 302s to Authentik on every path. Rule 1 says a
published tag is never re-pointed, so the fix shipped as v0.1.1 — and each
later round of review found more, so each fix took a new number rather than
quietly replacing the last. v0.1.1 is the one worth naming: it posts to ntfy
unauthenticated when the gateway is down, which is why leaving it pullable
was not acceptable.
⚠ Untagged child and attestation manifests were deliberately NOT deleted — some
are a live tag's own layers, and removing them would break it. The control after
deletion was an anonymous pull of v0.1.6 plus a real delivery.
⛔ A count stood here ("the 27 untagged…") and is gone for the same reason the tag list above is gone. It was a hand-maintained snapshot of live registry state, and every publish on this branch moved it — which is the argument, not a number to re-quote. Query it instead:
curl -s 'https://git.nastoupil.org/api/v1/packages/brandon?type=container&q=notify&limit=50' \
| jq '[.[] | select(.version | startswith("sha256:"))] | length'
⛔ q= and limit= are both load-bearing, omitting either fails SILENTLY,
and limit=50 is itself a bound that will eventually bite. Forgejo caps this
endpoint at 50 regardless of what you ask for. The notify package grows by 5
items per publish, so it is a small number of releases from truncating the page
— silently, with no error, returning a wrong number by the same mechanism this
paragraph describes. ⚠ A live item count stood here and is deliberately gone:
a hand-maintained registry snapshot inside the paragraph arguing against
hand-maintained registry snapshots. Run the command to see where it stands. When it truncates, paginate; do not raise the number. A hardcoded
bound against a monotonically growing set is the same shape as the
hand-maintained count this paragraph replaced. The first version of this command had neither and returned 26
against the 39 stated in the paragraph it replaced — in the paragraph arguing that
hand-maintained snapshots go stale. Forgejo pages this endpoint at 30 by
default with a hard cap of 50, and returns a truncated page with no error;
without q= it also aggregates every package under brandon, not just
notify. So the replacement for a stale number was a command that reported a
different wrong number with no signal it had truncated. Deleting a duplicated
fact only helps if what replaces it actually answers the question.
Consuming a plugin
Plugins are settings-only by construction. ⛔ A plugin step cannot also carry
commands:, entrypoint:, OR environment: — three conditions, not two,
and the third is the one that gets missed. Container.IsPlugin()
(pipeline/frontend/yaml/types/container.go, read at v3.17.0):
return len(c.Commands) == 0 && len(c.Entrypoint) == 0 && len(c.Environment) == 0
⚠ Why the third one matters more than it looks. A secret with a non-empty
image allow-list is refused to a NON-plugin step, hard enough to fail the
pipeline compile — "secret %q is only allowed to be used by plugins …"
(truncated; upstream continues "(a filter has been set on the secret). Note:
Image filters do not work for normal steps"). So
adding an environment: block to pass a token silently disqualifies the step
and breaks any image-filtered secret it uses. Pass credentials under
settings: instead; notify reads both spellings as of v0.2.0 precisely so
this is possible. (An earlier version of this paragraph listed only two
conditions, and that omission is what made notify:v0.1.6 incompatible with
an image-filtered secret.)
A plugin also runs non-privileged unless its image is listed in
WOODPECKER_PLUGINS_PRIVILEGED on the server.
steps:
- name: notify-on-failure
image: git.nastoupil.org/brandon/notify:v0.2.5@sha256:90a891d057f2c90274efa53778c663fa7b5a93312ca59f2f28028cdf0c51b544
settings:
title: "${CI_REPO} failed"
# ⛔ SET `topic:` EXPLICITLY. The default is `alerts`, and the estate's
# CI credential is write-only on `ci` — publishing to `alerts` with it is
# REFUSED, and per ADR-0021 §1 a refused delivery is a counted drop that
# still exits 0. So omitting this line does not fail the build, it just
# silently stops notifying: the exact failure this plugin exists to close,
# reintroduced inside its own usage example. Use whatever topic YOUR
# credential is granted; the point is that it is stated, not defaulted.
topic: ci
# ⚠ Under settings:, NOT environment: — an environment: block makes the
# step a non-plugin and breaks any image-filtered secret. Secret names
# are DASHED; the underscored form cannot exist as a K8s Secret.
#
# ⛔ ntfy ONLY in this example, and NOT because one credential is enough
# in general. W10.3 PR11 retires `atlas-internal-hook-token` from the
# estate's CI secret plane (cluster-gitops PR #328); once that lands,
# copying such a line names a secret the plane will not serve — a COMPILE
# failure (status=error, zero steps). Check the inventory rather than this
# comment for what is served right now.
#
# ⚠ It was retired for two reasons, and only the first is about topics.
# The gateway's allow-list does not admit `ci`, the topic the CI
# credential is granted, so that pairing returned 400 every time. AND, on
# an ADMITTED topic, the gateway's own metrics show every dispatch dropped
# with `transport_error` and zero deliveries — measured, on seven
# consecutive days, the full window those pods have existed. Do not add a gateway credential on the theory that a different
# topic fixes it: verify a delivery first, and if you do, mint a
# notify-scoped credential rather than reusing a shared bearer.
#
# ⚠ If you DO supply the gateway token, supply the ntfy one too. With only
# the gateway token a gateway outage leaves the fallback uncredentialled
# and the notification is refused rather than sent unauthenticated — a
# counted drop, not a delivery.
ntfy_token:
from_secret: ntfy-token
when:
status: [failure]
Registry
git.nastoupil.org — the Forgejo instance on the VPS (ADR-0029), speaking OCI
registry/2.0.
⚠ This repo is public so its packages are anonymously pullable, which is
what lets a GitHub-hosted runner consume doc-gates without a credential
(W10.3 D1, option B).
✅ PROVEN 2026-08-26, W10.3 PR5 signal 15. A GitHub-hosted ubuntu-latest
runner pulled a digest-pinned image from this registry anonymously:
Digest: sha256:90a891d057f2c90274efa53778c663fa7b5a93312ca59f2f28028cdf0c51b544
Status: Downloaded newer image for git.nastoupil.org/brandon/notify@sha256:90a891d0...
⛔ AND IT DOES NOT FLIP TO PRIVATE IN PR5. This paragraph used to say it
did, and that two credentials would appear with it — a GitHub secret on
cluster-gitops plus a dockerconfigjson wired via
WOODPECKER_BACKEND_K8S_PULL_SECRET_NAMES. The plan's D1 caveat 2 is explicit
that a credential is minted only if anonymous pull fails. It did not fail,
so PR5 minted nothing and cluster-gitops still holds four GitHub secrets, not
five. Flipping this repo private later is still possible, but it is a new
decision that re-incurs both credentials — not something already scheduled.
Testing a plugin
Every plugin carries tests alongside its implementation. cache-restore also
has a production-image suite because its retired shell version twice diverged
between the host and Alpine/busybox.
⚠ Nothing runs them automatically yet. This repo has no .woodpecker/
pipeline — a real gap for a catalog whose thesis is CI reuse, and a deliberate
one: adding a pipeline here first would make this repo the estate's first real
pipeline, which is the precondition PR7's OM-9 re-run is scheduled against.
That precondition was unspent when this was written: no .woodpecker/
config existed anywhere in the estate, and PR1 publishes an image and adds no
pipeline. ⛔ No longer true — homelab-bootstrap/.woodpecker/validate.yaml landed with
W10.3 PR3 and is the estate's only live Woodpecker CI, and the only CI of
any kind among the archived-mirror repos.
⚠ Not "the estate's only live CI" full stop, and ADR-0035 is the authority
that refutes it, not the one that supports it. That ADR's own table lists
cluster-gitops and nastoupil-home as live mirrors — D2 still binds, and
cluster-gitops' GitHub Actions validate workflow was running successfully at
the time of writing. An earlier version of this sentence made the broader claim and
cited ADR-0035 for it. The ⛔ paragraph just
below already said the wave would add it; this one had not caught up.
⛔ And nothing in W10.3 closes this repo's gap. The wave adds .woodpecker/
to homelab-bootstrap (PR3) and dotfiles-dev (PR4), and adds one Actions job
to cluster-gitops (PR5). None of those runs the suites below. So the present
condition is simply that they run when a human types the command, and closing
that is unowned follow-up work — not something already scheduled that a
reader can wait for.
bash plugins/notify/test/notify.test.sh
bash plugins/cache-restore/test/image.test.sh
⛔ Every case must be capable of failing. Three of W10.2's verification
steps were not, and each cost real time: docker exec woodpecker-server env
(the image has no env binary — use docker inspect), SOPS_AGE_KEY_FILE
without HOME isolation (sops falls back to the default path, so an empty file
and a wrong key both return 0), and count(metric) < 2 (an empty vector
yields no series, so the rule never fires — use sum(...) or vector(0)).
Prove a suite can fail by mutating the property it claims to test and watching it go red — and treat the mutation score as the real number, not the case count.
notify's first suite scored 40%: 32 of 53 mutations survived a green
32/32 run. It tested the control flow thoroughly and the security hardenings
not at all — every hardening this catalog advertises was guarded by an
assertion that could not fail, including one whose own comment claimed it
inspected what the stub received. Three independent reviews converged on it.
Six review rounds took it from 40% to 152 cases with every reported mutation killed — 172 cases as of v0.2.2. "Every reported" is the honest phrasing, not "mutation-clean". Each round found real defects the previous round's fixes had missed, twice in the same shape: a hardening applied to one value and not its siblings, or to one leg and not the other.
What the suite now kills: the @-body swap, both Authorization headers, CRLF
stripping on every header-bound value and the log-bound marker fields, topic
validation, the gateway payload's shape and each field, both default endpoints
and their trailing-slash trimming, the marker's escaping and each of its
fields, the failover credential check, allow_anonymous exact-matching, the
gateway-202 dedup branch, the payload-build-failure branch (reached by running
with jq off PATH), and the timeout-vs-transport-failure distinction on both
legs.