ci: cite YAML by anchor instead of line number, and guard it (REN-183)
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

Comments that pointed at other YAML as `file.yaml:N` went stale on any
edit above the cited line; three drift fixes had been merged by hand
(REN-181). The cited block now carries `# anchor: <name>` and the
comment cites `path.yaml#<name>`.

- Sweep: all 32 line-number citations in YAML comments converted.
  Several were already stale at head and now point at the right block
  (see PR). Vendored files (Flux gotk-components, IMC CRD) get no
  anchor; those two citations name the object/schema path instead.
- Guard: scripts/ci/yaml-citations.sh in ci-manifests-test.yml; fails
  on a line-number citation or a path#name whose file/anchor is gone.
  Self-test in ci-scripts-test.yml.
- CONTRIBUTING.md: the convention in three lines.

Comment-only under kubernetes/: every file parses to identical objects
before and after, so Flux applies nothing and no ConfigMap data changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
atlas-bot
2026-09-26 08:44:54 +00:00
co-authored by Claude Opus 5.5
parent 2608c264d3
commit 21a7583523
24 changed files with 301 additions and 49 deletions
+22 -2
View File
@@ -36,6 +36,7 @@ when:
- "kubernetes/**"
- ".woodpecker/**"
- "scripts/ci/promtool-rules.sh"
- "scripts/ci/yaml-citations.sh"
# THE THIRD DISCOVERY CHECK WENT TO A SCRIPT. kustomize-build and shellcheck
# both hand-roll the same shape — `find`, a floor guard against empty discovery,
@@ -50,10 +51,10 @@ when:
# ci-scripts-test.yml. A FOURTH inline copy should go the same way; do not add
# one here.
#
# Every step carries `depends_on: []` so all five run in PARALLEL and every
# Every step carries `depends_on: []` so all six run in PARALLEL and every
# failure shows in one run. Woodpecker's default is sequential, which would stop
# at the first failure and make a contributor fix-and-repush to discover the
# next — and these five are independent, cheap, offline checks with no ordering
# next — and these six are independent, cheap, offline checks with no ordering
# between them. Keep it on every step: `depends_on` is per-step, so dropping it
# from one silently re-serialises that step behind the others.
steps:
@@ -320,3 +321,22 @@ steps:
image: prom/prometheus:v3.15.0
commands:
- sh scripts/ci/promtool-rules.sh
# Comments cite other YAML blocks by anchor name, never by line number
# (REN-183; the exact syntax is in CONTRIBUTING.md, not repeated here because
# an example written in this comment would itself be checked). Line numbers drift on any edit
# above them and nothing noticed: three drift fixes were merged by hand
# before this step existed. This fails on a new line-number citation and on
# a citation whose file or anchor marker does not exist.
# Existence only — it never judges whether the anchored block still says
# what the citing comment claims, so ordinary edits cannot turn it red.
#
# It scans every YAML file in the checkout, but this pipeline only runs on
# the paths in `when:` above, so an edit confined to ansible/ or esphome/ is
# checked on the next PR that does touch kubernetes/ or .woodpecker/.
# Self-test: scripts/ci/test-yaml-citations.sh, in ci-scripts-test.yml.
- name: yaml-citations
depends_on: [] # see note above `steps:`
image: gitea.renner.uno/gitea_admin/ci-k8s:2026-09-08
commands:
- sh scripts/ci/yaml-citations.sh
+8 -2
View File
@@ -23,14 +23,14 @@
# only runs when someone remembers to touch the right directory is not a guard.
#
# Only in homelab (scripts/ci/ lives here); the file is not deployed to the
# mirrored repos, so it does not run on their PRs. Both tests are offline (no
# mirrored repos, so it does not run on their PRs. All tests here are offline (no
# token, no network, no cluster) — the ci-claude image just provides sh + jq, and
# it is already pulled by pr-review.yml on the same event, so the runner has it
# cached.
when:
- event: pull_request
# `depends_on: []` on both so they run in parallel and one failure does not hide
# `depends_on: []` on every step so they run in parallel and one failure does not hide
# the other. Same reasoning as the note above `steps:` in ci-manifests-test.yml.
steps:
- name: model-selection-tests
@@ -44,3 +44,9 @@ steps:
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.280
commands:
- sh scripts/ci/test-promtool-rules.sh
- name: yaml-citations-guard-tests
depends_on: []
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.280
commands:
- sh scripts/ci/test-yaml-citations.sh
+7
View File
@@ -0,0 +1,7 @@
# Contributing
## Citing other YAML from a comment
- Mark the block you cite with a comment line `# anchor: <name>` (kebab-case, unique in that file). If the block is inside a `|` block scalar, put the marker just above the key, never inside the scalar.
- Cite it as `path.yaml#<name>`, with the path relative to the citing file's directory or to the repo root. Never cite `path.yaml:<line>`, because line numbers go stale on any edit above them.
- `scripts/ci/yaml-citations.sh` enforces this in `ci-manifests-test.yml`: it fails on line-number citations and on citations whose file or anchor does not exist.
+5 -3
View File
@@ -40,12 +40,13 @@ metadata:
# fixed in place because the sentence lives inside the `config.yaml: |` block
# scalar below: any edit to it changes ConfigMap DATA, and
# `configmap.reloader.stakater.com/reload: gatus-config`
# (gatus/deployment.yaml:13) would then roll the gatus Deployment — a
# (deployment.yaml#reloader) would then roll the gatus Deployment — a
# production restart for a comment. This header is outside `data:`, so it is
# inert; the served config is byte-identical.
#
# * infrastructure/network-policies/paperless-ns.yaml:34-36 ALREADY admits
# the whole monitoring namespace on every port, and monitoring is where
# * ../../infrastructure/network-policies/paperless-ns.yaml#monitoring-admit
# ALREADY admits the whole monitoring namespace on every port, and
# monitoring is where
# blackbox-exporter runs. Cilium unions the allow rules of every CNP
# selecting an endpoint, so no extra policy is required.
# * The in-cluster paperless probe therefore exists, added with no change
@@ -60,6 +61,7 @@ metadata:
# The Web Service 11 entry below stays a reachability-only check on purpose.
# The app-health half of the pair is the blackbox probe named above.
data:
# anchor: gatus-endpoints
config.yaml: |
storage:
type: sqlite
+1
View File
@@ -6,6 +6,7 @@ metadata:
labels:
app: gatus
annotations:
# anchor: reloader
# Gatus reads its config once at startup and never re-reads the file, so
# without this a ConfigMap change reconciles cleanly and then does nothing:
# git and the running checks drift apart silently, with no failed resource
@@ -93,10 +93,11 @@ spec:
# are the first targets here that leave the monitoring namespace.
#
# Both sit entirely behind an Authentik outpost — navidrome.renner.uno
# on ak-outpost-navidrome-gate (navidrome/ingress.yaml:21),
# lidarr.renner.uno on ak-outpost-lidarr-gate
# (navidrome/lidarr.yaml:218) — so an EXTERNAL probe of either name
# measures the gate, not the app, and stays green while the app behind
# on ak-outpost-navidrome-gate
# (../navidrome/ingress.yaml#navidrome-gate), lidarr.renner.uno on
# ak-outpost-lidarr-gate (../navidrome/lidarr.yaml#lidarr-gate) — so an
# EXTERNAL probe of either name measures the gate, not the app, and
# stays green while the app behind
# a live gate is dead. That external signal is still wanted, and REN-2
# adds it from UptimeRobot; these are the other half of the pair.
#
@@ -124,7 +125,7 @@ spec:
# infrastructure/network-policies/monitoring-ns.yaml
# (default-deny-ingress) and monitoring-alloy-syslog.yaml
# (allow-syslog-world-ingress). blackbox-exporter runs in that same
# namespace (apps/monitoring/helmrelease-blackbox-exporter.yaml:5).
# namespace (../monitoring/helmrelease-blackbox-exporter.yaml#namespace).
# Re-verified at 2b71dda1 across all 635 yaml files in the repo — 73
# Cilium objects, 38 carrying an egress or egressDeny block, none of
# them in the monitoring namespace, and the single
@@ -142,9 +143,10 @@ spec:
# gatus checks, so status.renner.uno is unaffected and no workload is
# restarted. What breaks is the signal, and it breaks misleadingly.
# All three targets start failing BlackboxProbeFailing
# (prometheusrule-blackbox.yaml:34 — severity warning, for: 10m); none
# of the three is in that rule's exclusion list. BlackboxProbesAbsent
# does NOT fire, because the other 17 targets keep producing
# (prometheusrule-blackbox.yaml#blackbox-probe-failing — severity
# warning, for: 10m); none of the three is in that rule's exclusion
# list. BlackboxProbesAbsent does NOT fire, because the other 17
# targets keep producing
# probe_success — so it reads as three apps going down, not as a
# policy change. WHO NOTICES: warning routes to the default `email`
# receiver, admin@renner.uno (helmrelease-prometheus.yaml alertmanager
@@ -156,16 +158,17 @@ spec:
# `flux reconcile kustomization monitoring-rules --with-source`.
#
# /ping on both: unauthenticated, and already the pods' own kubelet
# probe path (navidrome/deployment.yaml:80-93,
# navidrome/lidarr.yaml:114-127). For lidarr that it is ungated is
# proven independently of the gate — lidarr.yaml:29 curls /ping in a
# probe path (../navidrome/deployment.yaml#kubelet-probes,
# ../navidrome/lidarr.yaml#kubelet-probes). For lidarr that it is
# ungated is proven independently of the gate — its reconcile script
# (../navidrome/lidarr.yaml#reconcile-script) curls /ping in a
# loop BEFORE it has read the API key out of config.xml.
#
# STATUS CODE, unproven for all three of the new targets: module
# http_2xx follows redirects and passes only on a FINAL 2xx, while
# every piece of repo evidence behind these paths establishes only
# `< 400` — a kubelet httpGet accepts 200-399, and lidarr.yaml:29's
# `curl -f` without -L exits 0 on a 302. That evidence proves the
# `< 400` — a kubelet httpGet accepts 200-399, and that reconcile
# script's `curl -f` without -L exits 0 on a 302. That evidence proves the
# paths are UNGATED, not that they answer 200. Check
# probe_http_status_code on the first scrapes; a target that settles
# on a 3xx gets REVERTED, not widened to accept 3xx.
@@ -207,10 +210,12 @@ spec:
# BLAST RADIUS paragraph above.
- http://navidrome.navidrome.svc.cluster.local:80/ping
- http://lidarr.navidrome.svc.cluster.local:80/ping
# anchor: paperless-app-health
# APP-HEALTH probe for paperless. Same rule as the block above
# (monitoring-vantage-point-rule); the blind spot it closes is the one
# gatus already documents at apps/gatus/configmap.yaml:205-214 —
# through the paperless-gate outpost EVERY path, /api/ included,
# gatus already documents in its Web Service 11 comment
# (../gatus/configmap.yaml#gatus-endpoints) — through the
# paperless-gate outpost EVERY path, /api/ included,
# answers 302 to an unauthenticated caller, so no external probe of
# paperless.renner.uno can tell a live app from a live gate. That
# external signal is still wanted, and REN-2 adds it from UptimeRobot;
@@ -218,10 +223,12 @@ spec:
#
# That gatus comment says the in-cluster probe "needs a
# CiliumNetworkPolicy first". That sentence is simply WRONG, not
# merely inapplicable: paperless-ns.yaml:34-36 already admits the
# whole monitoring namespace, which is where blackbox-exporter runs,
# merely inapplicable:
# ../../infrastructure/network-policies/paperless-ns.yaml#monitoring-admit
# already admits the whole monitoring namespace, which is where
# blackbox-exporter runs,
# and Cilium unions the allow rules of every CNP selecting an
# endpoint (the mechanism apps/paperless/network-policy.yaml:22-26
# endpoint (the mechanism ../paperless/network-policy.yaml#cnp-union
# relies on). The EGRESS paragraph in the block above applies here
# identically. Nothing in the paperless namespace is edited by this
# change.
@@ -229,19 +236,22 @@ spec:
# The stale sentence is deliberately left in place in
# apps/gatus/configmap.yaml: it sits INSIDE the `config.yaml: |`
# block scalar, so editing it changes ConfigMap data and
# `configmap.reloader.stakater.com/reload` (gatus/deployment.yaml:13)
# rolls the gatus Deployment. The correction is written at the top of
# `configmap.reloader.stakater.com/reload`
# (../gatus/deployment.yaml#reloader) rolls the gatus Deployment. The
# correction is written at the top of
# that file instead, outside `data:`, where it costs no restart.
#
# /accounts/login/ because paperless-ngx has no unauthenticated health
# endpoint at all. It is the app's own established "the webserver
# answers" signal — the same path its startup, liveness and readiness
# probes use (paperless.yaml:252-272) — and it renders a Django view
# probes use (../paperless/paperless.yaml#http-probes) — and it
# renders a Django view
# rather than a static file. It does NOT cover the consumer/celery
# workers: see process-liveness-vs-protocol-liveness.
#
# The cluster-local Host header is accepted only because
# PAPERLESS_ALLOWED_HOSTS is "*" (paperless.yaml:27). Narrow that and
# PAPERLESS_ALLOWED_HOSTS is "*"
# (../paperless/paperless.yaml#allowed-hosts). Narrow that and
# Django answers 400 here and this probe goes permanently red.
#
# This is the target most likely to fail the STATUS CODE check in the
@@ -335,10 +345,12 @@ spec:
# WHY A SEPARATE jobName, and why these must NEVER be folded into the
# blackbox-http Probe above: after PR 2b these targets fail permanently and BY
# DESIGN. BlackboxProbeFailing and BlackboxProbesAbsent
# (prometheusrule-blackbox.yaml:35,50) are both scoped to job="blackbox-http",
# so a distinct job is outside both and stays silent — and the hand-maintained
# instance!~ exclusion regex at :35, whose fragility that file warns about, does
# not have to grow a fifth entry. Adding these to blackbox-http instead would
# (prometheusrule-blackbox.yaml#blackbox-probe-failing and
# prometheusrule-blackbox.yaml#blackbox-probes-absent) are both scoped to
# job="blackbox-http", so a distinct job is outside both and stays silent — and
# the hand-maintained instance!~ exclusion regex in the first of them, whose
# fragility that file warns about, does not have to grow a fifth entry. Adding
# these to blackbox-http instead would
# page the on-call forever.
#
# The inverted alert that gives these targets teeth — CNPNegativeControlReachable,
@@ -377,12 +389,14 @@ spec:
# exporter's 9187). Neither that rule nor paperless-allow-blackbox
# exists yet at this commit — that is the point, and why this target
# must read 1 now. Service is CNPG-generated from Cluster `pprl-pg`
# (paperless/cluster.yaml:4) and is the name paperless itself dials
# (paperless.yaml:46).
# (../paperless/cluster.yaml#pprl-pg) and is the name paperless itself
# dials (../paperless/paperless.yaml#db-host).
- pprl-pg-rw.paperless.svc.cluster.local:5432
# Three witnesses that the whole-namespace `monitoring` admit is gone
# for workloads no new rule selects. Ports from the Services themselves:
# redis.yaml:76, tika.yaml:57, gotenberg.yaml:63.
# ../paperless/redis.yaml#service-port,
# ../paperless/tika.yaml#service-port,
# ../paperless/gotenberg.yaml#service-port.
- redis.paperless.svc.cluster.local:6379
- tika.paperless.svc.cluster.local:9998
- gotenberg.paperless.svc.cluster.local:3000
@@ -23,7 +23,8 @@
#
# NO POLICY CHANGE IS NEEDED TO MAKE THESE WORK TODAY. Ingress: the paperless
# baseline still admits the whole `monitoring` namespace on every port
# (infrastructure/network-policies/paperless-ns.yaml:34-36) — that admission is
# (../../infrastructure/network-policies/paperless-ns.yaml#monitoring-admit) —
# that admission is
# exactly what REN-28 removes. Egress: the `monitoring` namespace is in no
# egress enforcement at all; its only two Cilium policies, monitoring-ns.yaml
# and monitoring-alloy-syslog.yaml, are ingress-only. Same dependency the three
@@ -88,7 +89,8 @@ spec:
# The test as written in §7 says "a paperless POD port other than
# 8000"; there is no such port. The paperless pod's only other listener
# is the bridge-tls-proxy socat, bound to 127.0.0.1:11143
# (apps/paperless/paperless.yaml:289), so every other port on that pod
# (../paperless/paperless.yaml#bridge-tls-proxy), so every other port
# on that pod
# is closed today and answers RST — there is no "reachable before" to
# record and the port half of paperless-allow-blackbox (:8000) cannot
# be tested at all. gotenberg is the half that CAN be: it is one of the
@@ -31,6 +31,7 @@ spec:
# target means. A target whose loss deserves a critical page gets its
# own rule — and lands in the list above. That is what keeps the
# exclusion list from being arbitrary.
# anchor: blackbox-probe-failing
- alert: BlackboxProbeFailing
expr: probe_success{job="blackbox-http", instance!~"https://fsn1\\.your-objectstorage\\.com|https://netbird\\.renner\\.uno|http://proxy-[12]\\.renner\\.uno:9000/health|https://ci\\.renner\\.uno|https://openbao\\.renner\\.uno/v1/sys/health.*"} == 0
for: 10m
@@ -46,6 +47,7 @@ spec:
# The check that the checks are running. Every rule above reads
# probe_success; if the series stop being produced at all, none of them
# can fire and the silence looks exactly like health.
# anchor: blackbox-probes-absent
- alert: BlackboxProbesAbsent
expr: absent(probe_success{job="blackbox-http"})
for: 15m
@@ -2,6 +2,7 @@ apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: blackbox-exporter
# anchor: namespace
namespace: monitoring
spec:
interval: 30m
@@ -83,6 +83,7 @@ spec:
- name: music
mountPath: /music
readOnly: true
# anchor: kubelet-probes
livenessProbe:
httpGet:
path: /ping
+1
View File
@@ -16,6 +16,7 @@ spec:
paths:
- path: /
pathType: Prefix
# anchor: navidrome-gate
backend:
service:
name: ak-outpost-navidrome-gate
+3
View File
@@ -23,6 +23,7 @@ metadata:
name: lidarr-reconcile
namespace: navidrome
data:
# anchor: reconcile-script
reconcile.sh: |
set -eu
API=http://localhost:8686/api/v1
@@ -111,6 +112,7 @@ spec:
mountPath: /config
- name: music
mountPath: /music
# anchor: kubelet-probes
livenessProbe:
httpGet:
path: /ping
@@ -213,6 +215,7 @@ spec:
paths:
- path: /
pathType: Prefix
# anchor: lidarr-gate
backend:
service:
name: ak-outpost-lidarr-gate
@@ -58,7 +58,8 @@
# aggregation did not already cover them. It does, so none is added:
# `flux-view-flux-system` carries `rbac.authorization.k8s.io/aggregate-to-view:
# "true"` and lists get/list/watch on `*` across all six API groups —
# `../../clusters/homelab/flux-system/gotk-components.yaml:232-254`. The
# the `flux-view-flux-system` ClusterRole in
# `../../clusters/homelab/flux-system/gotk-components.yaml`. The
# aggregation controller merges those rules into `view` itself, so this one
# binding picks up kustomizations, helmreleases, gitrepositories, imagepolicies,
# imagerepositories and imageupdateautomations. Read from git, not from a live
+1
View File
@@ -1,6 +1,7 @@
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
# anchor: pprl-pg
name: pprl-pg
namespace: paperless
annotations:
+1
View File
@@ -59,6 +59,7 @@ metadata:
spec:
selector:
app.kubernetes.io/name: gotenberg
# anchor: service-port
ports:
- port: 3000
targetPort: http
@@ -11,14 +11,19 @@
# the allow rules of every CNP selecting an endpoint (the same mechanism
# described below). The effective ingress set is this outpost PLUS, on
# every port:
# - the paperless namespace (paperless-ns.yaml:9-11)
# - the database namespace (paperless-ns.yaml:31-33)
# - the monitoring namespace (paperless-ns.yaml:34-36)
# - host / remote-node (paperless-ns.yaml:37-39)
# - the paperless namespace
# ../../infrastructure/network-policies/paperless-ns.yaml#paperless-admit
# - the database namespace
# ../../infrastructure/network-policies/paperless-ns.yaml#database-admit
# - the monitoring namespace
# ../../infrastructure/network-policies/paperless-ns.yaml#monitoring-admit
# - host / remote-node
# ../../infrastructure/network-policies/paperless-ns.yaml#host-admit
# So any pod in database or monitoring can reach this HTTP listener
# directly and set X-Authentik-Username to any value. Do not read this
# rule as the whole boundary. Narrowing is tracked in REN-28.
#
# anchor: cnp-union
# This is a CiliumNetworkPolicy (not a standard NetworkPolicy) so it
# composes additively with the cluster's baseline CNP
# `default-deny-ingress` for the paperless namespace — Cilium's allow
+4
View File
@@ -24,6 +24,7 @@ data:
PAPERLESS_ACCOUNT_DEFAULT_GROUPS: "Familie"
PAPERLESS_URL: "https://paperless.renner.uno"
PAPERLESS_CSRF_TRUSTED_ORIGINS: "https://paperless.renner.uno,https://docs.renner.uno"
# anchor: allowed-hosts
PAPERLESS_ALLOWED_HOSTS: "*"
PAPERLESS_TIME_ZONE: "Europe/Berlin"
PAPERLESS_OCR_LANGUAGE: "deu+eng"
@@ -43,6 +44,7 @@ data:
PAPERLESS_CONSUMER_POLLING_INTERVAL: "10"
PAPERLESS_REDIS: "redis://redis:6379"
PAPERLESS_DBENGINE: "postgresql"
# anchor: db-host
PAPERLESS_DBHOST: "pprl-pg-rw"
PAPERLESS_DBNAME: "paperless"
PAPERLESS_TIKA_ENABLED: "1"
@@ -249,6 +251,7 @@ spec:
# answers, giving that boot 30 * 20s = 10 minutes. Without it the
# 120s liveness delay plus three 30s periods kills the pod 210s in,
# part-way through a migration, and the next boot starts over.
# anchor: http-probes
startupProbe:
httpGet:
path: /accounts/login/
@@ -282,6 +285,7 @@ spec:
capabilities:
drop: ["ALL"]
add: ["CHOWN", "DAC_OVERRIDE", "FOWNER", "SETGID", "SETUID"]
# anchor: bridge-tls-proxy
- name: bridge-tls-proxy
image: alpine/socat:1.8.0.3
args:
+1
View File
@@ -72,6 +72,7 @@ metadata:
spec:
selector:
app.kubernetes.io/name: redis
# anchor: service-port
ports:
- port: 6379
targetPort: redis
+1
View File
@@ -53,6 +53,7 @@ metadata:
spec:
selector:
app.kubernetes.io/name: tika
# anchor: service-port
ports:
- port: 9998
targetPort: http
@@ -36,7 +36,7 @@
# raised — no further PR needed. Until then this object logs one refused POST
# every 300s and nothing else: nothing pages, nothing degrades, and
# `audiobooks.renner.uno` stays covered by its 60s Gatus check
# (apps/gatus/configmap.yaml:163) throughout.
# (Web Service 8 in ../gatus/configmap.yaml#gatus-endpoints) throughout.
#
# REVERT: revert this commit. Flux prunes the object again and, because the
# monitor does not exist to begin with, nothing in UptimeRobot changes.
@@ -72,7 +72,8 @@
# namespace, and the reason is structural rather than a permissions gap:
#
# * `IngressURLSource` has exactly one field, `name` — there is no namespace
# field on it (crd-endpointmonitors.yaml:443-459).
# field on it (crd-endpointmonitors.yaml, schema path
# `spec.urlFrom.ingressRef`).
# * `discoverURLFromRefs` resolves that name against
# `ingressMonitor.Namespace` — the EndpointMonitor's OWN namespace
# (v2.2.13 pkg/kube/util/url.go:71 → :38-40).
@@ -240,7 +241,7 @@ metadata:
spec:
# `https://www.renner.uno` and not `/wp-admin` or `/wp-login.php`: those two
# paths are served by a SECOND Ingress that sits behind the Authentik outpost
# (apps/wordpress/ingress.yaml:50,57). `/` is the ungated one.
# (../wordpress/ingress.yaml#gated-paths). `/` is the ungated one.
url: https://www.renner.uno/
uptimeRobotConfig:
# `http`, not `keyword`. The survey recommends a keyword check here
@@ -273,7 +274,7 @@ spec:
# explicitly ignored when `spec.url` is set (pkg/kube/util/url.go:31-33).
# `/healthcheck` is ungated, so the check reaches application health rather
# than mere reachability — it is the same endpoint the Gatus check uses
# (apps/gatus/configmap.yaml:163).
# (Web Service 8 in ../gatus/configmap.yaml#gatus-endpoints).
url: https://audiobooks.renner.uno/healthcheck
uptimeRobotConfig:
monitorType: http
+1
View File
@@ -75,6 +75,7 @@ spec:
name: ak-outpost-wordpress-gate
port:
number: 9000
# anchor: gated-paths
# Covers admin-ajax.php too. That is only safe because the gift
# registry plugin -- the sole thing on this site that ever emitted
# an ajaxurl -- is being dropped in the migration. Verified against
@@ -6,6 +6,7 @@ metadata:
spec:
endpointSelector: {}
ingress:
# anchor: paperless-admit
- fromEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": paperless
@@ -25,15 +26,18 @@ spec:
# because both are load-bearing — CNPG reaches the postgres cluster
# from `database`, and the blackbox probe of
# http://paperless.paperless.svc.cluster.local:80/accounts/login/
# (kubernetes/apps/monitoring-rules/probes-blackbox.yaml:250) runs
# from `monitoring`.
# (kubernetes/apps/monitoring-rules/probes-blackbox.yaml#paperless-app-health)
# runs from `monitoring`.
# Scoping them to specific pods and ports is tracked in REN-28.
# anchor: database-admit
- fromEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": database
# anchor: monitoring-admit
- fromEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": monitoring
# anchor: host-admit
- fromEntities:
- host
- remote-node
+62
View File
@@ -0,0 +1,62 @@
#!/bin/sh
# Offline self-test for yaml-citations.sh: each case is a small fixture tree
# that must FAIL (or, for happy-path, PASS). Run: sh scripts/ci/test-yaml-citations.sh
set -eu
HERE="$(cd "$(dirname "$0")" && pwd)"
SUT="$HERE/yaml-citations.sh"
[ -f "$SUT" ] || { echo "FAIL: $SUT not found"; exit 1; }
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT INT TERM
fails=0
# new_tree NAME — a fixture with one anchored target the cases can cite.
new_tree() {
d="$TMP/$1"; mkdir -p "$d/k/a" "$d/k/b"
printf 'kind: X\n# anchor: target-block\nspec: {}\n' > "$d/k/a/target.yaml"
echo "$d"
}
# expect CASE want(pass|fail) DIR
expect() {
if YAML_CITATIONS_FLOOR=1 sh "$SUT" "$3" > "$TMP/$1.out" 2>&1; then got=pass; else got=fail; fi
if [ "$got" = "$2" ]; then echo "ok $1 ($got)"
else echo "FAIL $1: wanted $2, got $got"; sed 's/^/ /' "$TMP/$1.out"; fails=1; fi
}
d=$(new_tree happy-path)
printf '# see target.yaml#target-block and ../a/target.yaml#target-block\nx: 1\n' > "$d/k/a/same-dir.yaml"
printf '# root-relative: k/a/target.yaml#target-block\nx: 1\n' > "$d/k/b/root.yaml"
printf 'url: "http://h/p.yaml#frag" # a # inside a value is not a comment\n' > "$d/k/b/url.yaml"
expect happy-path pass "$d"
d=$(new_tree line-number)
printf '# see ../a/target.yaml:2\nx: 1\n' > "$d/k/b/c.yaml"
expect line-number fail "$d"
d=$(new_tree line-range-yml)
printf '# see ../a/target.yml:2-3\nx: 1\n' > "$d/k/b/c.yml"
expect line-range-yml fail "$d"
d=$(new_tree missing-file)
printf '# see ../a/gone.yaml#target-block\nx: 1\n' > "$d/k/b/c.yaml"
expect missing-file fail "$d"
d=$(new_tree missing-anchor)
printf '# see ../a/target.yaml#no-such-block\nx: 1\n' > "$d/k/b/c.yaml"
expect missing-anchor fail "$d"
d=$(new_tree duplicate-anchor)
printf '# anchor: target-block\n' >> "$d/k/a/target.yaml"
expect duplicate-anchor fail "$d"
d=$(new_tree malformed-anchor)
printf '# anchor: Not_Kebab\nx: 1\n' > "$d/k/b/c.yaml"
expect malformed-anchor fail "$d"
d="$TMP/empty-discovery"; mkdir -p "$d"
if sh "$SUT" "$d" > "$TMP/empty.out" 2>&1; then echo "FAIL empty-discovery: passed with no files"; fails=1
else echo "ok empty-discovery (fail)"; fi
exit $fails
+110
View File
@@ -0,0 +1,110 @@
#!/bin/sh
# Guard the citation convention in YAML comments (REN-183, see CONTRIBUTING.md).
#
# Run: sh scripts/ci/yaml-citations.sh [repo-root]
# Needs: sh, find, awk, grep (BusyBox is fine). Offline.
# Tests: scripts/ci/test-yaml-citations.sh
#
# A comment cites another block as `path.yaml#name`, and the cited block
# carries a marker line `# anchor: name`. `path` is relative to the citing
# file's directory or to the repo root. This fails when:
#
# * a comment cites by line number (`foo.yaml:12`). Line numbers go stale on
# the first unrelated edit above them, silently; three drift fixes were
# merged before this guard existed (REN-181).
# * a `path#name` citation names a file or an anchor that does not exist.
# * an anchor is not kebab-case, or is declared twice in one file.
#
# Existence only. It does not check that the anchored block still says what the
# citing comment claims — that stays a reviewer's job, and it is why this guard
# has no false positives from ordinary edits.
set -eu
ROOT="${1:-.}"
cd "$ROOT"
# Same loose-floor reasoning as ci-manifests-test.yml: catch discovery that
# silently returns nothing, not normal churn. ~650 YAML files exist today.
FLOOR="${YAML_CITATIONS_FLOOR:-50}"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT INT TERM
# Not `find -printf` (BusyBox lacks it).
find . -path ./.git -prune -o -type f \( -name '*.yaml' -o -name '*.yml' \) -print \
| sed 's|^\./||' | sort > "$TMP/files"
count=$(grep -c . "$TMP/files" || true)
if [ "$count" -lt "$FLOOR" ]; then
echo "FAIL: found only $count YAML files; floor is $FLOOR. Discovery is broken."
exit 1
fi
# One pass over every file. A YAML comment starts at a `#` that begins the line
# or follows whitespace, so a `#` inside a URL or a quoted value is not one.
# Emits tab-separated records:
# L file line text a line-number citation
# C file line path name a path#name citation
# A file line name an anchor declaration
# B file line text a malformed anchor declaration
while IFS= read -r f; do
awk -v f="$f" '
{
if (!match($0, /(^|[ \t])#/)) next
c = substr($0, RSTART)
if (c ~ /^[ \t]*# anchor:/) {
if (c ~ /^[ \t]*# anchor: [a-z0-9]+(-[a-z0-9]+)*[ \t]*$/) {
n = c; sub(/^[ \t]*# anchor: /, "", n); sub(/[ \t]*$/, "", n)
print "A\t" f "\t" NR "\t" n
} else {
print "B\t" f "\t" NR "\t" c
}
next
}
if (c ~ /\.ya?ml:[0-9]/) print "L\t" f "\t" NR "\t" c
while (match(c, /[A-Za-z0-9_.\/-]+\.ya?ml#[A-Za-z0-9_-]+/)) {
m = substr(c, RSTART, RLENGTH)
p = m; sub(/#.*/, "", p)
n = m; sub(/.*#/, "", n)
print "C\t" f "\t" NR "\t" p "\t" n
c = substr(c, RSTART + RLENGTH)
}
}' "$f"
done < "$TMP/files" > "$TMP/records"
fail=0
while IFS="$(printf '\t')" read -r kind f line a b; do
case "$kind" in
L)
echo "FAIL $f:$line cites by line number; use path.yaml#anchor instead"
echo " $a"
fail=1 ;;
B)
echo "FAIL $f:$line malformed anchor; want '# anchor: kebab-case-name'"
fail=1 ;;
C)
dir=$(dirname "$f")
if [ -f "$dir/$a" ]; then target="$dir/$a"
elif [ -f "$a" ]; then target="$a"
else
echo "FAIL $f:$line cites $a#$b, but no such file (tried $dir/$a and ./$a)"
fail=1; continue
fi
if ! grep -qE "^[[:space:]]*# anchor: $b[[:space:]]*\$" "$target"; then
echo "FAIL $f:$line cites $a#$b, but $target has no '# anchor: $b'"
fail=1
fi ;;
esac
done < "$TMP/records"
# Duplicate anchors in one file make a citation ambiguous.
awk -F '\t' '$1 == "A" { k = $2 "\t" $4; if (k in seen) print $2 ":" $3 " duplicate anchor " $4 " (first at line " seen[k] ")"; else seen[k] = $3 }' \
"$TMP/records" > "$TMP/dups"
if [ -s "$TMP/dups" ]; then
sed 's/^/FAIL /' "$TMP/dups"
fail=1
fi
anchors=$(grep -c '^A' "$TMP/records" || true)
cites=$(grep -c '^C' "$TMP/records" || true)
[ "$fail" -eq 0 ] && echo "ok $count YAML files, $anchors anchors, $cites path#anchor citations, 0 line-number citations"
exit $fail