diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a08ac8c..0373ff0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,7 +73,7 @@ jobs: - name: Set up Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: - python-version: "3.12" + python-version-file: ".python-version" - name: Install uv uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 diff --git a/.github/workflows/codacy.yml b/.github/workflows/codacy.yml index d7a5b95..8afd11e 100644 --- a/.github/workflows/codacy.yml +++ b/.github/workflows/codacy.yml @@ -83,7 +83,7 @@ jobs: - name: Run Codacy Analysis CLI if: ${{ env.CODACY_PROJECT_TOKEN != '' }} id: codacy_analysis - uses: codacy/codacy-analysis-cli-action@562ee3e92b8e92df8b67e0a5ff8aa8e261919c08 # pinned + uses: codacy/codacy-analysis-cli-action@562ee3e92b8e92df8b67e0a5ff8aa8e261919c08 # v4.4.7 with: # Check https://github.com/codacy/codacy-analysis-cli#project-token # to get your project token from your Codacy repository. diff --git a/AGENTS.md b/AGENTS.md index 8aef30a..a42d0af 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -125,7 +125,7 @@ When user requests commit message generation: ### Code Quality - **ALLOWED**: Run formatters/linters: `cargo fmt`, `cargo clippy`, `cargo doc`, `taplo fmt`, `taplo lint`, - `uv run ruff check --fix`, `uv run ruff format`, `shfmt -w`, `shellcheck -x`, `npx markdownlint --fix`, + `uv run ruff check --fix`, `uv run ruff format`, `shfmt -w`, `shellcheck -x`, `rumdl`, `dprint`, `typos`, `actionlint` - **NEVER**: Use `sed`, `awk`, `perl` for code edits - **ALWAYS**: Use `edit_files` tool for edits (and `create_file` for new files) @@ -205,8 +205,8 @@ macro-ification. ## Common Commands ```bash -just fix # Apply formatters/auto-fixes (mutating) just check # Lint/validators (non-mutating) +just fix # Apply formatters/auto-fixes (mutating) just ci # Full CI simulation (checks + tests + examples + bench compile) just test # Lib + doc tests (fast) just test-all # All tests (Rust + Python) diff --git a/README.md b/README.md index 923c033..7339c26 100644 --- a/README.md +++ b/README.md @@ -306,6 +306,14 @@ just fix # apply auto-fixes (mutating) just ci # lint + tests + examples + bench compile ``` +The repository uses Rust-native tooling for documentation and config checks: +`rumdl` for Markdown, `dprint` with `pretty_yaml` for YAML, `taplo` for TOML, +and `typos` for spelling. GitHub Actions references are SHA-pinned, restricted +to an explicit allowlist, and kept with readable version comments for review. + +CI runs `just ci` on Ubuntu, macOS, and Windows to keep platform coverage +aligned with the local comprehensive validation path. + For coverage commands and report locations, see [`docs/COVERAGE.md`](docs/COVERAGE.md). For the full set of developer commands, see `just --list` and `AGENTS.md`. diff --git a/cliff.toml b/cliff.toml index 9ef9267..818d102 100644 --- a/cliff.toml +++ b/cliff.toml @@ -95,7 +95,7 @@ commit_parsers = [ # Dependency bumps: keep all bump commits so archived release history # preserves CI/action/tooling dependency updates as well as Rust crates. { message = "^chore\\(deps\\): bump ", group = "Dependencies" }, - { message = "(?i)^bump ", group = "Dependencies" }, + { message = "(?i)^bump\\s+(deps?\\b|dependencies\\b|[A-Za-z0-9_.-]+/[A-Za-z0-9_./-]+|[A-Za-z0-9_.-]+\\s+from\\s+)", group = "Dependencies" }, # Conventional commit types { message = "^feat", group = "Added" }, diff --git a/justfile b/justfile index 6399718..e1459e6 100644 --- a/justfile +++ b/justfile @@ -27,7 +27,7 @@ _ensure-cargo-llvm-cov: set -euo pipefail installed_version="" if command -v cargo-llvm-cov >/dev/null; then - installed_version="$(cargo llvm-cov --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)" + installed_version="$(cargo llvm-cov --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)" fi if [[ "$installed_version" != "{{cargo_llvm_cov_version}}" ]]; then echo "❌ 'cargo-llvm-cov' {{cargo_llvm_cov_version}} not found. Install with:" @@ -40,7 +40,7 @@ _ensure-cargo-nextest: set -euo pipefail installed_version="" if cargo nextest --version >/dev/null 2>&1; then - installed_version="$(cargo nextest --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)" + installed_version="$(cargo nextest --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)" fi if [[ "$installed_version" != "{{cargo_nextest_version}}" ]]; then echo "❌ 'cargo-nextest' {{cargo_nextest_version}} not found. Install with:" @@ -53,7 +53,7 @@ _ensure-dprint: set -euo pipefail installed_version="" if command -v dprint >/dev/null; then - installed_version="$(dprint --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)" + installed_version="$(dprint --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)" fi if [[ "$installed_version" != "{{dprint_version}}" ]]; then echo "❌ 'dprint' {{dprint_version}} not found. Install with:" @@ -66,7 +66,7 @@ _ensure-git-cliff: set -euo pipefail installed_version="" if command -v git-cliff >/dev/null; then - installed_version="$(git-cliff --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)" + installed_version="$(git-cliff --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)" fi if [[ "$installed_version" != "{{git_cliff_version}}" ]]; then echo "❌ 'git-cliff' {{git_cliff_version}} not found. Install with:" @@ -84,7 +84,7 @@ _ensure-rumdl: set -euo pipefail installed_version="" if command -v rumdl >/dev/null; then - installed_version="$(rumdl --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)" + installed_version="$(rumdl --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)" fi if [[ "$installed_version" != "{{rumdl_version}}" ]]; then echo "❌ 'rumdl' {{rumdl_version}} not found. Install with:" @@ -107,7 +107,7 @@ _ensure-taplo: set -euo pipefail installed_version="" if command -v taplo >/dev/null; then - installed_version="$(taplo --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)" + installed_version="$(taplo --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)" fi if [[ "$installed_version" != "{{taplo_version}}" ]]; then echo "❌ 'taplo' {{taplo_version}} not found. Install with:" @@ -121,7 +121,7 @@ _ensure-typos: set -euo pipefail installed_version="" if command -v typos >/dev/null; then - installed_version="$(typos --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)" + installed_version="$(typos --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)" fi if [[ "$installed_version" != "{{typos_version}}" ]]; then echo "❌ 'typos' {{typos_version}} not found. Install with:" @@ -139,7 +139,7 @@ _ensure-zizmor: set -euo pipefail installed_version="" if command -v zizmor >/dev/null; then - installed_version="$(zizmor --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)" + installed_version="$(zizmor --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)" fi if [[ "$installed_version" != "{{zizmor_version}}" ]]; then echo "❌ 'zizmor' {{zizmor_version}} not found. Install with:" @@ -318,13 +318,20 @@ doc-check: # Examples examples: - cargo run --quiet --example det_5x5 - cargo run --quiet --example solve_5x5 - cargo run --quiet --example ldlt_solve_3x3 - cargo run --quiet --example const_det_4x4 - cargo run --quiet --features exact --example exact_det_3x3 - cargo run --quiet --features exact --example exact_sign_3x3 - cargo run --quiet --features exact --example exact_solve_3x3 + #!/usr/bin/env bash + set -euo pipefail + cargo build --examples + cargo build --features exact --examples + + exe_suffix="" + if [[ "${OS:-}" == "Windows_NT" ]]; then + exe_suffix=".exe" + fi + + for example in det_5x5 solve_5x5 ldlt_solve_3x3 const_det_4x4 exact_det_3x3 exact_sign_3x3 exact_solve_3x3; do + "target/debug/examples/${example}${exe_suffix}" + done + # Fix (mutating): apply formatters/auto-fixes fix: toml-fmt fmt python-fix shell-fmt markdown-fix yaml-fix @@ -492,35 +499,35 @@ setup-tools: echo "Ensuring cargo tools..." cargo_llvm_cov_version="{{cargo_llvm_cov_version}}" - if ! have cargo-llvm-cov || [[ "$(cargo llvm-cov --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)" != "$cargo_llvm_cov_version" ]]; then + if ! have cargo-llvm-cov || [[ "$(cargo llvm-cov --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)" != "$cargo_llvm_cov_version" ]]; then cargo install --locked cargo-llvm-cov --version "$cargo_llvm_cov_version" fi cargo_nextest_version="{{cargo_nextest_version}}" - if ! cargo nextest --version >/dev/null 2>&1 || [[ "$(cargo nextest --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)" != "$cargo_nextest_version" ]]; then + if ! cargo nextest --version >/dev/null 2>&1 || [[ "$(cargo nextest --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)" != "$cargo_nextest_version" ]]; then cargo install --locked cargo-nextest --version "$cargo_nextest_version" fi dprint_version="{{dprint_version}}" - if ! have dprint || [[ "$(dprint --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)" != "$dprint_version" ]]; then + if ! have dprint || [[ "$(dprint --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)" != "$dprint_version" ]]; then cargo install --locked dprint --version "$dprint_version" fi git_cliff_version="{{git_cliff_version}}" - if ! have git-cliff || [[ "$(git-cliff --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)" != "$git_cliff_version" ]]; then + if ! have git-cliff || [[ "$(git-cliff --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)" != "$git_cliff_version" ]]; then cargo install --locked git-cliff --version "$git_cliff_version" fi rumdl_version="{{rumdl_version}}" - if ! have rumdl || [[ "$(rumdl --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)" != "$rumdl_version" ]]; then + if ! have rumdl || [[ "$(rumdl --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)" != "$rumdl_version" ]]; then cargo install --locked rumdl --version "$rumdl_version" fi taplo_version="{{taplo_version}}" - if ! have taplo || [[ "$(taplo --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)" != "$taplo_version" ]]; then + if ! have taplo || [[ "$(taplo --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)" != "$taplo_version" ]]; then cargo install --locked taplo-cli --version "$taplo_version" fi typos_version="{{typos_version}}" - if ! have typos || [[ "$(typos --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)" != "$typos_version" ]]; then + if ! have typos || [[ "$(typos --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)" != "$typos_version" ]]; then cargo install --locked typos-cli --version "$typos_version" fi zizmor_version="{{zizmor_version}}" - if ! have zizmor || [[ "$(zizmor --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)" != "$zizmor_version" ]]; then + if ! have zizmor || [[ "$(zizmor --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)" != "$zizmor_version" ]]; then cargo install --locked zizmor --version "$zizmor_version" fi echo "" @@ -535,6 +542,11 @@ setup-tools: exit 1 fi + if ! have jq; then + echo "❌ 'jq' not found. See 'just setup' or install: brew install jq" + echo "" + fi + echo "" echo "Verifying required commands are available..." missing=0 @@ -599,6 +611,8 @@ shell-fmt: _ensure-shfmt shell-lint: shell-check +shell-fix: shell-fmt + # Spell check (typos) spell-check: _ensure-typos #!/usr/bin/env bash @@ -707,6 +721,10 @@ toml-lint: _ensure-taplo echo "No TOML files found to lint." fi +toml-check: toml-fmt-check toml-lint + +toml-fix: toml-fmt + # File validation validate-json: _ensure-jq #!/usr/bin/env bash diff --git a/pyproject.toml b/pyproject.toml index 28b2028..a101d56 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -141,7 +141,7 @@ package = true dev = [ "actionlint-py==1.7.12.24", "mypy>=1.19.0", - "pytest>=9.0.3", + "pytest==9.0.3", "ruff>=0.15.14", "semgrep==1.164.0", "ty>=0.0.40", diff --git a/scripts/tag_release.py b/scripts/tag_release.py old mode 100644 new mode 100755 index 9a4f79f..14f2778 --- a/scripts/tag_release.py +++ b/scripts/tag_release.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/env -S uv run """Create annotated git tags from CHANGELOG.md sections. Handles GitHub's 125KB tag-annotation size limit by falling back to a short diff --git a/scripts/tests/test_subprocess_utils.py b/scripts/tests/test_subprocess_utils.py index fd9f36b..a5ebe98 100644 --- a/scripts/tests/test_subprocess_utils.py +++ b/scripts/tests/test_subprocess_utils.py @@ -131,11 +131,29 @@ def test_run_safe_command_uses_safe_executable(self, mock_run: MagicMock, _mock_ args, _kwargs = mock_run.call_args assert args[0] == ["/usr/bin/gnuplot", "--version"] - def test_git_convenience_helpers(self) -> None: - assert get_git_commit_hash() - assert get_git_remote_url() + @patch("subprocess_utils.run_git_command") + def test_git_convenience_helpers(self, mock_run_git: MagicMock) -> None: + def fake_run_git(args: list[str], **_kwargs: object) -> subprocess_utils.subprocess.CompletedProcess[str]: + stdout_by_args = { + ("rev-parse", "HEAD"): "abc123def456\n", + ("remote", "get-url", "origin"): "https://github.com/example/repo.git\n", + ("rev-parse", "--git-dir"): ".git\n", + ("log", "--oneline", "-n", "1"): "abc123d message\n", + } + return subprocess_utils.subprocess.CompletedProcess(args=["git", *args], returncode=0, stdout=stdout_by_args[tuple(args)]) + + mock_run_git.side_effect = fake_run_git + + assert get_git_commit_hash() == "abc123def456" + assert get_git_remote_url() == "https://github.com/example/repo.git" assert check_git_repo() is True assert check_git_history() is True + assert [call_args.args[0] for call_args in mock_run_git.call_args_list] == [ + ["rev-parse", "HEAD"], + ["remote", "get-url", "origin"], + ["rev-parse", "--git-dir"], + ["log", "--oneline", "-n", "1"], + ] def test_find_project_root(self) -> None: assert (find_project_root() / "Cargo.toml").is_file() diff --git a/semgrep.yaml b/semgrep.yaml index 52fd809..79d7e4a 100644 --- a/semgrep.yaml +++ b/semgrep.yaml @@ -115,3 +115,22 @@ rules: - "/.github/workflows/**/*.yaml" patterns: - pattern-regex: '(?m)^\s*uses:\s*(?!\./)(?!docker://)[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)?@[a-fA-F0-9]{40}\s*$' + + - id: la-stack.docs.check-before-fix-command-order + languages: + - regex + severity: WARNING + message: "Document non-mutating just check commands before mutating just fix commands." + metadata: + category: maintainability + rationale: "User-facing workflow docs should encourage validation before mutation." + paths: + include: + - "/AGENTS.md" + - "/README.md" + - "/docs/**/*.md" + - "/justfile" + exclude: + - "/docs/archive/**" + patterns: + - pattern-regex: '(?ms)\bjust\s+fix\b.{0,400}\bjust\s+check\b|\bjust\s+python-fix\b.{0,400}\bjust\s+python-check\b' diff --git a/uv.lock b/uv.lock index 393d974..9150bd4 100644 --- a/uv.lock +++ b/uv.lock @@ -453,7 +453,7 @@ dev = [ dev = [ { name = "actionlint-py", specifier = "==1.7.12.24" }, { name = "mypy", specifier = ">=1.19.0" }, - { name = "pytest", specifier = ">=9.0.3" }, + { name = "pytest", specifier = "==9.0.3" }, { name = "ruff", specifier = ">=0.15.14" }, { name = "semgrep", specifier = "==1.164.0" }, { name = "ty", specifier = ">=0.0.40" },