[#454] adopt enforcement framework + STATUS; modernize librenms agent scripts
check-rules green; shellcheck fixes to dpkg.sh/os-updates.sh (legacy backticks, unquoted vars, array-quoted command builders); Discourse pointers added to basis docs; awx/k8s/compliance repos referenced. https://projects.knownelement.com/issues/454
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
#454
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# KNELIAC consumes no env secrets directly: Ansible vault secrets resolve
|
||||||
|
# via ~/.creds refs at runtime (see AGENTS.md secrets rule). Copy this file
|
||||||
|
# for local overrides if a playbook needs an env knob.
|
||||||
+8
-14
@@ -1,14 +1,8 @@
|
|||||||
# Ansible / AWX local artifacts
|
*.key
|
||||||
.fact_cache/
|
*.pem
|
||||||
.venv/
|
*.creds
|
||||||
*.retry
|
.env
|
||||||
*.log
|
.env.*
|
||||||
|
!.env.example
|
||||||
# Python
|
*.tmp
|
||||||
__pycache__/
|
vault-password*
|
||||||
*.pyc
|
|
||||||
|
|
||||||
# Editor / OS
|
|
||||||
.DS_Store
|
|
||||||
*.swp
|
|
||||||
*~
|
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# Makefile — convenience dispatch to scripts/.
|
||||||
|
#
|
||||||
|
# Not required. The scripts in scripts/ are the real entry points and work
|
||||||
|
# standalone. This file just gives you short verbs if you're at a terminal.
|
||||||
|
#
|
||||||
|
# In Mode 2 (Hermes/OWUI/MCP), agents call the scripts directly or via API —
|
||||||
|
# they don't need this file.
|
||||||
|
|
||||||
|
# Project-specific overrides for check-rules.sh
|
||||||
|
export PROJECT_DOC_EXEMPT ?= AGENTS.md STATUS.md WORKING.md README.md ADOPTING.md LICENSE .env.example questions-v1.md BASELINE-PROMPT.md PATTERNS.md
|
||||||
|
export PROJECT_DISCOURSE_HOST ?= community.turnsys.com
|
||||||
|
|
||||||
|
.PHONY: setup validate fast lint test garden up down status clean help
|
||||||
|
|
||||||
|
help: ## Show available targets
|
||||||
|
@grep -E '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | awk 'BEGIN{FS=":.*?## "}{printf " \033[36m%-12s\033[0m %s\n", $$1, $$2}'
|
||||||
|
|
||||||
|
setup: ## Install git hooks
|
||||||
|
@bash scripts/setup-hooks.sh
|
||||||
|
|
||||||
|
validate: ## Full rule audit (includes tests)
|
||||||
|
@bash scripts/check-rules.sh
|
||||||
|
|
||||||
|
fast: ## Fast rule audit (pre-commit equivalent)
|
||||||
|
@bash scripts/check-rules.sh --fast
|
||||||
|
|
||||||
|
lint: ## Lint shell scripts (shellcheck via docker)
|
||||||
|
@docker run --rm -v "$$(pwd):/mnt" koalaman/shellcheck:stable \
|
||||||
|
$$(find . -path ./.git -prune -o -path ./.tmp -prune -o -path ./vendor -prune -o -path ./node_modules -prune -o \( -name '*.sh' -o -name '*.bash' \) -print | sed 's|^\./|/mnt/|') || true
|
||||||
|
|
||||||
|
test: ## Run the test suite (override per project)
|
||||||
|
@bash scripts/test.sh
|
||||||
|
|
||||||
|
garden: ## Doc-sprawl / Discourse-migration report
|
||||||
|
@bash scripts/garden.sh
|
||||||
|
|
||||||
|
up: ## Bring up the docker-compose stack
|
||||||
|
@bash scripts/up.sh
|
||||||
|
|
||||||
|
down: ## Bring down the docker-compose stack
|
||||||
|
@bash scripts/down.sh
|
||||||
|
|
||||||
|
status: ## Show repo status snapshot
|
||||||
|
@echo "== branch =="; git branch --show-current 2>/dev/null || echo "(no branch)"
|
||||||
|
@echo "== last commit =="; git log --oneline -1 2>/dev/null || true
|
||||||
|
@echo "== working tree =="; git status --short 2>/dev/null || echo "(not a git repo)"
|
||||||
|
@echo "== STATUS.md head =="; sed -n '1,12p' STATUS.md 2>/dev/null || echo "(no STATUS.md)"
|
||||||
|
|
||||||
|
clean: ## Remove build/test artifacts (override per project)
|
||||||
|
@echo "make clean: nothing to clean — override this in your project's Makefile."
|
||||||
@@ -13,7 +13,7 @@ lives in `PFVCluster/provisioning/`, and is the project that **AWX**
|
|||||||
|
|
||||||
PFVCluster stopped being the catch-all: domain tooling moved to dedicated KNEL repos. Playbooks/roles here should reference these as their code homes (and vice versa — codified changes in those repos land here as AWX items per the IaC mandate #454):
|
PFVCluster stopped being the catch-all: domain tooling moved to dedicated KNEL repos. Playbooks/roles here should reference these as their code homes (and vice versa — codified changes in those repos land here as AWX items per the IaC mandate #454):
|
||||||
|
|
||||||
[siem](https://git.knownelement.com/KNEL/siem) · [netinfra](https://git.knownelement.com/KNEL/netinfra) · [ca](https://git.knownelement.com/KNEL/ca) · [monitoring](https://git.knownelement.com/KNEL/monitoring) · [inventory](https://git.knownelement.com/KNEL/inventory) · [facilities](https://git.knownelement.com/KNEL/facilities) · [perf](https://git.knownelement.com/KNEL/perf) · [backups](https://git.knownelement.com/KNEL/backups) · [mailgateway](https://git.knownelement.com/KNEL/mailgateway) · [secrets](https://git.knownelement.com/KNEL/secrets) · [apt-satellite](https://git.knownelement.com/KNEL/apt-satellite) · [security-onion](https://git.knownelement.com/KNEL/security-onion)
|
[siem](https://git.knownelement.com/KNEL/siem) · [netinfra](https://git.knownelement.com/KNEL/netinfra) · [ca](https://git.knownelement.com/KNEL/ca) · [monitoring](https://git.knownelement.com/KNEL/monitoring) · [inventory](https://git.knownelement.com/KNEL/inventory) · [facilities](https://git.knownelement.com/KNEL/facilities) · [perf](https://git.knownelement.com/KNEL/perf) · [backups](https://git.knownelement.com/KNEL/backups) · [mailgateway](https://git.knownelement.com/KNEL/mailgateway) · [secrets](https://git.knownelement.com/KNEL/secrets) · [apt-satellite](https://git.knownelement.com/KNEL/apt-satellite) · [security-onion](https://git.knownelement.com/KNEL/security-onion) · [awx](https://git.knownelement.com/KNEL/awx) · [k8s](https://git.knownelement.com/KNEL/k8s) · [compliance](https://git.knownelement.com/KNEL/compliance) (private)
|
||||||
|
|
||||||
Environment map (prod / preprod / sectestbed) + snapshot lifecycle runbook: [Discourse t/331](https://community.turnsys.com/t/331).
|
Environment map (prod / preprod / sectestbed) + snapshot lifecycle runbook: [Discourse t/331](https://community.turnsys.com/t/331).
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# STATUS.md
|
||||||
|
|
||||||
|
> Operations status lives on Discourse: https://community.turnsys.com/t/297
|
||||||
|
> This file is a pointer stub. Active ticket: [#454](https://projects.knownelement.com/issues/454) (100% IaC mandate)
|
||||||
|
> Questions: Redmine only (NeedsInput status) — questions-v*.md model retired 2026-09-05.
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
> Lane index + ops status: https://community.turnsys.com/t/297 — env map: https://community.turnsys.com/t/331
|
||||||
|
|
||||||
# KNELIAC Basis Analysis: KNELServerBuild → KNELIAC, and PFVCluster netinfra relocation (Redmine #472)
|
# KNELIAC Basis Analysis: KNELServerBuild → KNELIAC, and PFVCluster netinfra relocation (Redmine #472)
|
||||||
|
|
||||||
Date: 2026-08-28. Sources inspected read-only: `KNEL/KNELServerBuild`, `KNEL/KNELIAC`, `PFVCluster/netinfra` (+ targeted greps elsewhere in PFVCluster).
|
Date: 2026-08-28. Sources inspected read-only: `KNEL/KNELServerBuild`, `KNEL/KNELIAC`, `PFVCluster/netinfra` (+ targeted greps elsewhere in PFVCluster).
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
> Lane index + ops status: https://community.turnsys.com/t/297 — env map: https://community.turnsys.com/t/331
|
||||||
|
|
||||||
# KNELIAC Basis — Executive Summary
|
# KNELIAC Basis — Executive Summary
|
||||||
|
|
||||||
**Question:** How does legacy `KNELServerBuild` (bash) become `KNELIAC` (Ansible), and how does the PFVCluster netinfra dns/dhcp/ntp code (Redmine #472) move into KNELIAC?
|
**Question:** How does legacy `KNELServerBuild` (bash) become `KNELIAC` (Ansible), and how does the PFVCluster netinfra dns/dhcp/ntp code (Redmine #472) move into KNELIAC?
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Questions — RETIRED MODEL
|
||||||
|
|
||||||
|
Questions are Redmine tickets now (status: NeedsInput —
|
||||||
|
https://projects.knownelement.com/queries/11). This file stays only because
|
||||||
|
the repo framework requires it. Do not add questions here.
|
||||||
|
|
||||||
|
Repo ticket: [#454](https://projects.knownelement.com/issues/454).
|
||||||
|
Lane index on Discourse: https://community.turnsys.com/t/297
|
||||||
|
> Retired 2026-09-05.
|
||||||
@@ -8,15 +8,19 @@ if [ -x /usr/bin/dpkg-query ]; then
|
|||||||
|
|
||||||
[ -d /var/cache/librenms ] || mkdir -p /var/cache/librenms
|
[ -d /var/cache/librenms ] || mkdir -p /var/cache/librenms
|
||||||
|
|
||||||
if [ ! -e $FILE ]; then
|
refresh() {
|
||||||
dpkg-query -W --showformat='${Status} ${Package} ${Version} ${Architecture} ${Installed-Size}\n'|grep " installed "|cut -d\ -f4- > $FILE
|
dpkg-query -W --showformat='${Status} ${Package} ${Version} ${Architecture} ${Installed-Size}\n' \
|
||||||
|
| grep " installed " | cut -d\ -f4- > "$FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ ! -e "$FILE" ]; then
|
||||||
|
refresh
|
||||||
fi
|
fi
|
||||||
FILEMTIME=$(stat -c %Y $FILE)
|
FILEMTIME=$(stat -c %Y "$FILE")
|
||||||
FILEAGE=$(($DATE-$FILEMTIME))
|
FILEAGE=$((DATE - FILEMTIME))
|
||||||
if [ $FILEAGE -gt 1800 ]; then
|
if [ "$FILEAGE" -gt 1800 ]; then
|
||||||
dpkg-query -W --showformat='${Status} ${Package} ${Version} ${Architecture} ${Installed-Size}\n'|grep " installed "|cut -d\ -f4- > $FILE
|
refresh
|
||||||
fi
|
fi
|
||||||
echo "<<<dpkg>>>"
|
echo "<<<dpkg>>>"
|
||||||
cat $FILE
|
cat "$FILE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -12,59 +12,59 @@
|
|||||||
################################################################
|
################################################################
|
||||||
BIN_WC='/usr/bin/wc'
|
BIN_WC='/usr/bin/wc'
|
||||||
BIN_GREP='/bin/grep'
|
BIN_GREP='/bin/grep'
|
||||||
CMD_GREP='-c'
|
CMD_GREP=(-c)
|
||||||
CMD_WC='-l'
|
CMD_WC=(-l)
|
||||||
BIN_ZYPPER='/usr/bin/zypper'
|
BIN_ZYPPER='/usr/bin/zypper'
|
||||||
CMD_ZYPPER='-q lu'
|
CMD_ZYPPER=(-q lu)
|
||||||
BIN_YUM='/usr/bin/yum'
|
BIN_YUM='/usr/bin/yum'
|
||||||
CMD_YUM='-q check-update'
|
CMD_YUM=(-q check-update)
|
||||||
BIN_DNF='/usr/bin/dnf'
|
BIN_DNF='/usr/bin/dnf'
|
||||||
CMD_DNF='-q check-update'
|
CMD_DNF=(-q check-update)
|
||||||
BIN_APT='/usr/bin/apt-get'
|
BIN_APT='/usr/bin/apt-get'
|
||||||
CMD_APT='-qq -s upgrade'
|
CMD_APT=(-qq -s upgrade)
|
||||||
BIN_PACMAN='/usr/bin/pacman'
|
BIN_PACMAN='/usr/bin/pacman'
|
||||||
CMD_PACMAN='-Sup'
|
CMD_PACMAN=(-Sup)
|
||||||
|
|
||||||
################################################################
|
################################################################
|
||||||
# Don't change anything unless you know what are you doing #
|
# Don't change anything unless you know what are you doing #
|
||||||
################################################################
|
################################################################
|
||||||
if [ -f $BIN_ZYPPER ]; then
|
if [ -f "$BIN_ZYPPER" ]; then
|
||||||
# OpenSUSE
|
# OpenSUSE
|
||||||
UPDATES=`$BIN_ZYPPER $CMD_ZYPPER | $BIN_WC $CMD_WC`
|
UPDATES=$("$BIN_ZYPPER" "${CMD_ZYPPER[@]}" | "$BIN_WC" "${CMD_WC[@]}")
|
||||||
if [ $UPDATES -ge 2 ]; then
|
if [ "$UPDATES" -ge 2 ]; then
|
||||||
echo $(($UPDATES-2));
|
echo $((UPDATES - 2));
|
||||||
else
|
else
|
||||||
echo "0";
|
echo "0";
|
||||||
fi
|
fi
|
||||||
elif [ -f $BIN_DNF ]; then
|
elif [ -f "$BIN_DNF" ]; then
|
||||||
# Fedora
|
# Fedora
|
||||||
UPDATES=`$BIN_DNF $CMD_DNF | $BIN_WC $CMD_WC`
|
UPDATES=$("$BIN_DNF" "${CMD_DNF[@]}" | "$BIN_WC" "${CMD_WC[@]}")
|
||||||
if [ $UPDATES -ge 1 ]; then
|
if [ "$UPDATES" -ge 1 ]; then
|
||||||
echo $(($UPDATES-1));
|
echo $((UPDATES - 1));
|
||||||
else
|
else
|
||||||
echo "0";
|
echo "0";
|
||||||
fi
|
fi
|
||||||
elif [ -f $BIN_PACMAN ]; then
|
elif [ -f "$BIN_PACMAN" ]; then
|
||||||
# Arch
|
# Arch
|
||||||
UPDATES=`$BIN_PACMAN $CMD_PACMAN | $BIN_WC $CMD_WC`
|
UPDATES=$("$BIN_PACMAN" "${CMD_PACMAN[@]}" | "$BIN_WC" "${CMD_WC[@]}")
|
||||||
if [ $UPDATES -ge 1 ]; then
|
if [ "$UPDATES" -ge 1 ]; then
|
||||||
echo $(($UPDATES-1));
|
echo $((UPDATES - 1));
|
||||||
else
|
else
|
||||||
echo "0";
|
echo "0";
|
||||||
fi
|
fi
|
||||||
elif [ -f $BIN_YUM ]; then
|
elif [ -f "$BIN_YUM" ]; then
|
||||||
# CentOS / Redhat
|
# CentOS / Redhat
|
||||||
UPDATES=`$BIN_YUM $CMD_YUM | $BIN_WC $CMD_WC`
|
UPDATES=$("$BIN_YUM" "${CMD_YUM[@]}" | "$BIN_WC" "${CMD_WC[@]}")
|
||||||
if [ $UPDATES -ge 1 ]; then
|
if [ "$UPDATES" -ge 1 ]; then
|
||||||
echo $(($UPDATES-1));
|
echo $((UPDATES - 1));
|
||||||
else
|
else
|
||||||
echo "0";
|
echo "0";
|
||||||
fi
|
fi
|
||||||
elif [ -f $BIN_APT ]; then
|
elif [ -f "$BIN_APT" ]; then
|
||||||
# Debian / Devuan / Ubuntu
|
# Debian / Devuan / Ubuntu
|
||||||
UPDATES=`$BIN_APT $CMD_APT | $BIN_GREP $CMD_GREP 'Inst'`
|
UPDATES=$("$BIN_APT" "${CMD_APT[@]}" | "$BIN_GREP" "${CMD_GREP[@]}" 'Inst')
|
||||||
if [ $UPDATES -ge 1 ]; then
|
if [ "$UPDATES" -ge 1 ]; then
|
||||||
echo $UPDATES;
|
echo "$UPDATES";
|
||||||
else
|
else
|
||||||
echo "0";
|
echo "0";
|
||||||
fi
|
fi
|
||||||
|
|||||||
Executable
+274
@@ -0,0 +1,274 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# check-rules.sh — project rule audit engine.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# bash scripts/check-rules.sh # full audit (verbose, includes slow checks)
|
||||||
|
# bash scripts/check-rules.sh --fast # fast audit (quiet, skips slow checks) — for pre-commit
|
||||||
|
# bash scripts/check-rules.sh --quiet # full audit, only prints failures
|
||||||
|
#
|
||||||
|
# Exit code: 0 = all rules pass (warnings are non-fatal), 1 = one or more FAILED.
|
||||||
|
#
|
||||||
|
# This is a generalized version of the rules engine proven in the
|
||||||
|
# RCEO-PersonalAssistant project. Add project-specific checks by appending
|
||||||
|
# `check "<desc>" "<pass|warn|fail>"` calls below.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source "$HERE/lib/common.sh"
|
||||||
|
REPO_ROOT="$(repo_root)"
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
|
# --- argument parsing ---
|
||||||
|
RULE_FAST=false
|
||||||
|
RULE_VERBOSE=true
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--fast) RULE_FAST=true; RULE_VERBOSE=false ;;
|
||||||
|
--quiet) RULE_VERBOSE=false ;;
|
||||||
|
*) die "check-rules.sh: unknown argument '$arg'" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
export RULE_FAST RULE_VERBOSE
|
||||||
|
|
||||||
|
init_counters
|
||||||
|
$RULE_VERBOSE && echo "=== Project Rule Audit ==="
|
||||||
|
|
||||||
|
TODAY="$(date +%Y-%m-%d)"
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# 1. Shellcheck — every .sh/.bash must pass (zero warnings, incl. info-level).
|
||||||
|
# Runs in Docker so the host stays clean (no native shellcheck required).
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
$RULE_VERBOSE && log_step "Shell scripts (shellcheck)"
|
||||||
|
mapfile -d '' SH_FILES < <(find . -path ./.git -prune -o -path ./.tmp -prune -o -path ./.crush -prune -o -path ./vendor -prune -o -path ./oam/librenms-agent/agent -prune -o -path ./archive -prune -o -path ./node_modules -prune -o \( -name '*.sh' -o -name '*.bash' \) -print0 2>/dev/null)
|
||||||
|
if [ "${#SH_FILES[@]}" -gt 0 ]; then
|
||||||
|
if have shellcheck; then
|
||||||
|
if shellcheck "${SH_FILES[@]}" >/dev/null 2>&1; then
|
||||||
|
check "All shell scripts pass shellcheck (host)" "pass"
|
||||||
|
else
|
||||||
|
check "shellcheck reports violations — run: shellcheck <file>" "fail"
|
||||||
|
fi
|
||||||
|
elif have docker; then
|
||||||
|
MNT_FILES=()
|
||||||
|
for f in "${SH_FILES[@]}"; do MNT_FILES+=("/mnt/${f#./}"); done
|
||||||
|
if docker run --rm -v "$REPO_ROOT:/mnt" koalaman/shellcheck:stable "${MNT_FILES[@]}" >/dev/null 2>&1; then
|
||||||
|
check "All shell scripts pass shellcheck (docker)" "pass"
|
||||||
|
else
|
||||||
|
check "shellcheck (docker) reports violations" "fail"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
check "No shellcheck or docker available to lint scripts" "warn"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
check "No shell scripts to lint" "pass"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# 2. Docker image pinning — no ':latest' tags in compose or Dockerfiles.
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
$RULE_VERBOSE && log_step "Docker image pinning"
|
||||||
|
if grep -rqE '(image:|FROM).*:latest' --include='docker-compose*.y*ml' --include='Dockerfile*' . 2>/dev/null; then
|
||||||
|
check "No ':latest' image tags (pin everything)" "fail"
|
||||||
|
else
|
||||||
|
check "No ':latest' image tags" "pass"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# 2b. Container naming — every service in a docker-compose file MUST set an
|
||||||
|
# explicit container_name (never rely on Docker's default <dir>_<n>).
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
$RULE_VERBOSE && log_step "Container naming"
|
||||||
|
COMPOSE_FILES="$(find . -path ./.git -prune -o \( -name 'docker-compose*.yml' -o -name 'docker-compose*.yaml' -o -name 'compose.yml' -o -name 'compose.yaml' \) -print 2>/dev/null || true)"
|
||||||
|
if [ -n "$COMPOSE_FILES" ]; then
|
||||||
|
BAD=0
|
||||||
|
while IFS= read -r cf; do
|
||||||
|
[ -n "$cf" ] || continue
|
||||||
|
# Count top-level service keys (2-space indent under services:) and
|
||||||
|
# compare against the number of container_name: declarations.
|
||||||
|
svc_count=$(awk '/^services:/{f=1;next} f&&/^[^[:space:]]/{f=0} f&&/^[[:space:]]{2}[[:alnum:]_-]+:[[:space:]]*$/{c++} END{print c+0}' "$cf")
|
||||||
|
cn_count=$(grep -cE '^[[:space:]]*container_name:' "$cf" 2>/dev/null || echo 0)
|
||||||
|
if [ "${svc_count:-0}" -gt 0 ] && [ "$cn_count" -lt "$svc_count" ]; then
|
||||||
|
BAD=$((BAD + 1))
|
||||||
|
fi
|
||||||
|
done <<EOF
|
||||||
|
$COMPOSE_FILES
|
||||||
|
EOF
|
||||||
|
if [ "$BAD" -eq 0 ]; then
|
||||||
|
check "All compose services set container_name" "pass"
|
||||||
|
else
|
||||||
|
check "$BAD compose file(s) with services missing container_name" "fail"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
check "No compose files (container-name check skipped)" "pass"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# 3. Required-files manifest — the files every project using this template owns.
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
$RULE_VERBOSE && log_step "Required files"
|
||||||
|
REQUIRED_FILES="AGENTS.md STATUS.md .env.example scripts/check-rules.sh scripts/setup-hooks.sh"
|
||||||
|
REQUIRED_FILES="$REQUIRED_FILES ${PROJECT_REQUIRED_FILES:-}"
|
||||||
|
for f in $REQUIRED_FILES; do
|
||||||
|
if [ -f "$f" ]; then check "$f exists" "pass"; else check "$f MISSING" "fail"; fi
|
||||||
|
done
|
||||||
|
if compgen -G 'questions-v*.md' > /dev/null; then
|
||||||
|
latest_q="$(find . -maxdepth 1 -name 'questions-v*.md' -printf '%f\n' | sort -V | tail -1)"
|
||||||
|
check "questions-v*.md exists ($latest_q)" "pass"
|
||||||
|
else
|
||||||
|
check "questions-v*.md MISSING" "fail"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# 4. Doc freshness — STATUS.md touched today.
|
||||||
|
# Warning (not failure): staleness is a signal, not a break.
|
||||||
|
# Redmine is the system of record for work; Discourse for docs. STATUS.md is
|
||||||
|
# a scratchpad only — see BASELINE-PROMPT.md §3, §8.
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
$RULE_VERBOSE && log_step "Doc freshness"
|
||||||
|
if [ -f STATUS.md ]; then
|
||||||
|
STATUS_DATE="$(grep -oE 'Last updated: [0-9]{4}-[0-9]{2}-[0-9]{2}' STATUS.md | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}' || echo unknown)"
|
||||||
|
if [ "$STATUS_DATE" = "$TODAY" ]; then
|
||||||
|
check "STATUS.md updated today ($STATUS_DATE)" "pass"
|
||||||
|
else
|
||||||
|
check "STATUS.md is stale (last: $STATUS_DATE, today: $TODAY) — update it" "warn"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
check "STATUS.md MISSING" "fail"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# 4b. Discourse pointer-header policy (MANDATORY).
|
||||||
|
# Discourse is the system of record for documentation. In-repo .md files are
|
||||||
|
# stubs that point to a Discourse topic URL. Operational files exempt.
|
||||||
|
# Override exemptions via PROJECT_DOC_EXEMPT (space-separated globs of
|
||||||
|
# basenames) and the Discourse host via PROJECT_DISCOURSE_HOST.
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
$RULE_VERBOSE && log_step "Discourse pointer-header"
|
||||||
|
DISCOURSE_HOST="${PROJECT_DISCOURSE_HOST:-community.turnsys.com}"
|
||||||
|
DOC_EXEMPT="${PROJECT_DOC_EXEMPT:-AGENTS.md STATUS.md WORKING.md README.md ADOPTING.md LICENSE .env.example questions-v*.md BASELINE-PROMPT.md PATTERNS.md}"
|
||||||
|
POINTER_MISSING=0
|
||||||
|
while IFS= read -r -d '' f; do
|
||||||
|
base="$(basename "$f")"
|
||||||
|
exempt=false
|
||||||
|
for pat in $DOC_EXEMPT; do
|
||||||
|
# shellcheck disable=SC2254
|
||||||
|
case "$base" in $pat) exempt=true; break ;; esac
|
||||||
|
done
|
||||||
|
[ "$exempt" = true ] && continue
|
||||||
|
if ! grep -qF "$DISCOURSE_HOST" "$f" 2>/dev/null; then
|
||||||
|
if [ "$POINTER_MISSING" -eq 0 ]; then
|
||||||
|
$RULE_VERBOSE && printf ' %s\n' "Missing $DISCOURSE_HOST URL in:"
|
||||||
|
fi
|
||||||
|
POINTER_MISSING=$((POINTER_MISSING + 1))
|
||||||
|
$RULE_VERBOSE && printf ' %s\n' "$f"
|
||||||
|
fi
|
||||||
|
done < <(find . -path ./.git -prune -o -path ./.crush -prune -o -path ./.tmp -prune -o -path ./vendor -prune -o -path ./archive -prune -o -name '*.md' -print0 2>/dev/null)
|
||||||
|
if [ "$POINTER_MISSING" -eq 0 ]; then
|
||||||
|
check "All non-exempt .md cite Discourse ($DISCOURSE_HOST)" "pass"
|
||||||
|
else
|
||||||
|
check "$POINTER_MISSING .md file(s) missing Discourse pointer (see BASELINE-PROMPT.md §3)" "fail"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# 5. Git state — uncommitted changes are a warning (the pre-push hook hardens
|
||||||
|
# this where it matters).
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
$RULE_VERBOSE && log_step "Git state"
|
||||||
|
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||||
|
if git diff --quiet && git diff --cached --quiet; then
|
||||||
|
check "Working tree clean" "pass"
|
||||||
|
else
|
||||||
|
check "Uncommitted changes present" "warn"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
check "Not a git repo (git checks skipped)" "pass"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# 6. Hooks installed — self-check that git hooks were set up.
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
$RULE_VERBOSE && log_step "Git hooks"
|
||||||
|
if [ -f .git/hooks/pre-commit ]; then
|
||||||
|
check "pre-commit hook installed" "pass"
|
||||||
|
else
|
||||||
|
check "pre-commit NOT installed (run: bash scripts/setup-hooks.sh)" "warn"
|
||||||
|
fi
|
||||||
|
if [ -f .git/hooks/pre-push ]; then
|
||||||
|
check "pre-push hook installed" "pass"
|
||||||
|
else
|
||||||
|
check "pre-push NOT installed (run: bash scripts/setup-hooks.sh)" "warn"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# 7. WORKING.md completion — no unchecked tasks may remain at commit time.
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
$RULE_VERBOSE && log_step "Task completion"
|
||||||
|
if [ -f WORKING.md ]; then
|
||||||
|
UNCHECKED="$(grep -cF -- '- [ ]' WORKING.md || true)"
|
||||||
|
if [ "$UNCHECKED" -eq 0 ]; then
|
||||||
|
check "WORKING.md has no unchecked tasks" "pass"
|
||||||
|
else
|
||||||
|
check "WORKING.md has ${UNCHECKED} unchecked task(s) — finish them before committing" "fail"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
check "WORKING.md absent (no active task tracker)" "pass"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# 8. CNW markers — empty `CNW:` markers flag unresolved questions for the human.
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
$RULE_VERBOSE && log_step "Unresolved questions"
|
||||||
|
EMPTY_CNW="$(grep -rn 'CNW:$' . --include='*.md' 2>/dev/null | head -20 || true)"
|
||||||
|
if [ -z "$EMPTY_CNW" ]; then
|
||||||
|
check "No empty CNW: markers (unresolved questions)" "pass"
|
||||||
|
else
|
||||||
|
CNW_COUNT="$(printf '%s\n' "$EMPTY_CNW" | grep -c . || true)"
|
||||||
|
check "${CNW_COUNT} unresolved CNW: marker(s) — needs user input" "warn"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# 9. Hygiene — merge-conflict markers and trailing whitespace must never land.
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
$RULE_VERBOSE && log_step "File hygiene"
|
||||||
|
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||||
|
# archive/ excluded: legacy files are preserved verbatim and may contain
|
||||||
|
# decorative `====` banners that false-positive as conflict markers.
|
||||||
|
CONFLICT="$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -v '^archive/' | xargs -r grep -lE '^(<{7}( \S+)?|=======|>{7}( \S+)?)$' 2>/dev/null || true)"
|
||||||
|
if [ -z "$CONFLICT" ]; then check "No merge-conflict markers staged" "pass"; else check "Merge-conflict markers staged: $CONFLICT" "fail"; fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# 10. (slow, skipped in --fast) Project test suite via scripts/test.sh.
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
if [ "$RULE_FAST" = false ] && [ -x scripts/test.sh ]; then
|
||||||
|
$RULE_VERBOSE && log_step "Test suite (scripts/test.sh)"
|
||||||
|
if bash scripts/test.sh >/dev/null 2>&1; then
|
||||||
|
check "scripts/test.sh passes" "pass"
|
||||||
|
else
|
||||||
|
check "scripts/test.sh FAILS" "fail"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# 11. Access-channel policy — qemu guest-agent must NEVER be used as an
|
||||||
|
# execution or key-delivery channel. SSH (sshd) is the ONLY approved
|
||||||
|
# remote access path; all commands must be logged through standard
|
||||||
|
# auth/audit infrastructure. ITAR/CMMC environment — non-negotiable.
|
||||||
|
# Allowed: installing/checking qemu-guest-agent for Proxmox state
|
||||||
|
# visibility. Forbidden: `qm guest exec` + any `vm-guest` wrapper.
|
||||||
|
# Scans CODE only (not .md) so docs may describe the ban.
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
$RULE_VERBOSE && log_step "Access-channel policy (no guest-agent exec)"
|
||||||
|
GA_HITS="$(grep -rnE 'qm guest exec|vm-guest|_vm_guest' \
|
||||||
|
--include='*.sh' --include='*.bash' --include='*.py' \
|
||||||
|
. 2>/dev/null | grep -vE 'scripts/check-rules\.sh|/archive/' || true)"
|
||||||
|
if [ -z "$GA_HITS" ]; then
|
||||||
|
check "No guest-agent exec / vm-guest access patterns in code" "pass"
|
||||||
|
else
|
||||||
|
GA_COUNT="$(printf '%s\n' "$GA_HITS" | grep -c . || true)"
|
||||||
|
$RULE_VERBOSE && printf '%s\n' "$GA_HITS" | sed 's/^/ /'
|
||||||
|
check "${GA_COUNT} guest-agent exec / vm-guest reference(s) — SSH-only access policy (AGENTS.md)" "fail"
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_summary_and_exit
|
||||||
Executable
+39
@@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# docker-run.sh — canonical ephemeral-container wrapper.
|
||||||
|
#
|
||||||
|
# Keeps the host clean: every build/test/generation runs inside a pinned image.
|
||||||
|
# Ensures output files are owned by the invoking user (not root).
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# docker-run.sh <image> [command...]
|
||||||
|
# Runs <command> in <image> with the repo mounted at /data, cwd /data.
|
||||||
|
# With no command, drops into the image's default entrypoint.
|
||||||
|
# docker-run.sh --shell <image>
|
||||||
|
# Interactive shell inside the container (for debugging).
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# docker-run.sh python:3.12-slim python3 -m pytest
|
||||||
|
# docker-run.sh pandoc/extra report.md -o report.pdf
|
||||||
|
# docker-run.sh --shell node:20
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source "$HERE/lib/common.sh"
|
||||||
|
|
||||||
|
SHELL_MODE=false
|
||||||
|
case "${1:-}" in
|
||||||
|
--shell) SHELL_MODE=true; shift ;;
|
||||||
|
-h|--help)
|
||||||
|
sed -n '2,18p' "$0"; exit 0 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
[ "$#" -ge 1 ] || { sed -n '2,18p' "$0"; exit 1; }
|
||||||
|
|
||||||
|
if [ "$SHELL_MODE" = true ]; then
|
||||||
|
# ${SHELL:-sh} must expand inside the container, not in this outer shell.
|
||||||
|
# shellcheck disable=SC2016
|
||||||
|
docker_run "$1" sh -c 'exec "${SHELL:-sh}"'
|
||||||
|
else
|
||||||
|
docker_run "$@"
|
||||||
|
fi
|
||||||
Executable
+65
@@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# garden.sh — the gardening loop.
|
||||||
|
#
|
||||||
|
# Reports doc sprawl and files that violate the "Discourse is the system of
|
||||||
|
# record for documentation; gitea .md files are stubs" policy. Run via
|
||||||
|
# `bash scripts/garden.sh`. Findings are WARNINGS (advisory); fix them at a natural break.
|
||||||
|
#
|
||||||
|
# What it checks:
|
||||||
|
# 1. Markdown sprawl: count of .md files per directory (top-10 by count).
|
||||||
|
# 2. Oversized .md files (default >300 lines) that don't cite a Discourse URL
|
||||||
|
# — candidates to migrate to Discourse, leaving a stub.
|
||||||
|
# 3. .md files with no Discourse link at all (informational; exempt: the
|
||||||
|
# operational files in EXEMPT_FILES).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source "$HERE/lib/common.sh"
|
||||||
|
REPO_ROOT="$(repo_root)"
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
|
SIZE_LIMIT="${GARDEN_MD_LINE_LIMIT:-300}"
|
||||||
|
# Operational files that legitimately live next to code, not in Discourse.
|
||||||
|
EXEMPT_FILES="${GARDEN_EXEMPT:-AGENTS.md STATUS.md WORKING.md questions-v.*.md PATTERNS.md BASELINE-PROMPT.md README.md}"
|
||||||
|
|
||||||
|
log_step "Gardening report for $REPO_ROOT"
|
||||||
|
|
||||||
|
# --- 1. sprawl by directory -------------------------------------------------
|
||||||
|
log_info "Markdown file count by directory (top 10):"
|
||||||
|
find . -path ./.git -prune -o -name '*.md' -print 2>/dev/null \
|
||||||
|
| sed 's|/[^/]*$||' | sort | uniq -c | sort -rn | head -10 | sed 's/^/ /'
|
||||||
|
|
||||||
|
# --- 2. oversized .md without a Discourse link ------------------------------
|
||||||
|
log_info "Oversized .md (>${SIZE_LIMIT} lines) lacking a Discourse URL — migrate candidates:"
|
||||||
|
OVERSIZED=0
|
||||||
|
while IFS= read -r -d '' f; do
|
||||||
|
# skip exempt files (glob match against basename and relative path)
|
||||||
|
exempt=false
|
||||||
|
base=$(basename "$f")
|
||||||
|
rel=${f#./}
|
||||||
|
for pat in $EXEMPT_FILES; do
|
||||||
|
# shellcheck disable=SC2254 # glob match is intentional
|
||||||
|
case "$base" in $pat) exempt=true; break ;; esac
|
||||||
|
# shellcheck disable=SC2254
|
||||||
|
case "$rel" in $pat) exempt=true; break ;; esac
|
||||||
|
done
|
||||||
|
[ "$exempt" = true ] && continue
|
||||||
|
lines=$(wc -l < "$f" 2>/dev/null || echo 0)
|
||||||
|
if [ "$lines" -gt "$SIZE_LIMIT" ]; then
|
||||||
|
if ! grep -qiE 'community\.turnsys\.com|discourse' "$f" 2>/dev/null; then
|
||||||
|
printf ' %-60s %s lines\n' "$f" "$lines"
|
||||||
|
OVERSIZED=$((OVERSIZED + 1))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done < <(find . -path ./.git -prune -o -name '*.md' -print0 2>/dev/null)
|
||||||
|
[ "$OVERSIZED" -eq 0 ] && echo " (none)"
|
||||||
|
|
||||||
|
# --- 3. summary -------------------------------------------------------------
|
||||||
|
log_step "Gardening summary"
|
||||||
|
echo " Oversized non-Discourse .md files: $OVERSIZED"
|
||||||
|
if [ "$OVERSIZED" -eq 0 ]; then
|
||||||
|
log_ok "no migration candidates"
|
||||||
|
else
|
||||||
|
log_warn "$OVERSIZED file(s) to migrate to Discourse"
|
||||||
|
fi
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# lib/common.sh — shared helpers for shell scripts and hooks in this repo.
|
||||||
|
#
|
||||||
|
# Source it from any script:
|
||||||
|
# #!/usr/bin/env bash
|
||||||
|
# set -euo pipefail
|
||||||
|
# HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# # shellcheck source=lib/common.sh
|
||||||
|
# source "$HERE/lib/common.sh" # or the appropriate relative path
|
||||||
|
#
|
||||||
|
# This library exists to drive a known cross-project inconsistency to zero:
|
||||||
|
# every repo used to re-paste the ANSI color block, redefine log_* helpers,
|
||||||
|
# pick one of three incompatible shebangs, and roll its own docker wrapper.
|
||||||
|
# Import this once instead.
|
||||||
|
|
||||||
|
# Do NOT set -euo pipefail here unconditionally — some callers (git hooks)
|
||||||
|
# source this file and rely on controlling their own shell options. We only
|
||||||
|
# guarantee the functions below are defined.
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Config — override via environment before sourcing if needed
|
||||||
|
###############################################################################
|
||||||
|
: "${TEMPLATE_ROOT:=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
||||||
|
export TEMPLATE_ROOT
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# ANSI colors (defined once, used everywhere)
|
||||||
|
###############################################################################
|
||||||
|
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
|
||||||
|
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'; BOLD='\033[1m'; NC='\033[0m'
|
||||||
|
else
|
||||||
|
RED=''; GREEN=''; YELLOW=''; BLUE=''; BOLD=''; NC=''
|
||||||
|
fi
|
||||||
|
export RED GREEN YELLOW BLUE BOLD NC
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Logging
|
||||||
|
###############################################################################
|
||||||
|
log_info() { printf "${BLUE}›${NC} %s\n" "$*"; }
|
||||||
|
log_ok() { printf "${GREEN}✓${NC} %s\n" "$*"; }
|
||||||
|
log_warn() { printf "${YELLOW}⚠${NC} %s\n" "$*" >&2; }
|
||||||
|
log_error() { printf "${RED}✗${NC} %s\n" "$*" >&2; }
|
||||||
|
log_step() { printf "\n${BOLD}== %s ==${NC}\n" "$*"; }
|
||||||
|
|
||||||
|
die() { log_error "$*"; exit 1; }
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Predicates
|
||||||
|
###############################################################################
|
||||||
|
# have <cmd> — return 0 if <cmd> is on PATH
|
||||||
|
have() { command -v "$1" >/dev/null 2>&1; }
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Path helpers
|
||||||
|
###############################################################################
|
||||||
|
repo_root() {
|
||||||
|
# Prefer git's notion of the repo root, fall back to $TEMPLATE_ROOT, then pwd.
|
||||||
|
if git rev-parse --show-toplevel >/dev/null 2>&1; then
|
||||||
|
git rev-parse --show-toplevel
|
||||||
|
else
|
||||||
|
printf '%s\n' "${TEMPLATE_ROOT:-$(pwd)}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Privilege helpers
|
||||||
|
###############################################################################
|
||||||
|
# as_root — run the remaining args as root via sudo, or directly if already root.
|
||||||
|
as_root() {
|
||||||
|
if [ "$(id -u)" -eq 0 ]; then "$@"; else sudo "$@"; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Docker wrapper
|
||||||
|
###############################################################################
|
||||||
|
# docker_run <image> <args...>
|
||||||
|
# Ephemeral container, host-uid ownership, repo mounted at /data, cwd /data.
|
||||||
|
# Drives the "host stays clean; everything runs in containers" policy and
|
||||||
|
# ensures output files are owned by the invoking user, not root.
|
||||||
|
docker_run() {
|
||||||
|
[ "$#" -ge 1 ] || die "docker_run: image required"
|
||||||
|
local image="$1"; shift
|
||||||
|
have docker || die "docker not found on PATH"
|
||||||
|
local root
|
||||||
|
root="$(repo_root)"
|
||||||
|
docker run --rm \
|
||||||
|
--user "$(id -u):$(id -g)" \
|
||||||
|
-e HOME=/tmp \
|
||||||
|
-v "$root:/data" \
|
||||||
|
-w /data \
|
||||||
|
"$image" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Rule-audit accumulator (used by scripts/check-rules.sh)
|
||||||
|
# Globals read/written: RULE_PASS RULE_WARN RULE_FAIL
|
||||||
|
###############################################################################
|
||||||
|
init_counters() { RULE_PASS=0; RULE_WARN=0; RULE_FAIL=0; }
|
||||||
|
|
||||||
|
# check <description> <pass|warn|fail>
|
||||||
|
check() {
|
||||||
|
local desc="$1" result="$2"
|
||||||
|
case "$result" in
|
||||||
|
pass)
|
||||||
|
RULE_PASS=$((RULE_PASS + 1))
|
||||||
|
if [ "${RULE_VERBOSE:-true}" = true ]; then printf " ${GREEN}PASS${NC} %s\n" "$desc"; fi
|
||||||
|
;;
|
||||||
|
warn)
|
||||||
|
RULE_WARN=$((RULE_WARN + 1))
|
||||||
|
if [ "${RULE_VERBOSE:-true}" = true ]; then printf " ${YELLOW}WARN${NC} %s\n" "$desc"; fi
|
||||||
|
;;
|
||||||
|
fail)
|
||||||
|
RULE_FAIL=$((RULE_FAIL + 1))
|
||||||
|
printf " ${RED}FAIL${NC} %s\n" "$desc"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
die "check(): invalid result '$result' (use pass|warn|fail)"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# print_summary_and_exit
|
||||||
|
print_summary_and_exit() {
|
||||||
|
if [ "${RULE_VERBOSE:-true}" = true ]; then
|
||||||
|
printf "\n=== Summary ===\n PASS: %s\n WARN: %s\n FAIL: %s\n\n" \
|
||||||
|
"$RULE_PASS" "$RULE_WARN" "$RULE_FAIL"
|
||||||
|
fi
|
||||||
|
if [ "$RULE_FAIL" -gt 0 ]; then
|
||||||
|
if [ "${RULE_VERBOSE:-true}" = true ]; then
|
||||||
|
printf "AUDIT FAILED — %s rule(s) violated.\n" "$RULE_FAIL"
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ "${RULE_VERBOSE:-true}" = true ]; then printf "AUDIT PASSED.\n"; fi
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
Executable
+25
@@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# pre-commit — fast rule audit (< 1s typical).
|
||||||
|
# Hot-path bypass: commits that ONLY touch STATUS.md / WORKING.md skip the
|
||||||
|
# audit so frequent status/task commits stay frictionless.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
|
CHANGED="$(git diff --cached --name-only)"
|
||||||
|
HOT_PATHS="$(printf '%s\n' "$CHANGED" | grep -vE '^(STATUS.md|WORKING.md)$' || true)"
|
||||||
|
|
||||||
|
if [ -z "$HOT_PATHS" ]; then
|
||||||
|
echo "hot-path files only (STATUS/WORKING) — skipping rule audit"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! bash scripts/check-rules.sh --fast; then
|
||||||
|
echo ""
|
||||||
|
echo "pre-commit audit FAILED. Fix the violations above before committing."
|
||||||
|
echo "Full audit: bash scripts/check-rules.sh"
|
||||||
|
echo "Bypass: git commit --no-verify (emergencies only)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
Executable
+25
@@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# pre-push — full rule audit + clean-working-tree gate before pushing.
|
||||||
|
# Installed via: bash scripts/setup-hooks.sh
|
||||||
|
#
|
||||||
|
# Combines two proven policies observed across projects:
|
||||||
|
# - KNEL-AIMiddleware: block push if the working tree is dirty.
|
||||||
|
# - RCEO-PersonalAssistant: block push if the full test suite fails.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
|
echo "pre-push: running full rule audit..."
|
||||||
|
|
||||||
|
# Full audit (non-fast): runs the slow test suite via `make test` if present.
|
||||||
|
if ! bash scripts/check-rules.sh --quiet; then
|
||||||
|
echo ""
|
||||||
|
echo "pre-push audit FAILED. Push blocked."
|
||||||
|
echo "Re-run with output: bash scripts/check-rules.sh"
|
||||||
|
echo "Bypass: git push --no-verify (emergencies only)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "pre-push: all rules and tests passed."
|
||||||
|
exit 0
|
||||||
Executable
+44
@@ -0,0 +1,44 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# setup-hooks.sh — install this repo's git hooks.
|
||||||
|
#
|
||||||
|
# Mechanism: copy scripts/pre-commit and scripts/pre-push into .git/hooks/ and
|
||||||
|
# make them executable. This is the most portable pattern (works on any clone,
|
||||||
|
# no `git config core.hooksPath` mutation, survives config resets, idempotent).
|
||||||
|
#
|
||||||
|
# Run once after cloning: bash scripts/setup-hooks.sh
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source "$HERE/lib/common.sh"
|
||||||
|
REPO_ROOT="$(repo_root)"
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
|
[ -d .git ] || die "no .git directory here — run this from a git checkout"
|
||||||
|
|
||||||
|
HOOKS_DIR=".git/hooks"
|
||||||
|
HOOK_NAMES="pre-commit pre-push"
|
||||||
|
|
||||||
|
log_step "Installing git hooks"
|
||||||
|
for name in $HOOK_NAMES; do
|
||||||
|
src="scripts/$name"
|
||||||
|
dst="$HOOKS_DIR/$name"
|
||||||
|
[ -f "$src" ] || die "source hook not found: $src"
|
||||||
|
cp "$src" "$dst"
|
||||||
|
chmod +x "$dst"
|
||||||
|
log_ok "installed $dst"
|
||||||
|
done
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
Git hooks installed. The following now run automatically:
|
||||||
|
|
||||||
|
pre-commit fast rule audit (shellcheck, image pinning, container naming,
|
||||||
|
required files, doc freshness, Discourse pointers, WORKING.md
|
||||||
|
completion, hygiene).
|
||||||
|
Hot-path bypass for STATUS.md / WORKING.md.
|
||||||
|
pre-push full rule audit (includes scripts/test.sh) + clean-working-tree gate.
|
||||||
|
|
||||||
|
Bypass either with \`git commit --no-verify\` / \`git push --no-verify\`
|
||||||
|
(emergencies only).
|
||||||
|
EOF
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
#!/usr/bin/bash
|
||||||
|
#
|
||||||
|
# tests/shellcheck.sh — enforce shellcheck (via Docker) on all repo shell scripts
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# bash tests/shellcheck.sh # lint every .sh in the repo
|
||||||
|
# bash tests/shellcheck.sh path/a.sh path/b.sh # lint specific files
|
||||||
|
#
|
||||||
|
# Uses the koalaman/shellcheck:stable image (no native binary required).
|
||||||
|
# Exits non-zero if ANY script emits a diagnostic. Skips non-bash scripts
|
||||||
|
# (e.g. PHP files with a .sh extension) and the vendored/ trees.
|
||||||
|
#
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
IMAGE="koalaman/shellcheck:stable"
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
|
||||||
|
# --- Gather target files -----------------------------------------------------
|
||||||
|
mapfile -t FILES < <(
|
||||||
|
if [ "$#" -gt 0 ]; then
|
||||||
|
# Explicit args: resolve to repo-root-relative paths, keep only .sh
|
||||||
|
for f in "$@"; do
|
||||||
|
f_abs="$(cd "$(dirname "$f")" && pwd)/$(basename "$f")"
|
||||||
|
case "$f_abs" in
|
||||||
|
"$ROOT"/vendor/*) ;; # skip vendored
|
||||||
|
"$ROOT"/archive/provisioning/Agents/librenms/*) ;; # upstream librenms-agent scripts
|
||||||
|
"$ROOT"/oam/librenms-agent/agent/*) ;; # upstream librenms-agent scripts (verbatim port [#474])
|
||||||
|
"$ROOT"/archive/KNELServerBuild/*) ;; # archived legacy code, not maintained [#474]
|
||||||
|
*) case "$f_abs" in *.sh) echo "$f_abs";; esac ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
else
|
||||||
|
# Default: every .sh under the repo, minus vendor/
|
||||||
|
while IFS= read -r f_abs; do
|
||||||
|
case "$f_abs" in
|
||||||
|
"$ROOT"/vendor/*) ;; # skip vendored
|
||||||
|
"$ROOT"/archive/provisioning/Agents/librenms/*) ;; # upstream librenms-agent scripts
|
||||||
|
"$ROOT"/oam/librenms-agent/agent/*) ;; # upstream librenms-agent scripts (verbatim port [#474])
|
||||||
|
"$ROOT"/archive/KNELServerBuild/*) ;; # archived legacy code, not maintained [#474]
|
||||||
|
*) echo "$f_abs" ;;
|
||||||
|
esac
|
||||||
|
done < <(find "$ROOT" -type f -name '*.sh' \
|
||||||
|
-not -path '*/.git/*' -not -path "$ROOT/vendor/*" \
|
||||||
|
-not -path "$ROOT/.crush/*")
|
||||||
|
fi
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ "${#FILES[@]}" -eq 0 ]; then
|
||||||
|
echo "shellcheck.sh: no .sh files to check." >&2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Filter out non-bash scripts (e.g. PHP with a .sh extension) --------------
|
||||||
|
BASH_FILES=()
|
||||||
|
for f in "${FILES[@]}"; do
|
||||||
|
shebang=$(head -c 64 "$f" 2>/dev/null | head -n1)
|
||||||
|
case "$shebang" in
|
||||||
|
\#!/usr/bin/php*|\#!/usr/bin/env\ php*) ;; # PHP, skip
|
||||||
|
\#!*) BASH_FILES+=("$f") ;; # any other shebang → check
|
||||||
|
*) BASH_FILES+=("$f") ;; # no shebang → check anyway
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "${#BASH_FILES[@]}" -eq 0 ]; then
|
||||||
|
echo "shellcheck.sh: no bash scripts among targets." >&2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Run shellcheck in Docker (mount repo root, pass root-relative paths) -----
|
||||||
|
# shellcheck disable=SC2012 # basename loop is intentional
|
||||||
|
REL=()
|
||||||
|
for f in "${BASH_FILES[@]}"; do REL+=("${f#"$ROOT"/}"); done
|
||||||
|
|
||||||
|
# Disable checks that are intentional conventions of this codebase (not bugs):
|
||||||
|
# SC1090/SC1091 — cannot follow dynamically-computed `source` paths (KNEL framework)
|
||||||
|
# SC2029 — ssh orchestration deliberately expands the command client-side
|
||||||
|
DISABLES=(
|
||||||
|
-e SC1090
|
||||||
|
-e SC1091
|
||||||
|
-e SC2029
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "Checking ${#BASH_FILES[@]} script(s) with $IMAGE:"
|
||||||
|
printf ' %s\n' "${REL[@]}"
|
||||||
|
|
||||||
|
docker run --rm \
|
||||||
|
-v "$ROOT:/mnt:ro" \
|
||||||
|
-w /mnt \
|
||||||
|
"$IMAGE" -x "${DISABLES[@]}" "${REL[@]}"
|
||||||
|
rc=$?
|
||||||
|
|
||||||
|
if [ "$rc" -eq 0 ]; then
|
||||||
|
echo "shellcheck: PASS (${#BASH_FILES[@]} scripts clean)"
|
||||||
|
else
|
||||||
|
echo "shellcheck: FAIL (fix the findings above or add targeted disable directives)" >&2
|
||||||
|
fi
|
||||||
|
exit "$rc"
|
||||||
Reference in New Issue
Block a user