hyperhive/scripts/check-dashboard-descriptions.sh
iris a7e7ccd7df ci: add dashboard-description lint
Checks every grafana dashboard panel/board description against a length
cap and a small keyword list (operator name, tracker-tag references) so
the descriptions just shortened don't regress. Wired into both CI and
pre-push, same as the other content lints.
2026-09-20 14:42:39 +02:00

90 lines
3.7 KiB
Shell
Executable file

#!/bin/sh
# CI lint: keeps grafana dashboard panel/board descriptions short and free of
# meta-commentary — a tooltip answers "what is this number", not "how did we
# arrive at this panel" or "what did someone say about it".
#
# Two checks: length (over MAX_LEN means it became prose, not a short
# answer — fix by deleting the editorializing, not trimming words), and
# keywords (the operator's name, hardcoded rather than read from config —
# the class of mistake is what's caught, not one specific name; and a
# tracker tag `#1234`, reusing check-issue-refs.sh's own pattern and its
# same known false-positive class, e.g. `PKCS#11` — no `lint:allow` escape
# hatch here though, since JSON has no comment syntax to carry the marker;
# rephrase to drop the `#digits` shape instead).
#
# Deliberately NOT a blanket ban on the word "issue": forge/gitea panels
# legitimately show issue-tracker counts ("Currently open issues, across
# every repo." is exactly the short description this check wants to keep).
# The tracker-tag pattern is the part machine-checkable; a prose
# meta-reference with no `#N` needs a human reviewer, same as elsewhere.
#
# Uses `jq` — the right tool for JSON, unlike its zero-dependency siblings
# in this directory. This script does NOT nix-shell for it itself: the
# caller declares that dependency (see `pre-push` and
# `.forgejo/workflows/ci.yml`, the same `nix shell nixpkgs#jq --command sh
# <this file>` shape the vale/shellcheck jobs already use for their own
# tools), so a leaf script never hides a nix invocation inside itself.
#
# Emits a CI error annotation per hit, exits 1 if any hit is found.
set -eu
dashboards_dir="nix/host-modules/swarm-grafana/dashboards"
max_len=100
tracker_pattern='#[0-9]{2,5}([^0-9a-zA-Z]|$)'
files="$(find "$dashboards_dir" -maxdepth 1 -name '*.json' 2>/dev/null | sort)"
if [ -z "$files" ]; then
echo 'check-dashboard-descriptions: no dashboard json files found — path moved?' >&2
exit 1
fi
violations=0
checked=0
for f in $files; do
# One JSON object per description hit: {"title": "...", "description": "..."}
# -- recursive `..` walk catches both schema shapes uniformly, since in
# both a description always sits beside a sibling title in the same object.
while IFS= read -r row; do
[ -n "$row" ] || continue
title="$(printf '%s' "$row" | jq -r '.title')"
desc="$(printf '%s' "$row" | jq -r '.description')"
[ -n "$desc" ] || continue
checked=$((checked + 1))
len="$(printf '%s' "$desc" | wc -m | tr -d ' ')"
if [ "$len" -gt "$max_len" ]; then
violations=$((violations + 1))
printf '::error file=%s::panel "%s" description is %s chars (max %s) — shorten it to what the number is, drop the reasoning\n' \
"$f" "$title" "$len" "$max_len"
fi
if printf '%s' "$desc" | grep -qiE 'mara'; then
violations=$((violations + 1))
printf '::error file=%s::panel "%s" description names the operator — describe the metric, not who asked for the panel\n' \
"$f" "$title"
fi
if printf '%s' "$desc" | grep -qE "$tracker_pattern"; then
violations=$((violations + 1))
printf '::error file=%s::panel "%s" description references a tracker issue — write prose that stands on its own\n' \
"$f" "$title"
fi
done <<EOF
$(jq -c '[.. | objects | select(has("title") and has("description"))] | .[]' "$f")
EOF
done
if [ "$checked" -eq 0 ]; then
echo 'check-dashboard-descriptions: found zero title+description pairs — extractor or tree changed' >&2
exit 1
fi
if [ "$violations" -gt 0 ]; then
printf 'check-dashboard-descriptions: %s violation(s) across %s description(s) checked\n' \
"$violations" "$checked" >&2
exit 1
fi
echo "check-dashboard-descriptions: $checked description(s) checked, clean" >&2
exit 0