Compare commits

..
Author SHA1 Message Date
claude-botandClaude Opus 5 c0791e2747 docs(zigbee2mqtt): record the WS90 ZCL reporting configuration
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
The Wetterstation now carries a non-default reporting configuration: min
interval 5s on all ten reportable attributes, and tighter reportable-change
deadbands on temperature, humidity and pressure. None of it is expressible
in configuration.yaml - Zigbee2MQTT has no settings-schema field for
reporting configuration - so it lives only on the device and in Z2M's
database.db. This documents it and how to restore it.

min was 10s against an 8.8s transmit interval. Since min is a hold-off, every
change-triggered report was held past a transmit cycle and landed on the
next, so attributes reported at best every 17.6s and drifted out of phase.
The published stream showed no 8.8s periodicity at all. At min 5 each
attribute reports on its own transmit and the stream carries one publish per
cycle, measured at a mean gap of 8.69s. Publish volume fell from ~631/h to
~445/h because each publish is now a whole cycle rather than a fragment.

The deadbands come from the shared modernExtend helpers, which the WS90
definition composes without overrides. They are anti-chatter values sized for
battery-powered indoor sensors. Temperature was 1.00 C, giving a staircase
that moved about twice an hour; it is now 0.1 C, matching the resolution the
device actually reports. Pressure was 5 kPa - 50 hPa, which real barometric
pressure never traverses, so it only ever updated on the hourly heartbeat
that pressure_trend and weather_condition derive from. Note the same library
configures the identical ZCL attribute with a 5 hPa deadband in its older
reporting.ts helper, so the two code paths disagree by 10x.

Anything that makes Z2M re-run configure on the device reverts all of it, and
a fresh PVC will not have it. The restore commands are written out literally
rather than generated by a shell helper, because the nested quoting through
`kubectl exec -- sh -c` is where this goes wrong; each was executed and
verified as written, including the read-back, and re-applying is a no-op.

