#!/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