repo contract: CLAUDE.md commands, test_changed, CI gate; Makefile cover hook, ignore coverage output

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Krzysztof Rudnicki 2026-10-04 16:37:41 +02:00
parent 7739d33532
commit dadc2b3e6f
8 changed files with 147 additions and 0 deletions

25
.github/workflows/repo-contract.yml vendored Normal file
View File

@ -0,0 +1,25 @@
name: repo-contract
# The local pre-commit hook can be skipped (a fresh clone has no hooks), so the
# contract is enforced on push too. Mirrors md-naming.yml.
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
jobs:
repo-contract:
name: repo contract
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check out the shared gate
uses: actions/checkout@v4
with:
repository: kuhyx/utils
path: .utils
- name: Enforce the repo contract
run: bash .utils/scripts/check_repo_contract.sh

4
.gitignore vendored
View File

@ -489,3 +489,7 @@ gates/runnability-ci.tsv
# Per-course virtualenvs created by ./run.sh run.
.venv/
# test coverage output
.coverage
coverage.xml

View File

@ -67,6 +67,12 @@ repos:
- repo: local
hooks:
- id: repo-contract
name: repo contract (CLAUDE.md Commands + test_changed)
entry: scripts/check_repo_contract.sh
language: system
pass_filenames: false
files: ^(CLAUDE\.md|AGENTS\.md|scripts/test_changed\.sh)$
# Coursework deliverables are allowed under the two course trees;
# compiled output is allowed nowhere. See .binary-allowlist.
- id: no-binaries

View File

@ -155,3 +155,12 @@ applies.
| `.binary-allowlist` | Path-scoped: deliverables allowed under the two course trees, compiled output allowed nowhere. |
| `DOCS-runnability.md` | Per-course run status, recorded separately for `local` and `ci`. |
| `TODO-*.md` | Open decisions. Each carries the removal marker and is deleted when the work lands. |
## Commands
- run: n/a: coursework archive; per course use `./run.sh run <dir>`
- test: `scripts/test_suites.sh`
- test-changed: `scripts/test_changed.sh`
- lint: `ruff check .`
- coverage: `COVERAGE=1 scripts/test_suites.sh`
- coverage-gaps: `coverage-gaps coverage.xml`

5
Makefile Normal file
View File

@ -0,0 +1,5 @@
# finish gate hook: the root-level pytest cannot collect the ECRYPT sieve suite (relative CSVs), so
# the shared gate calls this target instead (see scripts/test_suites.sh).
.PHONY: cover
cover:
COVERAGE=1 scripts/test_suites.sh

26
scripts/check_repo_contract.sh Executable file
View File

@ -0,0 +1,26 @@
#!/bin/bash
# ============================================================================
# Fail if this repo breaks the shared repo contract.
#
# Thin delegate to the shared gate in ~/src/utils, which owns the contract
# text. Copy this file to scripts/check_repo_contract.sh in a repo; never copy
# the shared gate itself, or the shim would exec itself forever.
# ============================================================================
set -euo pipefail
readonly SHARED_GATE="${UTILS_ROOT:-$HOME/src/utils}/scripts/check_repo_contract.sh"
main() {
if [[ ! -x "$SHARED_GATE" ]]; then
echo "Error: shared repo-contract gate not found at $SHARED_GATE" >&2
echo " Clone github.com/kuhyx/utils to ~/src/utils, or set" >&2
echo " UTILS_ROOT to where it lives." >&2
exit 1
fi
exec bash "$SHARED_GATE" "$@"
}
main "$@"

32
scripts/test_changed.sh Executable file
View File

@ -0,0 +1,32 @@
#!/bin/bash
# ============================================================================
# Run only the pytest suites whose directory has changes vs HEAD (staged,
# unstaged and untracked). Quiet: failures plus a one-line summary per suite.
# Changes to shared config (pyproject.toml, this repo's test scripts) run all
# suites; changes outside any suite run nothing.
# ============================================================================
set -euo pipefail
ROOT="$(git rev-parse --show-toplevel)"
readonly ROOT
readonly SUITES=(Programming/ECOTE Programming/ECRYPT_PROJECT/sieve)
changed="$(cd "$ROOT" && { git diff --name-only HEAD 2>/dev/null || true; git ls-files --others --exclude-standard; } | sort -u)"
selected=()
all=0
for s in "${SUITES[@]}"; do
grep -q "^${s}/" <<<"$changed" && selected+=("$s")
done
grep -Eq '^(pyproject\.toml|scripts/test_suites\.sh|scripts/test_changed\.sh)$' <<<"$changed" && all=1
if [[ $all -eq 1 ]]; then
echo "shared test config changed: running all suites"
exec "$ROOT/scripts/test_suites.sh"
fi
if [[ ${#selected[@]} -eq 0 ]]; then
echo "no changes in a pytest suite: nothing to test"
exit 0
fi
SUITE_DIRS="${selected[*]}" exec "$ROOT/scripts/test_suites.sh"

40
scripts/test_suites.sh Executable file
View File

@ -0,0 +1,40 @@
#!/bin/bash
# ============================================================================
# Run the pytest suites that exist in this archive, each from its own directory
# (ECRYPT sieve reads its reference CSVs by relative path, so a repo-root
# `pytest` cannot collect it). Extra args are passed to every pytest run.
# Usage: scripts/test_suites.sh [pytest args]
# COVERAGE=1 scripts/test_suites.sh # writes coverage.xml at repo root
# ============================================================================
set -euo pipefail
ROOT="$(git rev-parse --show-toplevel)"
readonly ROOT
readonly SUITES=(Programming/ECOTE Programming/ECRYPT_PROJECT/sieve)
run_suites() {
local -a suites=("$@") extra=()
[[ ${#suites[@]} -eq 0 ]] && suites=("${SUITES[@]}")
if [[ "${COVERAGE:-0}" == 1 ]]; then
export COVERAGE_FILE="$ROOT/.coverage"
rm -f "$COVERAGE_FILE" "$ROOT/coverage.xml"
fi
local rc=0 s
for s in "${suites[@]}"; do
if [[ "${COVERAGE:-0}" == 1 ]]; then
extra=(--cov="$ROOT/$s" --cov-branch --cov-append --cov-config="$ROOT/pyproject.toml")
fi
(cd "$ROOT/$s" && python3 -m pytest -q --tb=short "${extra[@]}" "${PYTEST_ARGS[@]}") || rc=$?
done
if [[ "${COVERAGE:-0}" == 1 ]]; then
(cd "$ROOT" && python3 -m coverage xml -o coverage.xml --rcfile=pyproject.toml >/dev/null)
fi
return "$rc"
}
PYTEST_ARGS=("$@")
[[ -n "${SUITE_DIRS:-}" ]] && read -r -a SELECTED <<<"$SUITE_DIRS" || SELECTED=()
run_suites "${SELECTED[@]}"
exit $?