AGENTS.md and PLAN.md already told every phase to `bash .agents/validate.sh`, but the script itself was never tracked, so a fresh clone could not run the gate at all. Four checks: `uv run pytest --cov --cov-fail-under=94`, `uv run ruff check src tests agent`, `uv run pyright`, and a zero-dependency `node --check` of every served JS. Plus repo hygiene (tracked files matching .gitignore, log drops/key material, non-placeholder secrets in the tracked env templates) and an advisory `uv pip list --outdated`. Two rules are baked in because breaking them made a false green: run the project toolchain through `uv run` (a bare `python3`/`ruff` on PATH tests an interpreter nobody deploys and an older linter than the pyproject floor), and treat a missing tool as a failure rather than an `if command -v` skip. LLM-Generated: true
164 lines
8.0 KiB
Bash
Executable File
164 lines
8.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# .agents/validate.sh — validation gate for the phased-execution pipeline.
|
|
#
|
|
# A unit is only moved to .agents/phases/complete/ if this script exits 0,
|
|
# and a phase is only COMMITTED if its final pass exits 0 — so this script
|
|
# is the last line of defense for "no phase commits unless 100% passing".
|
|
# It MUST run the project's FULL test suite (every test, not a subset) plus
|
|
# its linter, so a failure anywhere in the project blocks the commit.
|
|
#
|
|
# Adapted to THIS project's real gate (AGENTS.md "Definition of done", PLAN.md
|
|
# "Validation / Verification Workflow"):
|
|
#
|
|
# uv run pytest --cov hermetic full suite, coverage floor 94%
|
|
# uv run ruff check src tests agent
|
|
# uv run pyright
|
|
#
|
|
# Two rules this script had to learn the hard way:
|
|
#
|
|
# * Run the PROJECT toolchain, not whatever is on PATH. `python3 -m pytest` here
|
|
# resolves to /usr/bin/python3 + ~/.local site-packages (tests/conftest.py puts
|
|
# src/ on sys.path, so it passes while testing an interpreter nobody deploys),
|
|
# and a bare `ruff` on PATH is older than the `ruff>=0.17.1` floor in
|
|
# pyproject.toml. `uv run` is the only invocation that matches the docs.
|
|
# * Never skip a check silently. `if command -v ruff; then ruff check .; fi`
|
|
# exits 0 when the linter is missing — a gate that can pass without linting is
|
|
# not a gate. A missing tool is a FAILURE here; the checks are listed by name.
|
|
set -uo pipefail
|
|
rc=0
|
|
|
|
# Run from the repo root no matter where the harness invoked us, so `src`,
|
|
# `tests`, `agent` and the pytest/ruff/pyright configs all mean the same tree.
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
cd "$ROOT" || { echo "validation FAILED: cannot cd to $ROOT"; exit 1; }
|
|
|
|
# --- the Python gate ----------------------------------------------------------
|
|
# uv is the documented entry point (`uv sync --python 3.14`); if it is gone the
|
|
# venv is still usable directly, and only a total absence of both is fatal.
|
|
if command -v uv >/dev/null 2>&1; then
|
|
RUN=(uv run)
|
|
elif [[ -x .venv/bin/pytest && -x .venv/bin/ruff && -x .venv/bin/pyright ]]; then
|
|
RUN=(.venv/bin/)
|
|
echo "note: uv not on PATH — using .venv/bin directly" >&2
|
|
else
|
|
echo "validation FAILED: no uv and no complete .venv (pytest+ruff+pyright)." >&2
|
|
echo "Run 'uv sync --python 3.14'. Refusing to guess a system interpreter:" >&2
|
|
echo "that path skips the pinned toolchain and can pass on the wrong stack." >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "== uv run pytest --cov (hermetic: no network, live tests skip) =="
|
|
# --cov-fail-under is the floor from PLAN.md (overall must not drop below 94%);
|
|
# the project floor in AGENTS.md is 80%, and we are at 95%.
|
|
"${RUN[@]}" pytest --cov --cov-fail-under=94 || rc=1
|
|
|
|
echo "== uv run ruff check src tests agent =="
|
|
"${RUN[@]}" ruff check src tests agent || rc=1
|
|
|
|
echo "== uv run pyright =="
|
|
# pyright's `include` in pyproject.toml is src+tests on purpose: agent/host_agent.py
|
|
# is stdlib-only and must stay runnable on the fleet's Python 3.10, so it is linted
|
|
# (ruff, above) but not type-checked against 3.14 stubs.
|
|
"${RUN[@]}" pyright || rc=1
|
|
|
|
# --- static assets: syntax only, no toolchain added ---------------------------
|
|
# There is no JS build step or JS linter (a locked anchor), so a broken page is
|
|
# only caught in a browser. `node --check` is a zero-dependency syntax gate that
|
|
# catches the class of error a stray brace makes; a missing node is a visible
|
|
# skip, never a silent pass of a check that ran nothing.
|
|
if command -v node >/dev/null 2>&1; then
|
|
echo "== node --check src/dashboards/web/static/*.js =="
|
|
for js in src/dashboards/web/static/*.js; do
|
|
node --check "$js" || rc=1
|
|
done
|
|
else
|
|
echo "note: node not on PATH — skipping the JS syntax check (browser verification only)" >&2
|
|
fi
|
|
|
|
# --- dependency currency (advisory, not a gate) -------------------------------
|
|
# AGENTS.md rule 8 checks this after touching dependencies; making it a hard gate
|
|
# would block an unrelated phase on an upstream release that landed five minutes
|
|
# ago, so it is loud in the log instead of silent.
|
|
if command -v uv >/dev/null 2>&1 && [[ -f uv.lock ]]; then
|
|
outdated="$(uv pip list --outdated 2>/dev/null | grep -vE '^(Package|---|$)' || true)"
|
|
if [[ -n "$outdated" ]]; then
|
|
echo "advisory: dependencies are out of date (AGENTS.md rule 8 — update on purpose):" >&2
|
|
printf ' %s\n' "$outdated" >&2
|
|
fi
|
|
fi
|
|
|
|
# --- repo hygiene (language-agnostic; runs in every project) ------------------
|
|
# Scoped staging commits everything the phase touched, so a file that is not
|
|
# ignored gets committed — including raw log drops and key material. These
|
|
# gates make the phase fail instead. Fix by untracking, keeping the working
|
|
# copy: git rm --cached <file>
|
|
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
# 1. A tracked file that matches a .gitignore rule should never be tracked.
|
|
junk="$(git ls-files -i -c --exclude-standard 2>/dev/null || true)"
|
|
if [[ -n "$junk" ]]; then
|
|
echo "repo hygiene: TRACKED files match .gitignore rules (untrack with 'git rm --cached <file>'):" >&2
|
|
printf ' %s\n' "$junk" >&2
|
|
rc=1
|
|
fi
|
|
|
|
# 2. Personal-data / key-material file SHAPES that must never be tracked,
|
|
# even in a project with no .gitignore rule for them.
|
|
DANGEROUS='(^|/)[^/]*\.log$|(^|/)[^/]*\.jsonl$|(^|/)\.env($|\.)|\.(pem|key|crt|cer|p12|pfx|keystore|jks)$|(^|/)(id_rsa|id_ecdsa|id_ed25519)(\.pub)?$|(^|/)(credentials|service-account|client_secret)([-a-z0-9]*)\.json$'
|
|
# Project allow-list (this repo's deliberate, documented choice — see .gitignore):
|
|
# the env *templates* `.env.example` / `.env.dev.example` are tracked on purpose,
|
|
# because `cp .env.example .env` is the setup documented in README.md and every
|
|
# real `.env` / `.env.dev` stays git-ignored. A template is documentation, not key
|
|
# material — and 2b below keeps that claim honest, so this allow-list cannot smuggle
|
|
# a live credential past the gate. Nothing else is allow-listed: logs, key material,
|
|
# and real dot-env files still fail here.
|
|
SAFE_TEMPLATES='(^|/)\.env[^/]*\.(example|template|sample)$'
|
|
bad="$(git ls-files | grep -E "$DANGEROUS" | grep -vE "$SAFE_TEMPLATES" || true)"
|
|
if [[ -n "$bad" ]]; then
|
|
echo "repo hygiene: TRACKED files look like log drops / key material (untrack with 'git rm --cached <file>' and add a .gitignore rule):" >&2
|
|
printf ' %s\n' "$bad" >&2
|
|
rc=1
|
|
fi
|
|
|
|
# 2b. A tracked env template may only carry placeholders: every
|
|
# *_PASSWORD|*_TOKEN|*_SECRET|*_KEY assignment must be empty or a placeholder
|
|
# word (AGENTS.md rule 6 — "never leak a credential", not even into a sample).
|
|
PLACEHOLDER='^(|changeme|change-me|replace-me[-a-z0-9]*|secret|placeholder|dummy|example|example\.net|x+)$'
|
|
tmpl_hits=""
|
|
while IFS= read -r f; do
|
|
[[ -z "$f" ]] && continue
|
|
while IFS= read -r line; do
|
|
val="${line#*=}"
|
|
val="${val%%#*}"
|
|
val=${val//[[:space:]]/}
|
|
val=${val//\"/}
|
|
val=${val//\'/}
|
|
[[ "${val,,}" =~ $PLACEHOLDER ]] && continue
|
|
tmpl_hits+=" $f: $line"$'\n'
|
|
done < <(grep -inE '^[[:space:]]*(export[[:space:]]+)?[A-Za-z0-9_]*(PASSWORD|TOKEN|SECRET|KEY)[A-Za-z0-9_]*=' "$f" 2>/dev/null || true)
|
|
done < <(git ls-files | grep -E "$SAFE_TEMPLATES" || true)
|
|
if [[ -n "$tmpl_hits" ]]; then
|
|
echo "repo hygiene: tracked env TEMPLATE carries a non-placeholder secret (move the real value to the git-ignored .env):" >&2
|
|
printf '%s' "$tmpl_hits" >&2
|
|
rc=1
|
|
fi
|
|
|
|
# 3. Project-specific content scan for personal data inside tracked files
|
|
# (client IPs, account ids, ...). Empty by default — fill it in per
|
|
# project, and allow-list documentation ranges (RFC 5737 / 4193 / 3849,
|
|
# 127.0.0.0/8, RFC1918) so fixtures do not trip it. Format: one ERE.
|
|
PII_PATTERNS="${PII_PATTERNS:-}"
|
|
if [[ -n "$PII_PATTERNS" ]]; then
|
|
hits="$(git ls-files -z | xargs -0 grep -IHnE "$PII_PATTERNS" 2>/dev/null || true)"
|
|
if [[ -n "$hits" ]]; then
|
|
echo "personal-data gate: TRACKED files match PII_PATTERNS — redact them:" >&2
|
|
printf ' %s\n' "$hits" >&2
|
|
rc=1
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
if [[ $rc -ne 0 ]]; then
|
|
echo "validation FAILED (see output above)"
|
|
fi
|
|
exit "$rc"
|