No behaviour change from this commit itself - the device was reconfigured at
runtime, this only records it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 19:15:22 +02:00
k.renner 2e00109f66 Merge pull request 'docs(zigbee2mqtt): the WS90 merge does not make the glitch detectable' (#561) from fix-ws90-merge-overclaim into master
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#561
2026-08-24 17:07:34 +00:00
security-bot 394d8995a6 chore(image-automation): bump woodpeckerci/woodpecker-server:v3.18.0
ci/woodpecker/cron/e2e Pipeline was successful
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Updates managed by image-automation-controller.
2026-08-24 10:50:03 +00:00
security-bot 0685044ce6 chore(image-automation): bump woodpeckerci/woodpecker-agent:v3.18.0
Updates managed by image-automation-controller.
2026-08-24 10:50:01 +00:00
claude-botandClaude Opus 5 06ffc3c910 docs(zigbee2mqtt): the WS90 merge does not make the glitch detectable
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
#560 claimed a merged episode "surfaces as a single all-zero frame". It
does not, and the difference decides whether a downstream filter is even
possible.

The zeros propagate attribute by attribute through the burst, so the
merged frame carries whatever subset had zeroed when that burst ended.
Across three episodes captured at nanosecond resolution the propagation
happened to complete, which is where the wrong generalisation came from.
Nothing guarantees it. A burst ending mid-propagation publishes a frame
with temperature at zero while humidity and pressure still read normally,
which is a physically consistent measurement - Z2M recomputes dew_point
from the false temperature - and is not separable from a genuine winter
reading.

The zeros also begin inside an otherwise normal transmit cycle, roughly
20ms after a valid reading in the same burst (measured: 27.7ms, 19.8ms,
22.0ms). They occupy a regular reporting slot, so no timing property
distinguishes them either.

Since the merge is last-wins and the zeros arrive at the tail of the
burst, it resolves that burst to the zeros and discards the valid reading
that led it. So debounce trades six bad samples per episode for one, at
the cost of one good sample. That is a bound on the corruption, not a
filter, and the comment now says so.

No behaviour change; the debounce value and device stanza are untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 06:41:14 +02:00
k.renner 89582c9c9f Merge pull request 'chore(ci): bump alpine to 3.24.1 in deploy-infrastructure' (#551) from bump-stale-ci-images into master
ci/woodpecker/push/deploy-infrastructure Pipeline was successful
ci/woodpecker/cron/e2e Pipeline was successful
ci/woodpecker/cron/update-audit Pipeline was successful
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#551
2026-08-23 20:59:54 +00:00
k.renner 3f8e9fe333 Merge pull request 'fix: [alert] CephHealthWarn (rook-ceph) (#557)' (#558) from alert-fix/issue-557 into master
Reviewed-on: k.renner/homelab#558
2026-08-23 20:59:29 +00:00
k.renner fd8412b9d7 Merge pull request 'fix: [alert] KubeJobFailed (openbao) (#554)' (#555) from alert-fix/issue-554 into master
Reviewed-on: k.renner/homelab#555
2026-08-23 20:58:45 +00:00
k.renner 1c51b78fda Merge pull request 'fix: [alert] KubeJobFailed (wordpress) (#550)' (#553) from alert-fix/issue-550 into master
Reviewed-on: k.renner/homelab#553
2026-08-23 20:58:28 +00:00
k.renner 96a51f1337 Merge pull request 'docs(zigbee2mqtt): correct the WS90 debounce rationale' (#560) from fix-ws90-debounce-rationale into master
Reviewed-on: k.renner/homelab#560
2026-08-23 20:57:51 +00:00
claude-botandClaude Opus 5 87c6f1235c docs(zigbee2mqtt): correct and trim the WS90 debounce comment
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
The value is unchanged and correct; the comment committed alongside it in
#559 was neither accurate nor the right place for most of what it said.

Accuracy. It cited the device's ZCL reporting configuration as the bound,
reading minRepIntval 10 as "the station updates every 10s". minRepIntval
is not a cadence: with maxRepIntval 3600 and a reportable change set, the
ZCL reporting model gives periodic reports at the MAXIMUM interval — once
an hour — plus change-driven reports each held back so it lands no sooner
than 10s after the previous. It is a hold-off on change-triggered reports
and says nothing about the gap between reports of different attributes,
which is what a debounce window sees.

The bound that applies is the documented transmit interval — readings
every 2s, transmitted every 8.8s — against Z2M's documented rule not to
set debounce above the sensor's update interval. Shelly's knowledge base
rounds the same figure to 9s.

It also claimed the window had to exceed 1s because "the recovery burst
straddles a second boundary", inferred from log lines stamped 11:42:11
and 11:42:12. Those are second-resolution timestamps and cannot
distinguish 10ms from 900ms. No lower bound was ever established.

Length. The rest was commit-message material sitting in a config file:
justification for the chosen value, gap-distribution and run-length
measurements, and notes reconciling my own earlier misreadings. Comments
state facts about the system, not the history of arriving at them. What
remains is what the device does, what debounce does to it, and the
constraint on the value — 41 lines down to 13.

Verified before writing: post-debounce frames arrive complete (all 22
fields, no partials) at a settled ~10s cadence.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 22:39:20 +02:00
k.renner 1ce3f449d4 Merge pull request 'feat(zigbee2mqtt): 2.13.0 and debounce the WS90 report burst' (#559) from worktree-z2m-2.13.0-ws90-debounce into master
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#559
2026-08-23 20:06:50 +00:00
claude-botandClaude Opus 5 91e55d3b56 feat(zigbee2mqtt): 2.13.0 and debounce the WS90 report burst
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Two changes to the Wetterstation (Ecowitt WS90, 0xc02cedfffe958f40), both
aimed at the station's recurring zero-drop glitch.

2.13.0 carries zigbee-herdsman-converters#12748, which discards the ZCL
non-value markers (0xffff / 0xff / 0xffffff) the WS90 emits when a reading
is unavailable. Without it those markers are scaled and published as
measurements — the 6553 m/s gust readings in Koenkk/zigbee2mqtt#31048. The
release also recovers the rain rate after a counter reset, which is the
same lifetime-counter dip that packages/wetterstation.yaml compensates for
on the Home Assistant side. Its only breaking change is dropping Node.js 20
for bare-metal installs; the container ships its own runtime. The amd64
manifest was verified pullable before this commit.

debounce: 2 addresses the part 2.13.0 does not fix. Koenkk/zigbee2mqtt#31048
is still open, and the author of #12748 states explicitly that it does not
cover the zero drops: "a zero can be a real reading for these attributes,
only the all-bits-set markers are provably invalid."

The station reports as a burst of seven per-cluster ZCL frames and Z2M
publishes its full cumulative state once per frame, so one device report
becomes seven MQTT messages. During a glitch the zeros appear progressively
across those seven, which is what makes the corruption unrecognisable
downstream: an intermediate frame carrying {"temperature":0,"humidity":92,
"pressure":96.9} is internally consistent (Z2M recomputes dew_point from the
false temperature) and cannot be told apart from a legitimate winter
reading. Merging the burst yields a single frame with the all-zero
signature, which a consumer can reject.

The window is bounded on both sides. Above: the device's own reporting
configuration sets minRepIntval 10 / maxRepIntval 3600 on all seven WS90
clusters (1024/1026/1027/1029 and 0xfc01/0xfc02/0xfc03), so no attribute
repeats inside 10s. Below: the recovery burst straddles a second boundary,
so 1s splits it and emits a partial frame.

Staggering was checked rather than assumed — the 10s floor is per attribute,
and nine attributes report across those clusters, so a trailing-edge timer
could in principle keep resetting and never flush. Over 6h the longest
unbroken chain of sub-2s gaps was 5, bounding worst-case deferral under
~10s.

The device is also absent from the seeded device list, which predates it
joining, so this adds the friendly_name too.

NOTE: this file is seed-only. A running Z2M owns its configuration.yaml on
the PVC, so this does not change the live instance — it only makes a fresh
PVC come up correctly. debounce is requiresRestart, so applying it live
needs the option set on the running instance and a restart; the image bump
supplies the restart.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 22:03:17 +02:00
issue-fix-bot 8386f755d1 fix: [alert] CephHealthWarn (rook-ceph) (#557)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
2026-08-23 18:46:50 +00:00
issue-fix-bot cf63aa61be fix: [alert] KubeJobFailed (openbao) (#554)
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
2026-08-22 02:45:05 +00:00
issue-fix-bot 08289947a1 fix: [alert] KubeJobFailed (wordpress) (#550)
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
2026-08-21 21:47:22 +00:00
security-bot 287cce7d1a chore(image-automation): bump ghcr.io/home-assistant/home-assistant:2026.8.3
ci/woodpecker/cron/update-all Pipeline failed
ci/woodpecker/cron/e2e Pipeline was successful
ci/woodpecker/cron/security-scan Pipeline was successful
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Updates managed by image-automation-controller.
2026-08-21 21:42:10 +00:00
claude-bot 5b6a55c8ba chore(ci): bump alpine to 3.24.1 in deploy-infrastructure
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
From the #426 audit. Tag verified present before bumping.

The kaniko bump that was originally in this branch is dropped. It would
have pinned a new `-debug` image, and debug variants are not to run in
the production cluster. kaniko is archived upstream, so the fix is
migration rather than a version bump; tracked in #552 along with the
`-debug` requirement.
2026-08-21 22:55:02 +02:00
21 changed files with 394 additions and 145 deletions
+128 -90
View File
@@ -14,12 +14,6 @@
# Flux-deployed CronJobs, so it is tracked separately in #330 rather than
# bundled here. Note this pipeline runs no LLM agent, so the exfil path that
# motivated the other cutovers does not apply to it.
#
# The build steps run the plain kaniko executor, which has no shell: they pass
# flags via `entrypoint` and read credentials from DOCKER_CONFIG. Registry auth
# is written once by the registry-auth step below. Dated tags are literals: bump
# them in the commit that changes an image. The build timestamp itself lives in
# the image at /etc/image-build-date, written by a RUN in the Containerfile.
when:
- event: push
path:
@@ -30,22 +24,10 @@ when:
evaluate: 'PIPELINE == "build-ci-images"'
steps:
- name: registry-auth
image: alpine:3.24.1
depends_on: []
environment:
GITEA_USER: gitea_admin
GITEA_TOKEN:
from_secret: ci_images_token
commands:
- mkdir -p .docker
- AUTH=$(printf '%s:%s' "$GITEA_USER" "$GITEA_TOKEN" | base64 | tr -d '\n')
- printf '{"auths":{"gitea.renner.uno":{"auth":"%s"}}}' "$AUTH" > .docker/config.json
- name: build-ansible
image: gcr.io/kaniko-project/executor:v1.24.0
image: gcr.io/kaniko-project/executor:v1.23.2-debug
directory: ci/images/ansible
depends_on: [registry-auth]
depends_on: []
# Only rebuild when THIS image's source (or the pipeline file) changed, or
# on a manual run. Without this every step ran on any matching push, so a
# secret-rotator commit needlessly rebuilt all four ~1.3G ci-* images and
@@ -55,47 +37,68 @@ steps:
- event: push
path: ["ci/images/ansible/**", ".woodpecker/build-ci-images.yml"]
environment:
DOCKER_CONFIG: "${CI_WORKSPACE}/.docker"
entrypoint:
- /kaniko/executor
- --context=.
- --dockerfile=Containerfile
- --destination=gitea.renner.uno/gitea_admin/ci-ansible:2026-08-23
- --destination=gitea.renner.uno/gitea_admin/ci-ansible:latest
GITEA_USER: gitea_admin
GITEA_TOKEN:
from_secret: ci_images_token
commands:
- mkdir -p /kaniko/.docker
- AUTH=$(printf '%s:%s' "$GITEA_USER" "$GITEA_TOKEN" | base64 | tr -d '\n')
- printf '{"auths":{"gitea.renner.uno":{"auth":"%s"}}}' "$AUTH" > /kaniko/.docker/config.json
- TAG=$(date +%Y-%m-%d)
- |
/kaniko/executor \
--context . \
--dockerfile Containerfile \
--destination gitea.renner.uno/gitea_admin/ci-ansible:$TAG \
--destination gitea.renner.uno/gitea_admin/ci-ansible:latest
- name: build-k8s
image: gcr.io/kaniko-project/executor:v1.24.0
image: gcr.io/kaniko-project/executor:v1.23.2-debug
directory: ci/images/k8s
depends_on: [registry-auth]
depends_on: []
when:
- event: manual
- event: push
path: ["ci/images/k8s/**", ".woodpecker/build-ci-images.yml"]
environment:
DOCKER_CONFIG: "${CI_WORKSPACE}/.docker"
entrypoint:
- /kaniko/executor
- --context=.
- --dockerfile=Containerfile
- --destination=gitea.renner.uno/gitea_admin/ci-k8s:2026-08-23
- --destination=gitea.renner.uno/gitea_admin/ci-k8s:latest
GITEA_USER: gitea_admin
GITEA_TOKEN:
from_secret: ci_images_token
commands:
- mkdir -p /kaniko/.docker
- AUTH=$(printf '%s:%s' "$GITEA_USER" "$GITEA_TOKEN" | base64 | tr -d '\n')
- printf '{"auths":{"gitea.renner.uno":{"auth":"%s"}}}' "$AUTH" > /kaniko/.docker/config.json
- TAG=$(date +%Y-%m-%d)
- |
/kaniko/executor \
--context . \
--dockerfile Containerfile \
--destination gitea.renner.uno/gitea_admin/ci-k8s:$TAG \
--destination gitea.renner.uno/gitea_admin/ci-k8s:latest
- name: build-tofu
image: gcr.io/kaniko-project/executor:v1.24.0
image: gcr.io/kaniko-project/executor:v1.23.2-debug
directory: ci/images/tofu
depends_on: [registry-auth]
depends_on: []
when:
- event: manual
- event: push
path: ["ci/images/tofu/**", ".woodpecker/build-ci-images.yml"]
environment:
DOCKER_CONFIG: "${CI_WORKSPACE}/.docker"
entrypoint:
- /kaniko/executor
- --context=.
- --dockerfile=Containerfile
- --destination=gitea.renner.uno/gitea_admin/ci-tofu:2026-08-23
- --destination=gitea.renner.uno/gitea_admin/ci-tofu:latest
GITEA_USER: gitea_admin
GITEA_TOKEN:
from_secret: ci_images_token
commands:
- mkdir -p /kaniko/.docker
- AUTH=$(printf '%s:%s' "$GITEA_USER" "$GITEA_TOKEN" | base64 | tr -d '\n')
- printf '{"auths":{"gitea.renner.uno":{"auth":"%s"}}}' "$AUTH" > /kaniko/.docker/config.json
- TAG=$(date +%Y-%m-%d)
- |
/kaniko/executor \
--context . \
--dockerfile Containerfile \
--destination gitea.renner.uno/gitea_admin/ci-tofu:$TAG \
--destination gitea.renner.uno/gitea_admin/ci-tofu:latest
# NOTE: ci-claude is deliberately NOT built here. It tracks upstream Claude
# Code releases on its own cadence and is built + version-tagged by
@@ -105,21 +108,28 @@ steps:
# Runtime image, not CI tooling: a static musl binary in a FROM-scratch image,
# used as the base for secret-rotation CronJobs. Replaces per-job inline Python.
- name: build-secret-rotator
image: gcr.io/kaniko-project/executor:v1.24.0
image: gcr.io/kaniko-project/executor:v1.23.2-debug
directory: secret-rotator
depends_on: [registry-auth]
depends_on: []
when:
- event: manual
- event: push
path: ["secret-rotator/**", ".woodpecker/build-ci-images.yml"]
environment:
DOCKER_CONFIG: "${CI_WORKSPACE}/.docker"
entrypoint:
- /kaniko/executor
- --context=.
- --dockerfile=Containerfile
- --destination=gitea.renner.uno/gitea_admin/secret-rotator:2026-08-23
- --destination=gitea.renner.uno/gitea_admin/secret-rotator:latest
GITEA_USER: gitea_admin
GITEA_TOKEN:
from_secret: ci_images_token
commands:
- mkdir -p /kaniko/.docker
- AUTH=$(printf '%s:%s' "$GITEA_USER" "$GITEA_TOKEN" | base64 | tr -d '\n')
- printf '{"auths":{"gitea.renner.uno":{"auth":"%s"}}}' "$AUTH" > /kaniko/.docker/config.json
- TAG=$(date +%Y-%m-%d)
- |
/kaniko/executor \
--context . \
--dockerfile Containerfile \
--destination gitea.renner.uno/gitea_admin/secret-rotator:$TAG \
--destination gitea.renner.uno/gitea_admin/secret-rotator:latest
# Runtime image, not CI tooling: a general-purpose alpine + jq base for
# workloads that need to read JSON from a shell script. Named for what it IS,
@@ -127,21 +137,28 @@ steps:
# container, but nothing about it is zigbee-specific. Exists so tools come from
# the image rather than an `apk add` on every container start.
- name: build-alpine-jq
image: gcr.io/kaniko-project/executor:v1.24.0
image: gcr.io/kaniko-project/executor:v1.23.2-debug
directory: ci/images/alpine-jq
depends_on: [registry-auth]
depends_on: []
when:
- event: manual
- event: push
path: ["ci/images/alpine-jq/**", ".woodpecker/build-ci-images.yml"]
environment:
DOCKER_CONFIG: "${CI_WORKSPACE}/.docker"
entrypoint:
- /kaniko/executor
- --context=.
- --dockerfile=Containerfile
- --destination=gitea.renner.uno/gitea_admin/alpine-jq:2026-08-23
- --destination=gitea.renner.uno/gitea_admin/alpine-jq:latest
GITEA_USER: gitea_admin
GITEA_TOKEN:
from_secret: ci_images_token
commands:
- mkdir -p /kaniko/.docker
- AUTH=$(printf '%s:%s' "$GITEA_USER" "$GITEA_TOKEN" | base64 | tr -d '\n')
- printf '{"auths":{"gitea.renner.uno":{"auth":"%s"}}}' "$AUTH" > /kaniko/.docker/config.json
- TAG=$(date +%Y-%m-%d)
- |
/kaniko/executor \
--context . \
--dockerfile Containerfile \
--destination gitea.renner.uno/gitea_admin/alpine-jq:$TAG \
--destination gitea.renner.uno/gitea_admin/alpine-jq:latest
# Runtime image, not CI tooling: upstream containernetworking/plugins binaries
# baked in, consumed by the multus cni-plugins-installer DaemonSet. Replaces a
@@ -149,21 +166,28 @@ steps:
# critical path of every node rebuild. The download now happens here, once,
# where the sha256 is checked — see the Containerfile for the full rationale.
- name: build-cni-plugins
image: gcr.io/kaniko-project/executor:v1.24.0
image: gcr.io/kaniko-project/executor:v1.23.2-debug
directory: ci/images/cni-plugins
depends_on: [registry-auth]
depends_on: []
when:
- event: manual
- event: push
path: ["ci/images/cni-plugins/**", ".woodpecker/build-ci-images.yml"]
environment:
DOCKER_CONFIG: "${CI_WORKSPACE}/.docker"
entrypoint:
- /kaniko/executor
- --context=.
- --dockerfile=Containerfile
- --destination=gitea.renner.uno/gitea_admin/cni-plugins:2026-08-23
- --destination=gitea.renner.uno/gitea_admin/cni-plugins:latest
GITEA_USER: gitea_admin
GITEA_TOKEN:
from_secret: ci_images_token
commands:
- mkdir -p /kaniko/.docker
- AUTH=$(printf '%s:%s' "$GITEA_USER" "$GITEA_TOKEN" | base64 | tr -d '\n')
- printf '{"auths":{"gitea.renner.uno":{"auth":"%s"}}}' "$AUTH" > /kaniko/.docker/config.json
- TAG=$(date +%Y-%m-%d)
- |
/kaniko/executor \
--context . \
--dockerfile Containerfile \
--destination gitea.renner.uno/gitea_admin/cni-plugins:$TAG \
--destination gitea.renner.uno/gitea_admin/cni-plugins:latest
# Runtime image, not CI tooling: alpine + the openssl CLI, for shell scripts
# needing s_client/x509. Named for what it is; first consumer is the paperless
@@ -171,21 +195,28 @@ steps:
# every paperless pod start. The openssl CLI is genuinely absent from the alpine
# base, so this one needs an image rather than a busybox substitute.
- name: build-alpine-openssl
image: gcr.io/kaniko-project/executor:v1.24.0
image: gcr.io/kaniko-project/executor:v1.23.2-debug
directory: ci/images/alpine-openssl
depends_on: [registry-auth]
depends_on: []
when:
- event: manual
- event: push
path: ["ci/images/alpine-openssl/**", ".woodpecker/build-ci-images.yml"]
environment:
DOCKER_CONFIG: "${CI_WORKSPACE}/.docker"
entrypoint:
- /kaniko/executor
- --context=.
- --dockerfile=Containerfile
- --destination=gitea.renner.uno/gitea_admin/alpine-openssl:2026-08-23
- --destination=gitea.renner.uno/gitea_admin/alpine-openssl:latest
GITEA_USER: gitea_admin
GITEA_TOKEN:
from_secret: ci_images_token
commands:
- mkdir -p /kaniko/.docker
- AUTH=$(printf '%s:%s' "$GITEA_USER" "$GITEA_TOKEN" | base64 | tr -d '\n')
- printf '{"auths":{"gitea.renner.uno":{"auth":"%s"}}}' "$AUTH" > /kaniko/.docker/config.json
- TAG=$(date +%Y-%m-%d)
- |
/kaniko/executor \
--context . \
--dockerfile Containerfile \
--destination gitea.renner.uno/gitea_admin/alpine-openssl:$TAG \
--destination gitea.renner.uno/gitea_admin/alpine-openssl:latest
# Heating CAN readers: open3e (Vitocal/VX3) + E3onCAN (E380 meter), one image
# run as two services on edge-1. Replaces pip-into-venv on the host, where an
@@ -193,18 +224,25 @@ steps:
# Ansible converge. Not a ci-* image -- it is a runtime artifact -- but it
# builds the same way and lands in the same registry so the audit sees it.
- name: build-heating-can
image: gcr.io/kaniko-project/executor:v1.24.0
image: gcr.io/kaniko-project/executor:v1.23.2-debug
directory: ci/images/heating-can
depends_on: [registry-auth]
depends_on: []
when:
- event: manual
- event: push
path: ["ci/images/heating-can/**", ".woodpecker/build-ci-images.yml"]
environment:
DOCKER_CONFIG: "${CI_WORKSPACE}/.docker"
entrypoint:
- /kaniko/executor
- --context=.
- --dockerfile=Containerfile
- --destination=gitea.renner.uno/gitea_admin/heating-can:2026-08-23
- --destination=gitea.renner.uno/gitea_admin/heating-can:latest
GITEA_USER: gitea_admin
GITEA_TOKEN:
from_secret: ci_images_token
commands:
- mkdir -p /kaniko/.docker
- AUTH=$(printf '%s:%s' "$GITEA_USER" "$GITEA_TOKEN" | base64 | tr -d '\n')
- printf '{"auths":{"gitea.renner.uno":{"auth":"%s"}}}' "$AUTH" > /kaniko/.docker/config.json
- TAG=$(date +%Y-%m-%d)
- |
/kaniko/executor \
--context . \
--dockerfile Containerfile \
--destination gitea.renner.uno/gitea_admin/heating-can:$TAG \
--destination gitea.renner.uno/gitea_admin/heating-can:latest
+1 -1
View File
@@ -13,7 +13,7 @@ when:
steps:
- name: validate-secrets
image: alpine:3.21
image: alpine:3.24.1
environment:
TF_VAR_proxmox_api_url:
from_secret: proxmox_api_url
-5
View File
@@ -22,8 +22,3 @@ FROM alpine:3.21@sha256:48b0309ca019d89d40f670aa1bc06e426dc0931948452e8491e3d650
# unanchored extraction therefore yields a device link key, and herdsman then
# forms a NEW Zigbee network and drops every device.
RUN apk add --no-cache jq
# Build timestamp, recorded in the image rather than the tag: the tag says which
# source revision was built, this says when. Last layer, so it only invalidates
# itself.
RUN date -u +%Y-%m-%dT%H:%M:%SZ > /etc/image-build-date
-5
View File
@@ -19,8 +19,3 @@ FROM alpine:3.21@sha256:48b0309ca019d89d40f670aa1bc06e426dc0931948452e8491e3d650
# for paperless to trust. busybox has no s_client equivalent, so openssl is not
# substitutable there.
RUN apk add --no-cache openssl
# Build timestamp, recorded in the image rather than the tag: the tag says which
# source revision was built, this says when. Last layer, so it only invalidates
# itself.
RUN date -u +%Y-%m-%dT%H:%M:%SZ > /etc/image-build-date
-5
View File
@@ -24,8 +24,3 @@ RUN pip3 install --break-system-packages --no-cache-dir \
RUN ansible-galaxy collection install \
community.proxmox \
community.docker
# Build timestamp, recorded in the image rather than the tag: the tag says which
# source revision was built, this says when. Last layer, so it only invalidates
# itself.
RUN date -u +%Y-%m-%dT%H:%M:%SZ > /etc/image-build-date
-6
View File
@@ -44,9 +44,3 @@ RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION} \
/usr \
/root \
/tmp
# Build timestamp, recorded in the image rather than the tag: the tag says which
# version was built, this says when. Deliberately the LAST layer and its own
# layer, so the byte-stable npm layer above still dedups in the registry — that
# dedup is what #351 was for, and a few dozen bytes here does not affect it.
RUN date -u +%Y-%m-%dT%H:%M:%SZ > /etc/image-build-date
-5
View File
@@ -66,8 +66,3 @@ FROM alpine:3.21@sha256:48b0309ca019d89d40f670aa1bc06e426dc0931948452e8491e3d650
# Alpine rather than scratch: the installer needs a shell to copy the binaries
# onto the host. busybox covers cp/mv/chown/ls, so nothing is added.
COPY --from=fetch /plugins/ /opt/cni-plugins/
# Build timestamp, recorded in the image rather than the tag: the tag says which
# source revision was built, this says when. Last layer, so it only invalidates
# itself.
RUN date -u +%Y-%m-%dT%H:%M:%SZ > /etc/image-build-date
-5
View File
@@ -120,8 +120,3 @@ USER open3e
WORKDIR /var/lib/open3e
ENTRYPOINT ["supervisord", "-c", "/etc/supervisord.conf"]
# Build timestamp, recorded in the image rather than the tag: the tag says which
# source revision was built, this says when. Last layer, so it only invalidates
# itself.
RUN date -u +%Y-%m-%dT%H:%M:%SZ > /etc/image-build-date
-5
View File
@@ -42,8 +42,3 @@ RUN curl -sfL -o /tmp/velero.tar.gz \
&& tar -xz -C /tmp -f /tmp/velero.tar.gz \
&& install "/tmp/velero-${VELERO_VERSION}-linux-amd64/velero" /usr/local/bin/velero \
&& rm -rf "/tmp/velero-${VELERO_VERSION}-linux-amd64" /tmp/velero.tar.gz
# Build timestamp, recorded in the image rather than the tag: the tag says which
# source revision was built, this says when. Last layer, so it only invalidates
# itself.
RUN date -u +%Y-%m-%dT%H:%M:%SZ > /etc/image-build-date
-5
View File
@@ -31,8 +31,3 @@ RUN pip3 install --break-system-packages --no-cache-dir \
RUN ansible-galaxy collection install \
community.proxmox \
community.docker
# Build timestamp, recorded in the image rather than the tag: the tag says which
# source revision was built, this says when. Last layer, so it only invalidates
# itself.
RUN date -u +%Y-%m-%dT%H:%M:%SZ > /etc/image-build-date
@@ -84,7 +84,7 @@ spec:
env:
- name: DB_URI
valueFrom:
secretKeyRef: { name: ha-pg-app, key: fqdn-uri }
secretKeyRef: {name: ha-pg-app, key: fqdn-uri}
volumeMounts:
- name: config
mountPath: /config
@@ -96,7 +96,7 @@ spec:
memory: 32Mi
containers:
- name: home-assistant
image: ghcr.io/home-assistant/home-assistant:2026.8.2 # {"$imagepolicy": "flux-system:home-assistant"}
image: ghcr.io/home-assistant/home-assistant:2026.8.3 # {"$imagepolicy": "flux-system:home-assistant"}
env:
- name: TZ
value: Europe/Berlin
@@ -176,3 +176,38 @@ data:
friendly_name: Treppenhaus Haustür Schalter 1
'0x001788010b00d011':
friendly_name: Treppenhaus Haustür Schalter 2
# Ecowitt WS90 weather station. Joined after the ZHA-mirrored block above
# was written, hence listed apart from it.
#
# This device also carries a non-default ZCL reporting configuration
# (per-attribute min interval and reportable-change deadbands). That is
# device-side state, not expressible here — Z2M has no settings field for
# it — so it is documented, with restore commands, in
# zigbee2mqtt-ws90-reporting.md. It is lost whenever Z2M re-runs
# `configure` on the device, and a fresh PVC will not have it.
#
# The station transmits every 8.8s, as a burst of seven per-cluster ZCL
# frames; Z2M publishes its full cumulative state once per frame. debounce
# merges a burst into one publish carrying the last value per attribute,
# and must stay below the transmit interval — a window that never sees
# silence never flushes.
#
# Every few days the firmware zeroes its readings for ~8s, then skips a
# transmit cycle and recovers on the next. The zeros propagate attribute
# by attribute through a burst rather than arriving together, and they
# start inside an otherwise normal cycle — the last valid reading and the
# first zero are ~20ms apart.
#
# Merging cuts an episode from six published bad frames to one. It does
# NOT make that frame recognisable: it carries whatever subset had zeroed
# when the burst ended, which may be every field or only temperature, and
# a frame reading {"temperature":0,"humidity":92,"pressure":96.9} is not
# separable from a valid winter reading. Since the merge is last-wins and
# the zeros arrive at the tail, it also discards the valid reading that
# led the burst. This bounds the corruption; it does not filter it.
#
# Z2M reads this only at startup (requiresRestart), and this file is
# SEED-ONLY per the header — the running instance takes it from its PVC.
'0xc02cedfffe958f40':
friendly_name: Wetterstation
debounce: 2
@@ -158,7 +158,7 @@ spec:
# Pinned tag. No Flux image-automation marker yet (unlike mosquitto) —
# add an infrastructure/image-automation/zigbee2mqtt.yaml policy as a
# follow-up if desired; pinned-and-manual is fine for the initial cutover.
image: ghcr.io/koenkk/zigbee2mqtt:2.12.1
image: ghcr.io/koenkk/zigbee2mqtt:2.13.0
imagePullPolicy: IfNotPresent
env:
- name: TZ
@@ -0,0 +1,149 @@
# WS90 Zigbee reporting configuration
Records the ZCL attribute-reporting configuration applied to the Wetterstation
(Ecowitt WS90 powered by Shelly, IEEE `0xc02cedfffe958f40`) and how to restore
it, because **this configuration lives on the device and in Z2M's
`database.db`, not in git**. Zigbee2MQTT has no settings-schema field for
reporting configuration, so there is nothing declarative to reconcile — Flux
cannot restore it and a fresh PVC will not have it.
## Applied configuration
Endpoint 1, all intervals in seconds. `change` is in the attribute's raw ZCL
units; the physical deadband is `change ÷ scale`.
| attribute | cluster | min | max | change | deadband | converter default |
| --- | --- | --- | --- | --- | --- | --- |
| temperature | `msTemperatureMeasurement` (1026) | 5 | 3600 | 10 | 0.1 °C | 10 / 3600 / 100 |
| humidity | `msRelativeHumidity` (1029) | 5 | 3600 | 50 | 0.5 % | 10 / 3600 / 100 |
| pressure | `msPressureMeasurement` (1027) | 5 | 3600 | 1 | 1 hPa | 10 / 3600 / 50 |
| illuminance | `msIlluminanceMeasurement` (1024) | 5 | 3600 | 5 | 5 lx | 10 / 3600 / 5 |
| windSpeed | `shellyWS90Wind` (0xfc01/0) | 5 | 3600 | 1 | — | 10 / 3600 / 1 |
| windDirection | `shellyWS90Wind` (0xfc01/4) | 5 | 3600 | 1 | — | 10 / 3600 / 1 |
| gustSpeed | `shellyWS90Wind` (0xfc01/7) | 5 | 3600 | 1 | — | 10 / 3600 / 1 |
| uvIndex | `shellyWS90UV` (0xfc02/0) | 5 | 3600 | 1 | — | 10 / 3600 / 1 |
| rainStatus | `shellyWS90Rain` (0xfc03/0) | 5 | 3600 | 1 | — | 10 / 3600 / 1 |
| precipitation | `shellyWS90Rain` (0xfc03/1) | 5 | 3600 | 1 | — | 10 / 3600 / 1 |
`genPowerCfg.batteryVoltage` (cluster 1, attribute 33) is deliberately left at
its hourly default.
The three `shellyWS90*` clusters are manufacturer-specific and require
`"options": {"manufacturerCode": 5264}` on every reconfiguration request.
## Why min is 5 and not 10
The station transmits every 8.8 s. `min` is a hold-off: a change-triggered
report is held until at least `min` seconds after the previous one. With
`min: 10` every report was therefore held past a transmit cycle and landed on
the next, so attributes reported at best every 17.6 s and drifted out of phase
with each other — the published stream showed no 8.8 s periodicity at all,
just a smear across 2–8 s.
At `min: 5` each attribute reports on its own transmit. The MQTT stream then
carries one publish per transmit cycle, measured at a mean gap of 8.69 s.
Total publish volume *fell* (~631/h to ~445/h) because each publish now
carries a whole cycle instead of a fragment of one.
Any value below 8.8 s achieves this. 5 leaves margin without approaching the
cycle time.
## Why the deadbands differ from the converter defaults
The defaults come from the shared `modernExtend` helpers in
zigbee-herdsman-converters (`m.temperature()`, `m.pressure()`, `m.humidity()`,
`m.illuminance()`), which the WS90 definition in `shelly.ts` composes without
overrides. They are anti-chatter values sized for battery-powered indoor
sensors, where report volume costs coin-cell life and mesh airtime. This
station is mains-and-solar and exists to resolve small changes, so the
trade-off runs the other way.
Each value above is matched to the resolution the device actually reports:
temperature and pressure arrive in 0.1-unit steps, humidity in whole percent.
Pressure is worth calling out. The converter default of `change: 50` with
`scale: 10` is a 5 kPa (50 hPa) deadband, which real barometric pressure never
traverses — so pressure only ever updated on the hourly `max` heartbeat, and
`pressure_trend` and `weather_condition` were derived from that. The same
library's older `reporting.ts` helper configures the identical ZCL attribute
with `change: 5`, a 5 hPa deadband, so the two code paths disagree by 10× and
the modernExtend value is the outlier.
## When this is lost
Anything that makes Z2M re-run `configure` on the device overwrites all of it
with the converter defaults:
- re-pairing or re-joining the device
- an explicit `zigbee2mqtt/bridge/request/device/configure`
- a converter update that changes the definition and triggers reconfiguration
It also will not exist on a fresh PVC. Re-apply with the commands below.
## Re-applying
Published from the `discovery-guard` sidecar, which already holds the
`zigbee2mqtt` broker credential (ACL `readwrite zigbee2mqtt/#`, which covers
`bridge/request/#`) so no credential has to leave the cluster:
Ten separate requests, one per attribute — written out literally rather than
generated by a shell helper, because the nested quoting through `kubectl exec
-- sh -c` is where this goes wrong. Each line below is in the exact form that
was executed and verified.
```sh
POD=$(kubectl get pod -n home-assistant -l app.kubernetes.io/name=zigbee2mqtt -o jsonpath='{.items[0].metadata.name}')
```
```sh
kubectl exec -n home-assistant "$POD" -c discovery-guard -- sh -c 'mosquitto_pub -h "$MQTT_HOST" -p 1883 -u "$MQTT_USER" -P "$MQTT_PASSWORD" -t "$BASE_TOPIC/bridge/request/device/configure_reporting" -m "{\"id\":\"0xc02cedfffe958f40\",\"endpoint\":1,\"cluster\":\"msTemperatureMeasurement\",\"attribute\":\"measuredValue\",\"minimum_report_interval\":5,\"maximum_report_interval\":3600,\"reportable_change\":10}"'
kubectl exec -n home-assistant "$POD" -c discovery-guard -- sh -c 'mosquitto_pub -h "$MQTT_HOST" -p 1883 -u "$MQTT_USER" -P "$MQTT_PASSWORD" -t "$BASE_TOPIC/bridge/request/device/configure_reporting" -m "{\"id\":\"0xc02cedfffe958f40\",\"endpoint\":1,\"cluster\":\"msRelativeHumidity\",\"attribute\":\"measuredValue\",\"minimum_report_interval\":5,\"maximum_report_interval\":3600,\"reportable_change\":50}"'
kubectl exec -n home-assistant "$POD" -c discovery-guard -- sh -c 'mosquitto_pub -h "$MQTT_HOST" -p 1883 -u "$MQTT_USER" -P "$MQTT_PASSWORD" -t "$BASE_TOPIC/bridge/request/device/configure_reporting" -m "{\"id\":\"0xc02cedfffe958f40\",\"endpoint\":1,\"cluster\":\"msPressureMeasurement\",\"attribute\":\"measuredValue\",\"minimum_report_interval\":5,\"maximum_report_interval\":3600,\"reportable_change\":1}"'
kubectl exec -n home-assistant "$POD" -c discovery-guard -- sh -c 'mosquitto_pub -h "$MQTT_HOST" -p 1883 -u "$MQTT_USER" -P "$MQTT_PASSWORD" -t "$BASE_TOPIC/bridge/request/device/configure_reporting" -m "{\"id\":\"0xc02cedfffe958f40\",\"endpoint\":1,\"cluster\":\"msIlluminanceMeasurement\",\"attribute\":\"measuredValue\",\"minimum_report_interval\":5,\"maximum_report_interval\":3600,\"reportable_change\":5}"'
kubectl exec -n home-assistant "$POD" -c discovery-guard -- sh -c 'mosquitto_pub -h "$MQTT_HOST" -p 1883 -u "$MQTT_USER" -P "$MQTT_PASSWORD" -t "$BASE_TOPIC/bridge/request/device/configure_reporting" -m "{\"id\":\"0xc02cedfffe958f40\",\"endpoint\":1,\"cluster\":\"shellyWS90Wind\",\"attribute\":\"windSpeed\",\"minimum_report_interval\":5,\"maximum_report_interval\":3600,\"reportable_change\":1,\"options\":{\"manufacturerCode\":5264}}"'
kubectl exec -n home-assistant "$POD" -c discovery-guard -- sh -c 'mosquitto_pub -h "$MQTT_HOST" -p 1883 -u "$MQTT_USER" -P "$MQTT_PASSWORD" -t "$BASE_TOPIC/bridge/request/device/configure_reporting" -m "{\"id\":\"0xc02cedfffe958f40\",\"endpoint\":1,\"cluster\":\"shellyWS90Wind\",\"attribute\":\"windDirection\",\"minimum_report_interval\":5,\"maximum_report_interval\":3600,\"reportable_change\":1,\"options\":{\"manufacturerCode\":5264}}"'
kubectl exec -n home-assistant "$POD" -c discovery-guard -- sh -c 'mosquitto_pub -h "$MQTT_HOST" -p 1883 -u "$MQTT_USER" -P "$MQTT_PASSWORD" -t "$BASE_TOPIC/bridge/request/device/configure_reporting" -m "{\"id\":\"0xc02cedfffe958f40\",\"endpoint\":1,\"cluster\":\"shellyWS90Wind\",\"attribute\":\"gustSpeed\",\"minimum_report_interval\":5,\"maximum_report_interval\":3600,\"reportable_change\":1,\"options\":{\"manufacturerCode\":5264}}"'
kubectl exec -n home-assistant "$POD" -c discovery-guard -- sh -c 'mosquitto_pub -h "$MQTT_HOST" -p 1883 -u "$MQTT_USER" -P "$MQTT_PASSWORD" -t "$BASE_TOPIC/bridge/request/device/configure_reporting" -m "{\"id\":\"0xc02cedfffe958f40\",\"endpoint\":1,\"cluster\":\"shellyWS90UV\",\"attribute\":\"uvIndex\",\"minimum_report_interval\":5,\"maximum_report_interval\":3600,\"reportable_change\":1,\"options\":{\"manufacturerCode\":5264}}"'
kubectl exec -n home-assistant "$POD" -c discovery-guard -- sh -c 'mosquitto_pub -h "$MQTT_HOST" -p 1883 -u "$MQTT_USER" -P "$MQTT_PASSWORD" -t "$BASE_TOPIC/bridge/request/device/configure_reporting" -m "{\"id\":\"0xc02cedfffe958f40\",\"endpoint\":1,\"cluster\":\"shellyWS90Rain\",\"attribute\":\"rainStatus\",\"minimum_report_interval\":5,\"maximum_report_interval\":3600,\"reportable_change\":1,\"options\":{\"manufacturerCode\":5264}}"'
kubectl exec -n home-assistant "$POD" -c discovery-guard -- sh -c 'mosquitto_pub -h "$MQTT_HOST" -p 1883 -u "$MQTT_USER" -P "$MQTT_PASSWORD" -t "$BASE_TOPIC/bridge/request/device/configure_reporting" -m "{\"id\":\"0xc02cedfffe958f40\",\"endpoint\":1,\"cluster\":\"shellyWS90Rain\",\"attribute\":\"precipitation\",\"minimum_report_interval\":5,\"maximum_report_interval\":3600,\"reportable_change\":1,\"options\":{\"manufacturerCode\":5264}}"'
```
These are idempotent — re-sending a request that matches the current
configuration is a no-op on the device.
The device must be awake and reachable; each request performs a real
`ConfigureReporting` exchange and fails if it is not.
## Verifying
Read back what the device actually accepted — Z2M records the negotiated
result, so this reflects the device, not the request:
```sh
kubectl exec -n home-assistant "$POD" -c zigbee2mqtt -- \
grep 0xc02cedfffe958f40 /app/data/database.db |
jq -r '.endpoints | to_entries[] | .value.configuredReportings[]?
| "cluster=\(.cluster) attr=\(.attrId) min=\(.minRepIntval) max=\(.maxRepIntval) change=\(.repChange)"'
```
All ten rows above should read `min=5 max=3600`. On the wire, the publish
cadence on `zigbee2mqtt/Wetterstation` should sit at ~8.7 s and temperature
should move in 0.1 °C steps; 1 °C steps mean the defaults are back.
## Making it durable upstream
The lasting fix is for the WS90 definition to pass explicit `reporting`
overrides rather than inherit indoor-sensor defaults, which would fix every
WS90 rather than this one. `m.temperature()` and the other helpers already
accept a `reporting` argument. The pressure divergence between
`modernExtend.ts` and `reporting.ts` is a separate upstream bug worth
reporting on its own.
@@ -8,6 +8,7 @@ resources:
- prometheusrule-cilium.yaml
- prometheusrule-hetzner-backup.yaml
- prometheusrule-loki.yaml
- prometheusrule-mariadb-backup.yaml
- prometheusrule-netbird-availability.yaml
- prometheusrule-netbird-backup.yaml
- prometheusrule-netbird-ingress.yaml
@@ -25,13 +25,27 @@ spec:
#
# 6h: Ceph warns for self-clearing conditions. Only a warning that
# does not clear is worth paging on.
#
# BLUESTORE_SLOW_OP_ALERT breaks that premise and is still NOT
# excluded (#557). Ceph holds the check for
# bluestore_slow_ops_warn_lifetime (24h) after the LAST slow op, so
# one multi-second stall outlives the 6h `for` and always pages,
# with latency back to normal long before anyone looks. Measured:
# both episodes in the 30d to 2026-08-23 ran ~26h and cleared
# untouched, the first ~24h after the last stall.
#
# Both began inside the weekly Velero restore-test window (Sun
# 08:00-13:00Z) — a 5h sustained restore that outruns the SLC cache
# on these QLC NVMe. Expected for the hardware, but not excludable:
# this is also the earliest signal of a genuinely failing drive, and
# all three are at 80-87% endurance used (see cephcluster.yaml).
expr: ceph_health_detail{name!~"AUTH_INSECURE_.+"} == 1
for: 6h
labels:
severity: warning
annotations:
summary: "Ceph health check {{ $labels.name }} active for 6h"
description: "Ceph health check {{ $labels.name }} has been active for six hours, so it is not a self-clearing blip. Only AUTH_INSECURE_* is excluded (kernel-blocked, see #527), so this is a new condition. Detail: `kubectl -n rook-ceph exec deploy/rook-ceph-tools -- ceph health detail`."
description: "Ceph health check {{ $labels.name }} has been active for six hours. Only AUTH_INSECURE_* is excluded (kernel-blocked, see #527). BLUESTORE_SLOW_OP_ALERT is sticky for 24h after the last stall and clears itself: check whether the stall fell inside the Sunday 08:00-13:00Z Velero restore-test window, which is expected on these QLC drives — one outside that window, or a repeat on the same OSD, means check that drive (#557). Detail: `kubectl -n rook-ceph exec deploy/rook-ceph-tools -- ceph health detail`."
- alert: CephPoolLostRedundancy
# Names the pool, which POOL_NO_REDUNDANCY does not.
@@ -0,0 +1,51 @@
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: mariadb-backup
namespace: monitoring
labels:
# kube-prometheus-stack's Prometheus selects on this label.
release: kube-prometheus-stack
spec:
groups:
- name: mariadb-backup.rules
rules:
- alert: WordpressMariadbBackupStale
# The nightly 02:45 logical dump (kubernetes/apps/wordpress/mariadb.yaml)
# is the only transaction-consistent copy of www.renner.uno's database
# — Velero snapshots the PVC, which for a running DB is not a restore
# point you would trust.
#
# KubeJobFailed already covers a failed Job, but only while the Job
# object survives. On #550 it cleared when the failed Job was
# garbage-collected, not when a backup succeeded — so a failure that
# is GC'd before anyone looks leaves no signal at all.
# lastSuccessfulTime lives on the CronJob and outlives Job GC.
#
# 26h = one missed cycle plus 2h slack, matching NetbirdVpsBackupStale.
# It counts any Job owned by the CronJob, so a manual
# `kubectl create job --from=cronjob/...` re-run clears it too.
expr: |
time() - kube_cronjob_status_last_successful_time{namespace="wordpress", cronjob="wordpress-mariadb-backup"} > 26 * 3600
for: 30m
labels:
severity: warning
annotations:
summary: "WordPress MariaDB backup has not succeeded in 26h+"
description: "The wordpress-mariadb-backup CronJob's last successful run is over 26 hours old. Check the newest Job's pod logs in the wordpress namespace: `kubectl -n wordpress get jobs`. The 2026-08-19 failure was `The specified bucket does not exist.` against renner-uno-k8s-mariadb while the endpoint itself probed healthy, so a green HetznerBackupEndpointDown does not clear this."
- alert: WordpressMariadbBackupNeverRan
# The staleness expression above goes blind when the metric is
# absent, which is what happens once the operator recreates the
# CronJob. Without this guard "never backed up" is quieter than
# "backed up late".
expr: |
kube_cronjob_created{namespace="wordpress", cronjob="wordpress-mariadb-backup"}
unless
kube_cronjob_status_last_successful_time{namespace="wordpress", cronjob="wordpress-mariadb-backup"}
for: 6h
labels:
severity: warning
annotations:
summary: "WordPress MariaDB backup has no recorded successful run"
description: "The wordpress-mariadb-backup CronJob exists but has no lastSuccessfulTime: either it has not completed once since the Backup CR was recreated, or every run since has failed. Check `kubectl -n wordpress get jobs` and that the hetzner-s3-creds ExternalSecret is synced."
+1 -1
View File
@@ -34,7 +34,7 @@ spec:
# Image automation, range 3.x (see image-automation/woodpecker.yaml).
# MUST stay version-matched with woodpecker-server (gRPC parity) — both
# track the same range/filter so they bump together on each release.
image: woodpeckerci/woodpecker-agent:v3.17.0 # {"$imagepolicy": "flux-system:woodpecker-agent"}
image: woodpeckerci/woodpecker-agent:v3.18.0 # {"$imagepolicy": "flux-system:woodpecker-agent"}
env:
- name: WOODPECKER_SERVER
value: woodpecker.woodpecker.svc.cluster.local:9000 # in-cluster gRPC, replaces LXC :9000
+1 -1
View File
@@ -40,7 +40,7 @@ spec:
# minor + patch auto-update within 3.x; only the 4.0 major is held for
# a reviewed bump. Safe now the DB is postgres (the old sqlite-surprise
# -bump hazard is gone). Stays in lockstep with the agent.
image: woodpeckerci/woodpecker-server:v3.17.0 # {"$imagepolicy": "flux-system:woodpecker-server"}
image: woodpeckerci/woodpecker-server:v3.18.0 # {"$imagepolicy": "flux-system:woodpecker-server"}
env:
- name: WOODPECKER_HOST
value: https://ci.renner.uno
@@ -51,7 +51,15 @@ spec:
startingDeadlineSeconds: 3600
jobTemplate:
spec:
backoffLimit: 1
# 4 (not the sibling rotators' 1-2) because this is the only DAILY
# rotator — every other CronJob here is quarterly, so it takes ~90x as
# many runs and is correspondingly more exposed to per-run transients.
# backoffLimit: 1 gave two attempts ~11 s apart, and on 2026-08-19 a
# sub-minute resolver transient took both out (Job
# claude-oauth-secret-sync-29785100, BackoffLimitExceeded), costing a
# day's sync. 4 spreads the attempts over ~150 s (10/20/40/80 s backoff),
# still well inside activeDeadlineSeconds.
backoffLimit: 4
activeDeadlineSeconds: 300
ttlSecondsAfterFinished: 259200 # 3 d
template:
-6
View File
@@ -21,14 +21,8 @@ COPY src ./src
RUN cargo build --release --target x86_64-unknown-linux-musl \
&& strip target/x86_64-unknown-linux-musl/release/secret-rotator
# Build timestamp, recorded in the image rather than the tag: the tag says which
# source revision was built, this says when. Written here because the final
# image is scratch and has no shell to run `date` in.
RUN date -u +%Y-%m-%dT%H:%M:%SZ > /build/image-build-date
FROM scratch
COPY --from=build /build/target/x86_64-unknown-linux-musl/release/secret-rotator /secret-rotator
COPY --from=build /build/image-build-date /etc/image-build-date
# Matches runAsUser on the rotation Jobs. There is no /etc/passwd in a scratch
# image, so this is a bare uid by necessity — which is also all Kubernetes needs.