Skip to content

Commit bf406d6

Browse files
ci(governance): R5b — version-string drift gate in reusable (#329)
## Summary Adds R5b to the `security-policy` job in `governance-reusable.yml`, right alongside R0 / R1 (where the existing tooling-version integrity rules live). Hard-blocks `Version: x.y.z` / `**Version**: x.y.z` / `_Version_= v1.2.3` patterns in repo-root `*.md` + `*.adoc` files (excluding `CHANGELOG.{md,adoc}`). Defers all version state to `CHANGELOG.md` (release history), `Cargo.toml`'s `[package].version` (semver pin), and the git log (dates). ## Why now echidna shipped this exact rule locally last week as `governance-doc-drift.yml` R5b ([echidna#171](hyperpolymath/echidna#171)). The drift pattern that motivated it — `Version: 1.5.0` in `README.adoc`, `**Version**: 2.3.0` in `CLAUDE.md`, `2.1.0` in `Cargo.toml`, all simultaneously — generalises to every long-lived repo. ## Scope decision R5b only (version strings). R5a (bare prover counts) stays repo-local in echidna because the count semantics are domain-specific (prover backends vs. stdlib functions vs. proof obligations vs. syntax features). A future generic R5 with per-repo configurable canonical patterns is the natural extension, but R5b stands alone today. Sub-tree docs (`docs/handover/`, `docs/decisions/`, `docs/releases/`, audit reports) deliberately out of scope — owner-managed snapshots, not the canonical narrative surface. ## Baseline scan results - **standards** (this repo): 0 hits ✓ - **proven**: 0 hits ✓ - **ephapax**: 0 hits ✓ - **affinescript**: 0 hits ✓ - **typed-wasm**: 0 hits ✓ Estate-wide rollout is safe — no caller repo will be retroactively broken. ## Self-tested - `python3 -c "import yaml; yaml.safe_load(...)"` → OK - Negative test (synthetic): - `*Version*: 1.2.3` → fires ✓ - `Version: v0.5.0` → fires ✓ - `**Version**= 9.9.9` → fires ✓ - `This is v1.2.3 software.` → correctly does NOT fire (no `Version:` prefix anchored) ## Downstream rollout Caller repos consume by bumping their `governance.yml` wrapper SHA pin. echidna can also drop its local R5b (keeping R5a) once it bumps the pin. Cohort fan-out will follow this change. ## Test plan - [x] YAML safe_load OK - [x] Self-test on this repo's root docs → 0 hits - [x] Negative test on synthetic drift → fires - [x] Baseline-scan on 4 cohort repos → 0 hits each - [ ] CI on this PR - [ ] Downstream caller-repo SHA-pin bumps (echidna + cohort) Refs hyperpolymath/echidna#171.
1 parent d3f799c commit bf406d6

1 file changed

Lines changed: 43 additions & 0 deletions

File tree

.github/workflows/governance-reusable.yml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,49 @@ jobs:
572572
[ "$R1" -gt 0 ] && { echo "❌ [R1] $R1 unversioned family-tool install(s) — pin tool: <name>@<ver>"; exit 1; }
573573
echo "✅ Tooling version integrity passed (R1 clean; R4 advisory via standards/tasks/tooling-integrity-lint.sh)"
574574
575+
- name: Documentation version-string drift (R5b)
576+
# Estate canonical-reference drift policy. Forbids pinned
577+
# `Version: x.y.z` strings in load-bearing top-level docs
578+
# because they reliably go stale and disagree with each other
579+
# (echidna had v1.5.0 in README.adoc, v2.3.0 in CLAUDE.md,
580+
# v2.1.0 in Cargo.toml simultaneously — cleared by echidna#169
581+
# / #170 / #171). CHANGELOG.md is the canonical release-history
582+
# surface; Cargo.toml's [package].version is the canonical
583+
# semver pin; the git log carries dates. Duplicating either
584+
# in human prose invites drift.
585+
#
586+
# Scope: repo-root *.md + *.adoc only. Sub-tree historical
587+
# docs (docs/handover/, docs/decisions/, docs/releases/,
588+
# audit reports) are owner-managed snapshots and not in
589+
# scope. CHANGELOG.{md,adoc} explicitly skipped.
590+
#
591+
# Companion rule R5a (bare prover counts) is repo-local where
592+
# the count semantics are domain-specific — see
593+
# `echidna/.github/workflows/governance-doc-drift.yml`.
594+
run: |
595+
set -uo pipefail
596+
PATTERN='^[[:space:]]*[*_]{0,2}Version[*_]{0,2}[[:space:]]*[:=][[:space:]]*v?[0-9]+\.[0-9]+\.[0-9]+'
597+
R5B=0
598+
shopt -s nullglob
599+
for doc in *.md *.adoc; do
600+
[ -f "$doc" ] || continue
601+
case "$doc" in CHANGELOG.md|CHANGELOG.adoc) continue ;; esac
602+
while IFS= read -r hit; do
603+
[ -n "$hit" ] || continue
604+
echo "❌ [R5b] pinned version string: $doc:$hit"
605+
R5B=$((R5B+1))
606+
done < <(grep -nE "$PATTERN" "$doc" 2>/dev/null || true)
607+
done
608+
if [ "$R5B" -gt 0 ]; then
609+
echo ""
610+
echo "❌ [R5b] $R5B pinned version-string line(s) in load-bearing docs."
611+
echo "Fix: drop the embedded version; defer to CHANGELOG.md (release"
612+
echo "history) and Cargo.toml's [package].version (semver pin) or the"
613+
echo "equivalent package manifest. Git log carries dates."
614+
exit 1
615+
fi
616+
echo "✅ [R5b] Documentation version-string drift: clean."
617+
575618
quality:
576619
name: Code quality + docs
577620
runs-on: ${{ inputs.runs-on }}

0 commit comments

Comments
 (0)