Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions .github/workflows/package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,6 @@ jobs:
exit 1
fi

- name: Install validation dependencies
run: python -m pip install ".[dev,typer,quality]"

- name: Run authoritative validation gate
run: ./tests/full_validate.sh

- name: Validate changelog
run: python scripts/validate_changelog.py

Expand Down
64 changes: 25 additions & 39 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ name: Tests

on:
push:
branches:
- main
tags:
- "v*"
pull_request:

permissions:
Expand Down Expand Up @@ -35,10 +39,10 @@ jobs:
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ matrix.python-version }}
- name: Install full validation dependencies
run: python -m pip install ".[dev,typer,quality]"
- name: Run authoritative validation gate
run: ./tests/full_validate.sh
- name: Install runtime test dependencies
run: python -m pip install ".[dev,typer]"
- name: Run platform and Python runtime suite
run: ./tests/full_validate.sh --gate runtime

quality:
name: Quality and security gates
Expand All @@ -55,37 +59,20 @@ jobs:
python-version: "3.13"
- name: Install quality dependencies
run: python -m pip install ".[dev,typer,quality]"
- name: Run formatting and lint checks
run: |
ruff format --check lib/python/base_cli scripts examples tests
ruff check lib/python/base_cli scripts examples tests
- name: Run strict typing and documentation checks
run: |
python -m mypy --strict examples/typed_consumer.py
python -m mypy --strict lib/python/base_cli
python scripts/validate_docs.py
python scripts/validate_changelog.py
python scripts/validate_schemas.py
python scripts/validate_contract_fixtures.py
node scripts/validate_contract_fixtures.mjs
python scripts/generate_compatibility_dashboard.py --check
python scripts/benchmark_runtime.py --check
python -m compileall -q examples
- name: Run tests with coverage threshold
run: python -m pytest --cov=base_cli --cov-report=term-missing --cov-report=json:coverage.json --cov-fail-under=80
- name: Enforce high-risk module coverage floors
run: python scripts/validate_coverage.py coverage.json
- name: Run static security checks
run: |
bandit -q -r lib/python/base_cli scripts -lll -iii
# The project itself is installed from this checkout and may not be
# published to PyPI yet (for example, while validating a release PR).
# Audit every installed third-party package without asking pip-audit
# to resolve the unpublished project distribution.
python -m pip freeze \
| grep -Eiv '^base-cli([[:space:]]|$)' \
> "$RUNNER_TEMP/base-cli-audit-requirements.txt"
pip-audit --strict -r "$RUNNER_TEMP/base-cli-audit-requirements.txt"
- name: Validate repository baseline
run: ./tests/full_validate.sh --gate baseline
- name: Run tests and enforce coverage budgets
run: ./tests/full_validate.sh --gate coverage
- name: Enforce strict typing
run: ./tests/full_validate.sh --gate typing
- name: Enforce formatting and lint
run: ./tests/full_validate.sh --gate style
- name: Validate docs, schemas, contracts, and generated files
run: ./tests/full_validate.sh --gate contracts
- name: Enforce benchmark budget
run: ./tests/full_validate.sh --gate benchmark
- name: Run dependency and static security checks
run: ./tests/full_validate.sh --gate security

linux-distributions:
name: Validate (${{ matrix.name }})
Expand Down Expand Up @@ -127,8 +114,7 @@ jobs:
python3 -m venv /tmp/base-cli-venv
/tmp/base-cli-venv/bin/python -m pip install ".[dev,typer]"
. /tmp/base-cli-venv/bin/activate
python -m pip install ".[quality]"
./tests/full_validate.sh
./tests/full_validate.sh --gate runtime
'

