#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT_DIR" AVAILABLE_CHECKS=( version changelog rust_fmt rust_clippy rust_core rust_bench python_lint python_typecheck python_test docs wasm manifest ) DEFAULT_CHECKS=("${AVAILABLE_CHECKS[@]}") python_env_ready=0 usage() { cat <<'EOF' Usage: scripts/pre_push_checks.sh scripts/pre_push_checks.sh [ ...] scripts/pre_push_checks.sh --list Runs the repo's basic local CI gate before push. By default it covers: version changelog rust_fmt rust_clippy rust_core rust_bench python_lint python_typecheck python_test docs wasm manifest Notes: - This mirrors the required CI categories we can run locally. - It intentionally skips the multi-Python test matrix, audit jobs, perf smoke, and benchmark-regression jobs. EOF } list_checks() { printf '%s\n' "${AVAILABLE_CHECKS[@]}" } need_cmd() { local command_name="$1" if ! command -v "$command_name" >/dev/null 2>&1; then echo "Missing required command: $command_name" >&2 exit 1 fi } run_cmd() { printf ' +' printf ' %q' "$@" printf '\n' "$@" } ensure_python_env() { if [[ "$python_env_ready" -eq 1 ]]; then return fi need_cmd uv run_cmd uv sync --extra dev --extra docs --extra mcp run_cmd uv run --extra dev --extra docs --extra mcp maturin develop --release python_env_ready=1 } run_version() { need_cmd python3 run_cmd python3 scripts/bump_version.py --check } run_changelog() { need_cmd python3 run_cmd python3 scripts/check_changelog.py } run_rust_fmt() { need_cmd cargo run_cmd cargo fmt --all -- --check } run_rust_clippy() { need_cmd cargo run_cmd cargo clippy --release -- -D warnings } run_rust_core() { need_cmd cargo run_cmd cargo build -p ferro_ta_core run_cmd cargo test -p ferro_ta_core } run_rust_bench() { need_cmd cargo run_cmd cargo bench -p ferro_ta_core --no-run } run_python_lint() { need_cmd uv run_cmd uv run --with ruff ruff check python/ tests/ run_cmd uv run --with ruff ruff format --check python/ tests/ } run_python_typecheck() { need_cmd uv run_cmd uv run --with mypy --with numpy python -m mypy python/ferro_ta --ignore-missing-imports --no-error-summary run_cmd uv run --with pyright python -m pyright python/ferro_ta } run_python_test() { ensure_python_env run_cmd uv run --extra dev --extra mcp --with pytest-cov pytest tests/unit/ tests/integration/ -v --cov=ferro_ta --cov-report=term-missing --cov-fail-under=65 } run_docs() { ensure_python_env run_cmd uv run --extra docs python -m sphinx -b html docs docs/_build -W --keep-going } run_wasm() { need_cmd node need_cmd wasm-pack local benchmark_json="../.wasm_benchmark.prepush.json" ( cd wasm trap 'rm -f "$benchmark_json"' EXIT run_cmd wasm-pack test --node run_cmd npm run build run_cmd node bench.js --json "$benchmark_json" ) } run_manifest() { need_cmd python3 run_cmd python3 scripts/check_api_manifest.py } run_check() { local check_name="$1" case "$check_name" in version) run_version ;; changelog) run_changelog ;; rust_fmt) run_rust_fmt ;; rust_clippy) run_rust_clippy ;; rust_core) run_rust_core ;; rust_bench) run_rust_bench ;; python_lint) run_python_lint ;; python_typecheck) run_python_typecheck ;; python_test) run_python_test ;; docs) run_docs ;; wasm) run_wasm ;; manifest) run_manifest ;; *) echo "Unknown check: $check_name" >&2 echo "Use --list to see supported checks." >&2 exit 1 ;; esac } if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then usage exit 0 fi if [[ "${1:-}" == "--list" ]]; then list_checks exit 0 fi selected_checks=() if [[ "$#" -gt 0 ]]; then selected_checks=("$@") else selected_checks=("${DEFAULT_CHECKS[@]}") fi total_checks="${#selected_checks[@]}" index=0 for check_name in "${selected_checks[@]}"; do index=$((index + 1)) printf '\n[%d/%d] %s\n' "$index" "$total_checks" "$check_name" run_check "$check_name" done printf '\nAll selected pre-push checks passed.\n'