wsl:
Expand All @@ -140,7 +126,7 @@ jobs:
with:
fetch-depth: 0
fetch-tags: true
- name: Validate repository baseline inside WSL
- name: Run platform and Python runtime suite inside WSL
shell: pwsh
run: |
$distros = (wsl --list --quiet 2>$null | Out-String)
Expand All @@ -150,4 +136,4 @@ jobs:
$drive = $env:GITHUB_WORKSPACE.Substring(0, 1).ToLowerInvariant()
$path = $env:GITHUB_WORKSPACE.Substring(2).Replace('\', '/')
$linuxWorkspace = "/mnt/$drive$path"
wsl --distribution Ubuntu --user root -- bash -lc "set -eu; cd '$linuxWorkspace'; sed -i 's/\r$//' tests/full_validate.sh tests/validate.sh; apt-get update -qq; apt-get install -y -qq nodejs npm python3-venv python3.14-venv; python3 -m venv /tmp/base-cli-venv; . /tmp/base-cli-venv/bin/activate; python -m pip install '.[dev,typer,quality]'; export BASE_CLI_BENCHMARK_PLATFORM=wsl; bash tests/full_validate.sh"
wsl --distribution Ubuntu --user root -- bash -lc "set -eu; cd '$linuxWorkspace'; sed -i 's/\r$//' tests/full_validate.sh tests/validate.sh; apt-get update -qq; apt-get install -y -qq nodejs npm python3-venv python3.14-venv; python3 -m venv /tmp/base-cli-venv; . /tmp/base-cli-venv/bin/activate; python -m pip install '.[dev,typer]'; bash tests/full_validate.sh --gate runtime"
38 changes: 31 additions & 7 deletions docs/testing.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
# Validation commands

The Base manifest declares `./tests/full_validate.sh` as the authoritative
test command. It runs the repository baseline checks, Python tests with the
coverage policy, strict typing, formatting and lint checks, schema and
contract validation, documentation checks, compatibility-dashboard and
performance checks, Bandit, and a strict `pip-audit` of the resolved
third-party environment. Bandit and pip-audit are required; a missing tool is
an error rather than a skipped check.
local aggregate. It composes the same named gates used by CI: repository
baseline, Python tests with coverage, strict typing, formatting and lint,
documentation/schema/contract validation, benchmark budgets, and security.
Bandit and pip-audit are required; a missing tool is an error rather than a
skipped check.

Run it from a clean checkout after installing the development and quality
extras:
Expand All @@ -18,7 +17,32 @@ python -m pip install '.[dev,typer,quality]'

`./tests/validate.sh` remains the fast repository-baseline check used when
dependencies are not yet installed. It is not a substitute for the full
validation gate. The full gate writes a machine-readable result to
validation gate. Individual gates can be selected for focused local work:

```bash
./tests/full_validate.sh --gate runtime
./tests/full_validate.sh --gate coverage
./tests/full_validate.sh --gate typing
./tests/full_validate.sh --gate style
./tests/full_validate.sh --gate contracts
./tests/full_validate.sh --gate benchmark
./tests/full_validate.sh --gate security
```

The Tests workflow runs the runtime suite across the OS/Python matrix and on
the supported Linux distributions/WSL. Its single quality job runs the
platform-independent coverage, typing, style, contract, benchmark, and
security gates once, with each group visible as a named Actions step. The
workflow validates feature branches through pull requests rather than
launching a second full run on every feature-branch push; direct pushes to
`main` and version tags remain validated. The
Package workflow focuses on release-boundary checks: building and validating
the wheel/sdist, checksums/SBOM, and clean installed-wheel smoke tests. It
does not repeat the source test, typing, lint, documentation, benchmark, or
security suites. `./tests/full_validate.sh` remains the one-command local
aggregate of all source gates.

The full gate writes a machine-readable result to
`$BASE_CLI_VALIDATION_RESULT` (or `/tmp/base-cli-validation-result.json`). If
Node.js is unavailable, the result is marked `partial`, the gate exits with
status `2`, and it cannot be reported as an authoritative pass.
168 changes: 121 additions & 47 deletions tests/full_validate.sh
Original file line number Diff line number Diff line change
@@ -1,55 +1,129 @@
#!/usr/bin/env bash

# Authoritative local validation entry point for the Base manifest.
# Composable validation gates. The default command remains the authoritative
# local aggregate; CI selects only the gate groups appropriate to each job.
set -euo pipefail

required_commands=(python ruff mypy bandit pip-audit)
for command in "${required_commands[@]}"; do
command -v "$command" >/dev/null 2>&1 || {
printf 'Missing validation tool: %s. Install the dev and quality extras first.\n' "$command" >&2
exit 1
}
done

./tests/validate.sh
python -m pytest --cov=base_cli --cov-report=term-missing --cov-report=json:coverage.json --cov-fail-under=80
python -m mypy --strict examples/typed_consumer.py
python -m mypy --strict lib/python/base_cli
ruff format --check lib/python/base_cli scripts examples tests
ruff check lib/python/base_cli scripts examples tests
python scripts/validate_docs.py
python scripts/validate_changelog.py
python scripts/validate_schemas.py
python scripts/validate_contract_fixtures.py
if command -v node >/dev/null 2>&1; then
node scripts/validate_contract_fixtures.mjs
gate="all"
node_contracts_missing=0
if [[ $# -gt 0 ]]; then
if [[ $# -ne 2 || "$1" != "--gate" ]]; then
printf 'Usage: %s [--gate baseline|runtime|coverage|typing|style|contracts|benchmark|security]\n' "$0" >&2
exit 2
fi
gate="$2"
fi
python scripts/generate_compatibility_dashboard.py --check
python scripts/benchmark_runtime.py --check
python -m compileall -q examples
python scripts/validate_coverage.py coverage.json

bandit -q -r lib/python/base_cli scripts -lll -iii

# Audit the resolved third-party environment without asking pip-audit to
# resolve the unpublished editable checkout itself. `sed` keeps this safe
# under `set -o pipefail` even when the environment contains no other package.
audit_requirements="$(mktemp)"
trap 'rm -f "$audit_requirements"' EXIT
python -m pip freeze \
| sed -E '/(^-e .*#egg=base[_-]cli|^base[_-]cli([[:space:]=@]|$))/Id' \
> "$audit_requirements"
pip-audit --strict -r "$audit_requirements"

validation_result="${BASE_CLI_VALIDATION_RESULT:-${TMPDIR:-/tmp}/base-cli-validation-result.json}"
if command -v node >/dev/null 2>&1; then

require_commands() {
local command
for command in "$@"; do
command -v "$command" >/dev/null 2>&1 || {
printf 'Missing validation tool: %s. Install the required development extras first.\n' "$command" >&2
exit 1
}
done
}

run_baseline() {
bash ./tests/validate.sh
}

run_runtime() {
require_commands python
python -m pytest
}

run_coverage() {
require_commands python
python -m pytest --cov=base_cli --cov-report=term-missing --cov-report=json:coverage.json --cov-fail-under=80
python scripts/validate_coverage.py coverage.json
}

run_typing() {
require_commands python mypy
python -m mypy --strict examples/typed_consumer.py
python -m mypy --strict lib/python/base_cli
}

run_style() {
require_commands ruff
ruff format --check lib/python/base_cli scripts examples tests
ruff check lib/python/base_cli scripts examples tests
}

run_contracts() {
require_commands python
python scripts/validate_docs.py
python scripts/validate_changelog.py
python scripts/validate_schemas.py
python scripts/validate_contract_fixtures.py
if command -v node >/dev/null 2>&1; then
node scripts/validate_contract_fixtures.mjs
elif [[ "$gate" == "all" ]]; then
node_contracts_missing=1
printf 'Node.js is unavailable; the cross-language contract gate is incomplete.\n' >&2
else
printf 'Node.js is unavailable; the cross-language contract gate is incomplete.\n' >&2
return 2

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Behavior gap: standalone --gate contracts does less than the same gate run inside all

When $gate == "all" and Node is missing, run_contracts() sets node_contracts_missing=1 and falls through to still run generate_compatibility_dashboard.py --check and compileall (lines 69-70). But when --gate contracts is invoked standalone (e.g. from a future CI step or a developer's shell) and Node is missing, it return 2s immediately at line 67, skipping those same two checks entirely.

So the 'contracts' gate silently validates a different, smaller set of things depending on whether it's invoked alone or as part of the aggregate — a maintainer relying on ./tests/full_validate.sh --gate contracts for local iteration on a Node-less machine gets less coverage (no dashboard/compileall check) than they'd get from the full run, with no message indicating those two checks were skipped. Consider running the dashboard/compileall checks before deciding whether to return 2, so the standalone gate's coverage doesn't regress based on the node-missing branch taken.

fi
python scripts/generate_compatibility_dashboard.py --check
python -m compileall -q examples
}

run_benchmark() {
require_commands python
python scripts/benchmark_runtime.py --check
}

run_security() {
require_commands bandit pip-audit python
bandit -q -r lib/python/base_cli scripts -lll -iii

# Audit third-party packages without asking pip-audit to resolve the
# unpublished editable checkout itself.
local audit_requirements
audit_requirements="$(mktemp)"
trap 'rm -f "$audit_requirements"' RETURN

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: cleanup trap no longer fires on the failure path it exists for

run_security() replaced the old script-wide trap ... EXIT with a function-scoped trap 'rm -f "$audit_requirements"' RETURN. Under set -e (active at the top of this script), a RETURN trap only fires when the function returns normally — if a command inside the function fails and triggers errexit, the whole script exits immediately without unwinding through the function's return, so the RETURN trap never runs.

Confirmed empirically (bash 5.3):

f() { local tmp; tmp="$(mktemp)"; trap 'rm -f "$tmp"' RETURN; false; }
case sec in sec) f ;; esac   # under set -e

→ the trap never fires and the temp file is leaked; the equivalent test with trap ... EXIT at script scope does clean up.

Failure scenario: pip-audit --strict (line 90) finds a vulnerability and exits non-zero — exactly the case this gate exists to catch. The mktemp file created at line 85 is now left behind in $TMPDIR instead of being removed, unlike the pre-refactor script. Restoring an EXIT trap (or an explicit cleanup on both paths) would fix it.

python -m pip freeze \
| sed -E '/(^-e .*#egg=base[_-]cli|^base[_-]cli([[:space:]=@]|$))/Id' \
> "$audit_requirements"
pip-audit --strict -r "$audit_requirements"
}

write_validation_result() {
local validation_result
validation_result="${BASE_CLI_VALIDATION_RESULT:-${TMPDIR:-/tmp}/base-cli-validation-result.json}"
if ((node_contracts_missing)); then
printf '%s\n' '{"status":"partial","skipped":["node contract validator"]}' > "$validation_result"
printf 'Validation result: partial; Node.js contract validation was skipped (%s).\n' "$validation_result"
return 2

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed behavior: dropped user-facing guidance on the partial-result path

The pre-refactor script's node-missing branch printed three lines, the last of which was:
printf 'This result is non-authoritative; install Node.js for the full gate.\n'

write_validation_result() keeps the JSON-status line and the 'Validation result: partial…' line but drops this one. A developer running ./tests/full_validate.sh (no args) on a machine without Node.js now sees the partial-result line and a bare exit 2, without the explicit remediation hint that used to accompany it. Low severity, but it's a real message loss with no equivalent added elsewhere — worth restoring the printf so the "how do I fix this" hint survives the refactor.

fi
printf '%s\n' '{"status":"full","skipped":[]}' > "$validation_result"
printf 'Validation result: full (%s)\n' "$validation_result"
else
printf '%s\n' '{"status":"partial","skipped":["node contract validator"]}' > "$validation_result"
printf 'Validation result: partial; Node.js contract validation was skipped (%s).\n' "$validation_result"
printf 'This result is non-authoritative; install Node.js for the full gate.\n'
exit 2
fi
}

printf 'Full base-cli validation passed.\n'
case "$gate" in
baseline) run_baseline ;;
runtime) run_runtime ;;
coverage) run_coverage ;;
typing) run_typing ;;
style) run_style ;;
contracts) run_contracts ;;
benchmark) run_benchmark ;;
security) run_security ;;
all)
run_baseline
run_coverage
run_typing
run_style
run_contracts
run_benchmark
run_security
write_validation_result
printf 'Full base-cli validation passed.\n'
;;
*)
printf 'Unknown validation gate: %s\n' "$gate" >&2
exit 2
;;
esac
Loading