From f782fcb1870f62ec2bf2d3e6ffba98c6451d63ce Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 13:08:45 +0000 Subject: [PATCH 1/3] chore(deps): update dependency jebel-quant/rhiza to v0.8.3 --- .rhiza/template.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.rhiza/template.yml b/.rhiza/template.yml index 090b719c..61699757 100644 --- a/.rhiza/template.yml +++ b/.rhiza/template.yml @@ -1,5 +1,5 @@ template-repository: "jebel-quant/rhiza" -template-branch: "v0.8.0" +template-branch: "v0.8.3" templates: - core From faf8c0e918fd0d13fdd157eb8a232b7b09393ff0 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Tue, 24 Feb 2026 17:17:48 +0400 Subject: [PATCH 2/3] fix conftest --- .rhiza/.cfg.toml | 5 ----- .rhiza/tests/api/conftest.py | 4 ++++ .rhiza/tests/conftest.py | 4 ++++ .rhiza/tests/sync/conftest.py | 4 ++++ tests/benchmarks/conftest.py | 4 ++++ tests/test_actions/conftest.py | 4 ++++ tests/test_cradle/conftest.py | 7 ++++++- 7 files changed, 26 insertions(+), 6 deletions(-) diff --git a/.rhiza/.cfg.toml b/.rhiza/.cfg.toml index e084738b..6ae25795 100644 --- a/.rhiza/.cfg.toml +++ b/.rhiza/.cfg.toml @@ -30,8 +30,3 @@ values = [ filename = "pyproject.toml" search = 'version = "{current_version}"' replace = 'version = "{new_version}"' - -# [[tool.bumpversion.files]] -# filename = ".rhiza/template-bundles.yml" -# search = 'version: "{current_version}"' -# replace = 'version: "{new_version}"' diff --git a/.rhiza/tests/api/conftest.py b/.rhiza/tests/api/conftest.py index 9cb5aef2..68fa2fba 100644 --- a/.rhiza/tests/api/conftest.py +++ b/.rhiza/tests/api/conftest.py @@ -5,6 +5,10 @@ - run_make: Helper to execute make commands with dry-run support (imported from test_utils) - setup_rhiza_git_repo: Initialize a git repo configured as rhiza origin (imported from test_utils) - SPLIT_MAKEFILES: List of split Makefile paths + +Security Notes: +- S101 (assert usage): Asserts are appropriate in test code for validating conditions +- S603/S607 (subprocess usage): Any subprocess calls use controlled inputs in test environments """ from __future__ import annotations diff --git a/.rhiza/tests/conftest.py b/.rhiza/tests/conftest.py index b2a1c026..af420d1a 100644 --- a/.rhiza/tests/conftest.py +++ b/.rhiza/tests/conftest.py @@ -4,6 +4,10 @@ (https://github.com/jebel-quant/rhiza). Provides test fixtures for testing git-based workflows and version management. + +Security Notes: +- S101 (assert usage): Asserts are appropriate in test code for validating conditions +- S603/S607 (subprocess usage): Any subprocess calls use controlled inputs in test environments """ import logging diff --git a/.rhiza/tests/sync/conftest.py b/.rhiza/tests/sync/conftest.py index 383c8b2f..146ae689 100644 --- a/.rhiza/tests/sync/conftest.py +++ b/.rhiza/tests/sync/conftest.py @@ -2,6 +2,10 @@ Provides environment setup for template sync, workflow versioning, and content validation tests. + +Security Notes: +- S101 (assert usage): Asserts are appropriate in test code for validating conditions +- S603/S607 (subprocess usage): Any subprocess calls use controlled inputs in test environments """ from __future__ import annotations diff --git a/tests/benchmarks/conftest.py b/tests/benchmarks/conftest.py index a3c07dee..3566788c 100644 --- a/tests/benchmarks/conftest.py +++ b/tests/benchmarks/conftest.py @@ -2,4 +2,8 @@ This file can be used to add custom fixtures or configuration for your benchmark tests. + +Security Notes: +- S101 (assert usage): Asserts are appropriate in test code for validating conditions +- S603/S607 (subprocess usage): Any subprocess calls use controlled inputs in test environments """ diff --git a/tests/test_actions/conftest.py b/tests/test_actions/conftest.py index 72221f6e..20bb34c9 100644 --- a/tests/test_actions/conftest.py +++ b/tests/test_actions/conftest.py @@ -3,6 +3,10 @@ This module provides fixtures for accessing GitHub Actions in the repository, including paths to the repository root, actions directory, and action.yml files. These fixtures simplify path handling in the test files. + +Security Notes: +- S101 (assert usage): Asserts are appropriate in test code for validating conditions +- S603/S607 (subprocess usage): Any subprocess calls use controlled inputs in test environments """ import glob diff --git a/tests/test_cradle/conftest.py b/tests/test_cradle/conftest.py index 117d1365..875a268a 100644 --- a/tests/test_cradle/conftest.py +++ b/tests/test_cradle/conftest.py @@ -1,4 +1,9 @@ -"""Global fixtures for cradle tests.""" +"""Global fixtures for cradle tests. + +Security Notes: +- S101 (assert usage): Asserts are appropriate in test code for validating conditions +- S603/S607 (subprocess usage): Any subprocess calls use controlled inputs in test environments +""" from pathlib import Path From 6b0f5f262866b4ffdcfc9202da3f7b18295c5f7f Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Tue, 24 Feb 2026 17:18:51 +0400 Subject: [PATCH 3/3] sync --- .github/copilot-instructions.md | 24 +++++ .github/hooks/session-end.sh | 29 ++++-- .github/hooks/session-start.sh | 18 +++- .github/workflows/rhiza_release.yml | 15 ++- .gitignore | 4 + .rhiza/.cfg.toml | 4 +- .rhiza/.env | 1 - .rhiza/.rhiza-version | 2 +- .rhiza/docs/CONFIG.md | 4 +- .rhiza/history | 16 +++- .rhiza/make.d/book.mk | 49 ++++++---- .rhiza/make.d/docs.mk | 4 +- .rhiza/make.d/marimo.mk | 2 +- .rhiza/make.d/quality.mk | 18 +++- .rhiza/make.d/test.mk | 51 ++++++++-- .rhiza/requirements/tools.txt | 2 +- .rhiza/rhiza.mk | 13 ++- .rhiza/scripts/.gitkeep | 0 .rhiza/tests/README.md | 45 +++++++++ .rhiza/tests/api/conftest.py | 9 +- .rhiza/tests/api/test_makefile_targets.py | 26 +++-- .rhiza/tests/conftest.py | 7 +- .rhiza/tests/integration/test_book_targets.py | 15 ++- .rhiza/tests/integration/test_sbom.py | 13 +++ .rhiza/tests/sync/conftest.py | 8 +- .rhiza/tests/sync/test_readme_validation.py | 95 +++++++++++++++++-- .rhiza/tests/sync/test_rhiza_version.py | 43 +++++++++ .rhiza/tests/test_utils.py | 9 +- CONTRIBUTING.md | 46 +++++++++ Makefile | 34 +++++++ docs/ARCHITECTURE.md | 4 - docs/CUSTOMIZATION.md | 2 + docs/GLOSSARY.md | 6 ++ docs/QUICK_REFERENCE.md | 13 +++ docs/SECURITY.md | 5 +- docs/TESTS.md | 29 ++++++ renovate.json | 56 ----------- tests/benchmarks/conftest.py | 9 +- tests/benchmarks/test_benchmarks.py | 10 +- 39 files changed, 590 insertions(+), 150 deletions(-) delete mode 100644 .rhiza/scripts/.gitkeep delete mode 100644 renovate.json diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 22bae654..185bc71d 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -151,6 +151,30 @@ For DevContainers and Codespaces, the `.devcontainer/` configuration and `bootst 4. **Format**: Run `make fmt` before committing. 5. **Verify**: Run `make deptry` to check dependencies. +## GitHub Agentic Workflows (gh-aw) + +This repository uses GitHub Agentic Workflows for AI-driven automation. +Agentic workflow files are Markdown files in `.github/workflows/` with +`.lock.yml` compiled counterparts. + +**Key Commands:** +- `make gh-aw-compile` or `gh aw compile` — Compile workflow `.md` files to `.lock.yml` +- `make gh-aw-run WORKFLOW=` or `gh aw run ` — Run a specific workflow locally +- `make gh-aw-status` — Check status of all agentic workflows +- `make gh-aw-setup` — Configure secrets and engine for first-time setup + +**Important Rules:** +- **Never edit `.lock.yml` files directly** — Always edit the `.md` source and recompile +- Workflows must be compiled before they can run in GitHub Actions +- After editing any `.md` workflow, always run `make gh-aw-compile` and commit both files + +**Available Starter Workflows:** +- `daily-repo-status.md` — Daily repository health reports +- `ci-doctor.md` — Automatic CI failure diagnosis +- `issue-triage.md` — Automatic issue classification and labeling + +For more details, see `docs/GH_AW.md`. + ## Key Files - `Makefile`: Main entry point for tasks. diff --git a/.github/hooks/session-end.sh b/.github/hooks/session-end.sh index ce9f0edd..93b995c8 100755 --- a/.github/hooks/session-end.sh +++ b/.github/hooks/session-end.sh @@ -6,16 +6,31 @@ set -euo pipefail echo "[copilot-hook] Running post-work quality gates..." +# Format code echo "[copilot-hook] Formatting code..." -make fmt || { - echo "[copilot-hook] WARNING: Formatting check failed." +if ! make fmt; then + echo "[copilot-hook] ❌ ERROR: Formatting check failed" + echo "[copilot-hook] 💡 Remediation: Review the formatting errors above" + echo "[copilot-hook] 💡 Common fixes:" + echo "[copilot-hook] - Run 'make fmt' locally to see detailed errors" + echo "[copilot-hook] - Check for syntax errors in modified files" + echo "[copilot-hook] - Ensure all files follow project style guidelines" exit 1 -} +fi +echo "[copilot-hook] ✓ Code formatting passed" +# Run tests echo "[copilot-hook] Running tests..." -make test || { - echo "[copilot-hook] WARNING: Tests failed." +if ! make test; then + echo "[copilot-hook] ❌ ERROR: Tests failed" + echo "[copilot-hook] 💡 Remediation: Review the test failures above" + echo "[copilot-hook] 💡 Common fixes:" + echo "[copilot-hook] - Run 'make test' locally to see detailed output" + echo "[copilot-hook] - Check if new code broke existing functionality" + echo "[copilot-hook] - Verify test assertions match expected behavior" + echo "[copilot-hook] - Review test logs in _tests/ directory" exit 1 -} +fi +echo "[copilot-hook] ✓ Tests passed" -echo "[copilot-hook] All quality gates passed." +echo "[copilot-hook] ✅ All quality gates passed" diff --git a/.github/hooks/session-start.sh b/.github/hooks/session-start.sh index 13700cd3..22023f32 100755 --- a/.github/hooks/session-start.sh +++ b/.github/hooks/session-start.sh @@ -9,19 +9,29 @@ echo "[copilot-hook] Validating environment..." # Verify uv is available if ! command -v uv >/dev/null 2>&1 && [ ! -x "./bin/uv" ]; then - echo "[copilot-hook] ERROR: uv not found. Run 'make install' to set up the environment." + echo "[copilot-hook] ❌ ERROR: uv not found" + echo "[copilot-hook] 💡 Remediation: Run 'make install' to set up the environment" + echo "[copilot-hook] 💡 Alternative: Ensure uv is in PATH or ./bin/uv exists" exit 1 fi +echo "[copilot-hook] ✓ uv is available" # Verify virtual environment exists if [ ! -d ".venv" ]; then - echo "[copilot-hook] ERROR: .venv not found. Run 'make install' to set up the environment." + echo "[copilot-hook] ❌ ERROR: .venv not found" + echo "[copilot-hook] 💡 Remediation: Run 'make install' to create the virtual environment" + echo "[copilot-hook] 💡 Details: The .venv directory should contain Python dependencies" exit 1 fi +echo "[copilot-hook] ✓ Virtual environment exists" # Verify virtual environment is on PATH (activated via copilot-setup-steps.yml) if ! command -v python >/dev/null 2>&1 || [[ "$(command -v python)" != *".venv"* ]]; then - echo "[copilot-hook] WARNING: .venv/bin is not on PATH. The agent may not use the correct Python." + echo "[copilot-hook] ⚠️ WARNING: .venv/bin is not on PATH" + echo "[copilot-hook] 💡 Note: The agent may not use the correct Python version" + echo "[copilot-hook] 💡 Remediation: Ensure .venv/bin is added to PATH before running the agent" +else + echo "[copilot-hook] ✓ Virtual environment is activated" fi -echo "[copilot-hook] Environment validated successfully." +echo "[copilot-hook] ✅ Environment validated successfully" diff --git a/.github/workflows/rhiza_release.yml b/.github/workflows/rhiza_release.yml index 47f6080e..d7230e3e 100644 --- a/.github/workflows/rhiza_release.yml +++ b/.github/workflows/rhiza_release.yml @@ -136,11 +136,16 @@ jobs: TAG_VERSION=${TAG_VERSION#v} PROJECT_VERSION=$(uv version --short) - if [[ "$PROJECT_VERSION" != "$TAG_VERSION" ]]; then - echo "::error::Version mismatch: pyproject.toml has '$PROJECT_VERSION' but tag is '$TAG_VERSION'" + # Normalize tag version to PEP 440 format for comparison. + # Tags use semver format (e.g., 0.11.1-beta.1) while uv version --short + # returns PEP 440 normalized format (e.g., 0.11.1b1). + NORMALIZED_TAG=$(uv run --with packaging --no-project python3 -c "from packaging.version import Version; print(Version('$TAG_VERSION'))") + + if [[ "$PROJECT_VERSION" != "$NORMALIZED_TAG" ]]; then + echo "::error::Version mismatch: pyproject.toml has '$PROJECT_VERSION' but tag is '$NORMALIZED_TAG' (from tag '$TAG_VERSION')" exit 1 fi - echo "Version verified: $PROJECT_VERSION matches tag" + echo "Version verified: $PROJECT_VERSION matches tag (normalized: $NORMALIZED_TAG)" - name: Detect buildable Python package id: buildable @@ -174,8 +179,8 @@ jobs: run: | printf "[INFO] Generating SBOM in CycloneDX format...\n" # Note: uvx caches the tool environment, so the second call is fast - uvx --from 'cyclonedx-bom>=7.0.0' cyclonedx-py environment --of JSON -o sbom.cdx.json - uvx --from 'cyclonedx-bom>=7.0.0' cyclonedx-py environment --of XML -o sbom.cdx.xml + uvx --from 'cyclonedx-bom>=7.0.0' cyclonedx-py environment --pyproject pyproject.toml --of JSON -o sbom.cdx.json + uvx --from 'cyclonedx-bom>=7.0.0' cyclonedx-py environment --pyproject pyproject.toml --of XML -o sbom.cdx.xml printf "[INFO] SBOM generation complete\n" printf "Generated files:\n" ls -lh sbom.cdx.* diff --git a/.gitignore b/.gitignore index af820bd5..1e8a34fc 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ _marimushka _mkdocs _benchmarks _jupyter +_site # temp file used by Junie .output.txt @@ -80,6 +81,9 @@ coverage.json .pytest_cache/ cover/ +# Security scanning baselines (regenerate as needed) +.bandit-baseline.json + # Translations *.mo *.pot diff --git a/.rhiza/.cfg.toml b/.rhiza/.cfg.toml index 6ae25795..c96b1dd1 100644 --- a/.rhiza/.cfg.toml +++ b/.rhiza/.cfg.toml @@ -1,5 +1,5 @@ [tool.bumpversion] -parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(?:-(?P[a-z]+)\\.(?P\\d+))?(?:\\+build\\.(?P\\d+))?" +parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(?:[-]?(?P[a-z]+)[\\.]?(?P\\d+))?(?:\\+build\\.(?P\\d+))?" serialize = ["{major}.{minor}.{patch}-{release}.{pre_n}+build.{build_n}", "{major}.{minor}.{patch}+build.{build_n}", "{major}.{minor}.{patch}-{release}.{pre_n}", "{major}.{minor}.{patch}"] search = "{current_version}" replace = "{new_version}" @@ -21,7 +21,9 @@ optional_value = "prod" values = [ "dev", "alpha", + "a", # PEP 440 short form for alpha "beta", + "b", # PEP 440 short form for beta "rc", "prod" ] diff --git a/.rhiza/.env b/.rhiza/.env index 2c7edd5a..df5caf68 100644 --- a/.rhiza/.env +++ b/.rhiza/.env @@ -1,6 +1,5 @@ MARIMO_FOLDER=book/marimo/notebooks SOURCE_FOLDER=src -SCRIPTS_FOLDER=.rhiza/scripts # Book-specific variables BOOK_TITLE=Project Documentation diff --git a/.rhiza/.rhiza-version b/.rhiza/.rhiza-version index 142464bf..bc859cbd 100644 --- a/.rhiza/.rhiza-version +++ b/.rhiza/.rhiza-version @@ -1 +1 @@ -0.11.0 \ No newline at end of file +0.11.2 diff --git a/.rhiza/docs/CONFIG.md b/.rhiza/docs/CONFIG.md index 205deb56..3c41a263 100644 --- a/.rhiza/docs/CONFIG.md +++ b/.rhiza/docs/CONFIG.md @@ -1,6 +1,6 @@ # Rhiza Configuration -This directory contains platform-agnostic scripts and utilities for the repository that can be used by GitHub Actions, GitLab CI, or other CI/CD systems. +This directory contains platform-agnostic utilities for the repository that can be used by GitHub Actions, GitLab CI, or other CI/CD systems. ## Important Documentation @@ -14,8 +14,6 @@ This directory contains platform-agnostic scripts and utilities for the reposito ## Structure -- **scripts/** - Shell scripts for common tasks (book generation, release, etc.) -- **scripts/customisations/** - Repository-specific customisation hooks - **utils/** - Python utilities for version management GitHub-specific composite actions are located in `.github/rhiza/actions/`. diff --git a/.rhiza/history b/.rhiza/history index 634f5a8e..c7ae4ed4 100644 --- a/.rhiza/history +++ b/.rhiza/history @@ -1,7 +1,7 @@ # Rhiza Template History # This file lists all files managed by the Rhiza template. # Template repository: jebel-quant/rhiza -# Template branch: v0.8.0 +# Template branch: v0.8.3 # # Files under template control: .editorconfig @@ -14,7 +14,9 @@ .github/hooks/hooks.json .github/hooks/session-end.sh .github/hooks/session-start.sh +.github/secret_scanning.yml .github/workflows/copilot-setup-steps.yml +.github/workflows/renovate_rhiza_sync.yml .github/workflows/rhiza_benchmarks.yml .github/workflows/rhiza_book.yml .github/workflows/rhiza_ci.yml @@ -48,6 +50,7 @@ .rhiza/make.d/custom-env.mk .rhiza/make.d/custom-task.mk .rhiza/make.d/docs.mk +.rhiza/make.d/gh-aw.mk .rhiza/make.d/github.mk .rhiza/make.d/marimo.mk .rhiza/make.d/quality.mk @@ -59,10 +62,10 @@ .rhiza/requirements/tests.txt .rhiza/requirements/tools.txt .rhiza/rhiza.mk -.rhiza/scripts/.gitkeep .rhiza/templates/minibook/custom.html.jinja2 .rhiza/tests/README.md .rhiza/tests/api/conftest.py +.rhiza/tests/api/test_gh_aw_targets.py .rhiza/tests/api/test_github_targets.py .rhiza/tests/api/test_makefile_api.py .rhiza/tests/api/test_makefile_targets.py @@ -75,6 +78,13 @@ .rhiza/tests/integration/test_sbom.py .rhiza/tests/integration/test_test_mk.py .rhiza/tests/integration/test_virtual_env_unexport.py +.rhiza/tests/security/test_security_patterns.py +.rhiza/tests/shell/test_scripts.sh +.rhiza/tests/stress/README.md +.rhiza/tests/stress/__init__.py +.rhiza/tests/stress/conftest.py +.rhiza/tests/stress/test_git_stress.py +.rhiza/tests/stress/test_makefile_stress.py .rhiza/tests/structure/test_lfs_structure.py .rhiza/tests/structure/test_project_layout.py .rhiza/tests/structure/test_requirements.py @@ -89,6 +99,7 @@ CODE_OF_CONDUCT.md CONTRIBUTING.md LICENSE Makefile +SECURITY.md book/marimo/notebooks/rhiza.py docs/ARCHITECTURE.md docs/BOOK.md @@ -100,7 +111,6 @@ docs/QUICK_REFERENCE.md docs/SECURITY.md docs/TESTS.md pytest.ini -renovate.json ruff.toml tests/benchmarks/conftest.py tests/benchmarks/test_benchmarks.py diff --git a/.rhiza/make.d/book.mk b/.rhiza/make.d/book.mk index 715db7f8..26fb0b10 100644 --- a/.rhiza/make.d/book.mk +++ b/.rhiza/make.d/book.mk @@ -4,7 +4,16 @@ # and compiling a companion book (minibook). # Declare phony targets (they don't produce files) -.PHONY: marimushka mkdocs-build book +.PHONY: marimushka mkdocs-build book test benchmark stress hypothesis-test docs + +# Define default no-op targets for test-related book dependencies. +# These are used when test.mk is not available or tests are not installed, +# ensuring 'make book' succeeds even without a test environment. +test:: ; @: +benchmark:: ; @: +stress:: ; @: +hypothesis-test:: ; @: +docs:: ; @: # Define a default no-op marimushka target that will be used # when book/marimo/marimo.mk doesn't exist or doesn't define marimushka @@ -40,6 +49,9 @@ BOOK_SECTIONS := \ "API|_pdoc/index.html|pdoc/index.html|_pdoc|pdoc" \ "Coverage|_tests/html-coverage/index.html|tests/html-coverage/index.html|_tests/html-coverage|tests/html-coverage" \ "Test Report|_tests/html-report/report.html|tests/html-report/report.html|_tests/html-report|tests/html-report" \ + "Benchmarks|_tests/benchmarks/report.html|tests/benchmarks/report.html|_tests/benchmarks|tests/benchmarks" \ + "Stress Tests|_tests/stress/report.html|tests/stress/report.html|_tests/stress|tests/stress" \ + "Hypothesis Tests|_tests/hypothesis/report.html|tests/hypothesis/report.html|_tests/hypothesis|tests/hypothesis" \ "Notebooks|_marimushka/index.html|marimushka/index.html|_marimushka|marimushka" \ "Official Documentation|_mkdocs/index.html|docs/index.html|_mkdocs|docs" @@ -49,27 +61,10 @@ BOOK_SECTIONS := \ # 1. Aggregates API docs, coverage, test reports, notebooks, and MkDocs site into _book. # 2. Generates links.json to define the book structure. # 3. Uses 'minibook' to compile the final HTML site. -book:: test docs marimushka mkdocs-build ## compile the companion book +book:: test benchmark stress hypothesis-test docs marimushka mkdocs-build ## compile the companion book @printf "${BLUE}[INFO] Building combined documentation...${RESET}\n" @rm -rf _book && mkdir -p _book - @if [ -f "_tests/coverage.json" ]; then \ - printf "${BLUE}[INFO] Generating coverage badge JSON...${RESET}\n"; \ - mkdir -p _book/tests; \ - ${UV_BIN} run python -c "\ -import json; \ -data = json.load(open('_tests/coverage.json')); \ -pct = int(data['totals']['percent_covered']); \ -color = 'brightgreen' if pct >= 90 else 'green' if pct >= 80 else 'yellow' if pct >= 70 else 'orange' if pct >= 60 else 'red'; \ -badge = {'schemaVersion': 1, 'label': 'coverage', 'message': f'{pct}%', 'color': color}; \ -json.dump(badge, open('_book/tests/coverage-badge.json', 'w'))"; \ - printf "${BLUE}[INFO] Coverage badge JSON:${RESET}\n"; \ - cat _book/tests/coverage-badge.json; \ - printf "\n"; \ - else \ - printf "${YELLOW}[WARN] No coverage.json found, skipping badge generation${RESET}\n"; \ - fi - @printf "{\n" > _book/links.json @first=1; \ for entry in $(BOOK_SECTIONS); do \ @@ -91,6 +86,22 @@ json.dump(badge, open('_book/tests/coverage-badge.json', 'w'))"; \ printf "${YELLOW}[WARN] Missing $$name, skipping${RESET}\n"; \ fi; \ done; \ + if [ -n "$$GITHUB_REPOSITORY" ]; then \ + CF_REPO="$$GITHUB_REPOSITORY"; \ + else \ + CF_REPO=$$(git remote get-url origin 2>/dev/null | sed 's|.*github\.com[:/]||' | sed 's|\.git$$||'); \ + fi; \ + if [ -n "$$CF_REPO" ]; then \ + CF_URL="https://www.codefactor.io/repository/github/$$CF_REPO"; \ + HTTP_CODE=$$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "$$CF_URL" 2>/dev/null || echo "000"); \ + if [ "$$HTTP_CODE" = "200" ]; then \ + if [ $$first -eq 0 ]; then printf ",\n" >> _book/links.json; fi; \ + printf " \"CodeFactor\": \"$$CF_URL\"" >> _book/links.json; \ + printf "${BLUE}[INFO] Adding CodeFactor...${RESET}\n"; \ + else \ + printf "${YELLOW}[WARN] CodeFactor page not accessible (HTTP $$HTTP_CODE), skipping${RESET}\n"; \ + fi; \ + fi; \ printf "\n}\n" >> _book/links.json @printf "${BLUE}[INFO] Generated links.json:${RESET}\n" diff --git a/.rhiza/make.d/docs.mk b/.rhiza/make.d/docs.mk index ad445038..803a066a 100644 --- a/.rhiza/make.d/docs.mk +++ b/.rhiza/make.d/docs.mk @@ -74,7 +74,7 @@ mkdocs-build:: install-uv ## build MkDocs documentation site @if [ -f "$(MKDOCS_CONFIG)" ]; then \ rm -rf "$(MKDOCS_OUTPUT)"; \ MKDOCS_OUTPUT_ABS="$$(pwd)/$(MKDOCS_OUTPUT)"; \ - ${UVX_BIN} --with mkdocs-material --with "pymdown-extensions>=10.0" mkdocs build \ + ${UVX_BIN} --with "mkdocs-material<10.0" --with "pymdown-extensions>=10.0" --with "mkdocs<2.0" mkdocs build \ -f "$(MKDOCS_CONFIG)" \ -d "$$MKDOCS_OUTPUT_ABS"; \ else \ @@ -85,7 +85,7 @@ mkdocs-build:: install-uv ## build MkDocs documentation site # Useful for local development and previewing changes. mkdocs-serve: install-uv ## serve MkDocs site with live reload @if [ -f "$(MKDOCS_CONFIG)" ]; then \ - ${UVX_BIN} --with mkdocs-material --with "pymdown-extensions>=10.0" mkdocs serve \ + ${UVX_BIN} --with "mkdocs-material<10.0" --with "pymdown-extensions>=10.0" --with "mkdocs<2.0" mkdocs serve \ -f "$(MKDOCS_CONFIG)"; \ else \ printf "${RED}[ERROR] $(MKDOCS_CONFIG) not found${RESET}\n"; \ diff --git a/.rhiza/make.d/marimo.mk b/.rhiza/make.d/marimo.mk index 25975735..6a1bd1ed 100644 --- a/.rhiza/make.d/marimo.mk +++ b/.rhiza/make.d/marimo.mk @@ -61,7 +61,7 @@ marimushka:: install-uv ## export Marimo notebooks to HTML OUTPUT_DIR="$$CURRENT_DIR/${MARIMUSHKA_OUTPUT}"; \ cd "${MARIMO_FOLDER}" && \ UVX_DIR=$$(dirname "$$(command -v uvx || echo "${INSTALL_DIR}/uvx")") && \ - "${UVX_BIN}" "marimushka>=0.1.9" export --notebooks "." --output "$$OUTPUT_DIR" --bin-path "$$UVX_DIR" && \ + "${UVX_BIN}" "marimushka>=0.3.3" export --notebooks "." --output "$$OUTPUT_DIR" --bin-path "$$UVX_DIR" && \ cd "$$CURRENT_DIR"; \ fi; \ fi diff --git a/.rhiza/make.d/quality.mk b/.rhiza/make.d/quality.mk index 62713090..1efd063b 100644 --- a/.rhiza/make.d/quality.mk +++ b/.rhiza/make.d/quality.mk @@ -2,7 +2,7 @@ # This file provides targets for code quality checks, linting, and formatting. # Declare phony targets (they don't produce files) -.PHONY: all deptry fmt +.PHONY: all deptry fmt todos ##@ Quality and Formatting all: fmt deptry test docs-coverage security typecheck rhiza-test ## run all CI targets locally @@ -22,3 +22,19 @@ deptry: install-uv ## Run deptry fmt: install-uv ## check the pre-commit hooks and the linting @${UVX_BIN} -p ${PYTHON_VERSION} pre-commit run --all-files + +todos: ## search and report all TODO/FIXME/HACK comments in the codebase + @printf "${BLUE}[INFO] Searching for TODO, FIXME, and HACK comments...${RESET}\n" + @printf "${BOLD}Found the following items:${RESET}\n\n" + @find . -type f \( -name "*.py" -o -name "*.mk" -o -name "*.sh" -o -name "*.md" -o -name "*.yml" -o -name "*.yaml" \) \ + -not -path "./.venv/*" \ + -not -path "./.git/*" \ + -not -path "./node_modules/*" \ + -not -path "./.tox/*" \ + -not -path "./build/*" \ + -not -path "./dist/*" \ + -print0 | xargs -0 grep -nHE "(TODO|FIXME|HACK):" 2>/dev/null | \ + grep -v "make todos" | \ + awk -F: '{ printf "${YELLOW}%s${RESET}:${GREEN}%s${RESET}: %s\n", $$1, $$2, substr($$0, index($$0,$$3)) }' || \ + printf "${GREEN}[SUCCESS] No TODO/FIXME/HACK comments found!${RESET}\n" + @printf "\n${BLUE}[INFO] Search complete.${RESET}\n" diff --git a/.rhiza/make.d/test.mk b/.rhiza/make.d/test.mk index 945af342..bf3b7165 100644 --- a/.rhiza/make.d/test.mk +++ b/.rhiza/make.d/test.mk @@ -4,7 +4,7 @@ # executing performance benchmarks. # Declare phony targets (they don't produce files) -.PHONY: test benchmark typecheck security docs-coverage hypothesis-test +.PHONY: test benchmark typecheck security docs-coverage hypothesis-test coverage-badge stress # Default directory for tests TESTS_FOLDER := tests @@ -20,7 +20,7 @@ COVERAGE_FAIL_UNDER ?= 90 # 2. Creates directories for HTML coverage and test reports. # 3. Invokes pytest via the local virtual environment. # 4. Generates terminal output, HTML coverage, JSON coverage, and HTML test reports. -test: install ## run all tests +test:: install ## run all tests @rm -rf _tests; if [ -z "$$(find ${TESTS_FOLDER} -name 'test_*.py' -o -name '*_test.py' 2>/dev/null)" ]; then \ @@ -31,6 +31,7 @@ test: install ## run all tests if [ -d ${SOURCE_FOLDER} ]; then \ ${UV_BIN} run pytest \ --ignore=${TESTS_FOLDER}/benchmarks \ + --ignore=${TESTS_FOLDER}/stress \ --cov=${SOURCE_FOLDER} \ --cov-report=term \ --cov-report=html:_tests/html-coverage \ @@ -41,6 +42,7 @@ test: install ## run all tests printf "${YELLOW}[WARN] Source folder ${SOURCE_FOLDER} not found, running tests without coverage${RESET}\n"; \ ${UV_BIN} run pytest \ --ignore=${TESTS_FOLDER}/benchmarks \ + --ignore=${TESTS_FOLDER}/stress \ --html=_tests/html-report/report.html; \ fi @@ -69,7 +71,7 @@ security: install ## run security scans (pip-audit and bandit) # 2. Executes benchmarks found in the benchmarks/ subfolder. # 3. Generates histograms and JSON results. # 4. Runs a post-analysis script to process the results. -benchmark: install ## run performance benchmarks +benchmark:: install ## run performance benchmarks @if [ -d "${TESTS_FOLDER}/benchmarks" ]; then \ printf "${BLUE}[INFO] Running performance benchmarks...${RESET}\n"; \ ${UV_BIN} pip install pytest-benchmark==5.2.3 pygal==3.1.0; \ @@ -98,18 +100,55 @@ docs-coverage: install ## check documentation coverage with interrogate # 1. Checks if hypothesis tests exist in the tests directory. # 2. Runs pytest with hypothesis-specific settings and statistics. # 3. Generates detailed hypothesis examples and statistics. -hypothesis-test: install ## run property-based tests with Hypothesis +hypothesis-test:: install ## run property-based tests with Hypothesis @if [ -z "$$(find ${TESTS_FOLDER} -name 'test_*.py' -o -name '*_test.py' 2>/dev/null)" ]; then \ printf "${YELLOW}[WARN] No test files found in ${TESTS_FOLDER}, skipping hypothesis tests.${RESET}\n"; \ exit 0; \ fi; \ printf "${BLUE}[INFO] Running Hypothesis property-based tests...${RESET}\n"; \ mkdir -p _tests/hypothesis; \ - ${UV_BIN} run pytest \ + PYTEST_HTML_TITLE="Hypothesis tests" ${UV_BIN} run pytest \ --ignore=${TESTS_FOLDER}/benchmarks \ -v \ --hypothesis-show-statistics \ --hypothesis-seed=0 \ -m "hypothesis or property" \ --tb=short \ - --html=_tests/hypothesis/report.html \ No newline at end of file + --html=_tests/hypothesis/report.html; \ + exit_code=$$?; \ + if [ $$exit_code -eq 5 ]; then \ + printf "${YELLOW}[WARN] No hypothesis/property tests collected, skipping.${RESET}\n"; \ + exit 0; \ + fi; \ + exit $$exit_code + +# The 'coverage-badge' target generates an SVG coverage badge from the JSON coverage report. +# 1. Checks if the coverage JSON file exists. +# 2. Creates the assets/ directory if needed. +# 3. Runs genbadge via uvx to produce the SVG badge. +coverage-badge: test ## generate coverage badge from _tests/coverage.json + @if [ ! -f _tests/coverage.json ]; then \ + printf "${RED}[ERROR] Coverage report not found at _tests/coverage.json, run 'make test' first.${RESET}\n"; \ + exit 1; \ + fi; \ + mkdir -p assets; \ + printf "${BLUE}[INFO] Generating coverage badge...${RESET}\n"; \ + ${UVX_BIN} genbadge coverage -i _tests/coverage.json -o assets/coverage-badge.svg; \ + printf "${GREEN}[SUCCESS] Coverage badge saved to assets/coverage-badge.svg${RESET}\n" + +# The 'stress' target runs stress/load tests. +# 1. Checks if stress tests exist in the tests/stress directory. +# 2. Runs pytest with the stress marker to execute only stress tests. +# 3. Generates an HTML report of stress test results. +stress:: install ## run stress/load tests + @if [ ! -d "${TESTS_FOLDER}/stress" ]; then \ + printf "${YELLOW}[WARN] Stress tests folder not found, skipping stress tests.${RESET}\n"; \ + exit 0; \ + fi; \ + printf "${BLUE}[INFO] Running stress/load tests...${RESET}\n"; \ + mkdir -p _tests/stress; \ + ${UV_BIN} run pytest \ + -v \ + -m stress \ + --tb=short \ + --html=_tests/stress/report.html diff --git a/.rhiza/requirements/tools.txt b/.rhiza/requirements/tools.txt index ed3e3cac..262ffc01 100644 --- a/.rhiza/requirements/tools.txt +++ b/.rhiza/requirements/tools.txt @@ -4,4 +4,4 @@ python-dotenv==1.2.1 # for now needed until rhiza-tools is finished typer==0.21.1 -ty==0.0.17 +ty==0.0.18 diff --git a/.rhiza/rhiza.mk b/.rhiza/rhiza.mk index 68dd38ac..de4a61b5 100644 --- a/.rhiza/rhiza.mk +++ b/.rhiza/rhiza.mk @@ -33,6 +33,7 @@ RESET := \033[0m readme \ summarise-sync \ sync \ + sync-experimental \ validate \ version-matrix @@ -76,7 +77,7 @@ endef export RHIZA_LOGO # Declare phony targets for Rhiza Core -.PHONY: print-logo sync validate readme pre-sync post-sync pre-validate post-validate +.PHONY: print-logo sync sync-experimental validate readme pre-sync post-sync pre-validate post-validate # Hook targets (double-colon rules allow multiple definitions) # Note: pre-install/post-install are defined in bootstrap.mk @@ -101,6 +102,16 @@ sync: pre-sync ## sync with template repository as defined in .rhiza/template.ym fi @$(MAKE) post-sync +sync-experimental: pre-sync ## sync with template repository using cruft-based merge (experimental, requires rhiza-cli >= 0.11.1-beta.1) + @printf "${YELLOW}[WARN] sync-experimental uses a beta version of rhiza-cli (>= 0.11.1-beta.1) and is not yet stable${RESET}\n" + @if git remote get-url origin 2>/dev/null | grep -iqE 'jebel-quant/rhiza(\.git)?$$'; then \ + printf "${BLUE}[INFO] Skipping sync-experimental in rhiza repository (no template.yml by design)${RESET}\n"; \ + else \ + $(MAKE) install-uv; \ + ${UVX_BIN} "rhiza>=0.11.1b1" sync .; \ + fi + @$(MAKE) post-sync + summarise-sync: install-uv ## summarise differences created by sync with template repository @if git remote get-url origin 2>/dev/null | grep -iqE 'jebel-quant/rhiza(\.git)?$$'; then \ printf "${BLUE}[INFO] Skipping summarise-sync in rhiza repository (no template.yml by design)${RESET}\n"; \ diff --git a/.rhiza/scripts/.gitkeep b/.rhiza/scripts/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/.rhiza/tests/README.md b/.rhiza/tests/README.md index 697bdad1..c020c084 100644 --- a/.rhiza/tests/README.md +++ b/.rhiza/tests/README.md @@ -34,6 +34,30 @@ Template sync, workflows, versioning, and content validation tests. These tests - `test_readme_validation.py` — README code block execution and validation - `test_docstrings.py` — Doctest validation across source modules +#### Skipping README code blocks with `+RHIZA_SKIP` + +By default, every `python` and `bash` code block in `README.md` is executed or +syntax-checked by `test_readme_validation.py`. To mark a block as intentionally +non-runnable (e.g. illustrative snippets or environment-specific commands), add +`+RHIZA_SKIP` to the opening fence line: + +~~~markdown +```python +RHIZA_SKIP +# This block will NOT be executed or syntax-checked +from my_env import some_function +some_function() +``` + +```bash +RHIZA_SKIP +# This bash block will NOT be syntax-checked +run-something --only-on-ci +``` +~~~ + +Markdown renderers (including GitHub) ignore everything after the first word on +a fence line, so the block still renders as a normal highlighted code block. +Blocks without `+RHIZA_SKIP` continue to be validated as before. + ### `utils/` Tests for utility code and test infrastructure. These tests validate the testing framework itself and utility scripts. @@ -44,6 +68,14 @@ Dependency validation tests. These tests ensure that project dependencies are co - `test_dependency_health.py` — Validates pyproject.toml and requirements files +### `stress/` +Stress tests that verify Rhiza's stability under heavy load. These tests execute Rhiza-specific operations under concurrent load and repeated execution to detect race conditions, resource leaks, and performance degradation. + +- `test_makefile_stress.py` — Makefile operations under concurrent/repeated load +- `test_git_stress.py` — Git operations under concurrent load + +See [stress/README.md](stress/README.md) for detailed documentation. + ## Running Tests ### Run all tests @@ -61,6 +93,19 @@ uv run pytest .rhiza/tests/integration/ uv run pytest .rhiza/tests/sync/ uv run pytest .rhiza/tests/utils/ uv run pytest .rhiza/tests/deps/ +uv run pytest .rhiza/tests/stress/ +``` + +### Run stress tests with custom parameters +```bash +# Run all stress tests (default: 100 iterations, 10 workers) +uv run pytest .rhiza/tests/stress/ -v + +# Run with fewer iterations (faster) +uv run pytest .rhiza/tests/stress/ -v --iterations=10 + +# Skip stress tests when running full test suite +uv run pytest .rhiza/tests/ -v -m "not stress" ``` ### Run a specific test file diff --git a/.rhiza/tests/api/conftest.py b/.rhiza/tests/api/conftest.py index 68fa2fba..798bc75b 100644 --- a/.rhiza/tests/api/conftest.py +++ b/.rhiza/tests/api/conftest.py @@ -7,8 +7,10 @@ - SPLIT_MAKEFILES: List of split Makefile paths Security Notes: -- S101 (assert usage): Asserts are appropriate in test code for validating conditions -- S603/S607 (subprocess usage): Any subprocess calls use controlled inputs in test environments +- S101 (assert usage): Asserts are used in pytest tests to validate conditions +- S603/S607 (subprocess usage): Any subprocess calls (via run_make) are for testing + Makefile targets in isolated environments with controlled inputs +- Test code operates in a controlled environment with trusted inputs """ from __future__ import annotations @@ -39,6 +41,7 @@ ".rhiza/make.d/presentation.mk", ".rhiza/make.d/github.mk", ".rhiza/make.d/agentic.mk", + ".rhiza/make.d/gh-aw.mk", ".rhiza/make.d/docker.mk", ".rhiza/make.d/docs.mk", ] @@ -70,7 +73,7 @@ def setup_tmp_makefile(logger, root, tmp_path: Path): else: # Create a minimal, deterministic .rhiza/.env for tests so they don't # depend on the developer's local configuration which may vary. - env_content = "SCRIPTS_FOLDER=.rhiza/scripts\nCUSTOM_SCRIPTS_FOLDER=.rhiza/customisations/scripts\n" + env_content = "CUSTOM_SCRIPTS_FOLDER=.rhiza/customisations/scripts\n" (tmp_path / ".rhiza" / ".env").write_text(env_content) logger.debug("Copied Makefile from %s to %s", root / "Makefile", tmp_path / "Makefile") diff --git a/.rhiza/tests/api/test_makefile_targets.py b/.rhiza/tests/api/test_makefile_targets.py index 302aea2e..fe95a37d 100644 --- a/.rhiza/tests/api/test_makefile_targets.py +++ b/.rhiza/tests/api/test_makefile_targets.py @@ -148,12 +148,6 @@ def test_uv_no_modify_path_is_exported(self, logger): out = strip_ansi(proc.stdout) assert "Value of UV_NO_MODIFY_PATH:\n1" in out - def test_script_folder_is_github_scripts(self, logger): - """`SCRIPTS_FOLDER` should point to `.rhiza/scripts`.""" - proc = run_make(logger, ["print-SCRIPTS_FOLDER"], dry_run=False) - out = strip_ansi(proc.stdout) - assert "Value of SCRIPTS_FOLDER:\n.rhiza/scripts" in out - def test_that_target_coverage_is_configurable(self, logger): """Test target should respond to COVERAGE_FAIL_UNDER variable.""" # Default case: ensure the flag is present @@ -164,6 +158,19 @@ def test_that_target_coverage_is_configurable(self, logger): proc_override = run_make(logger, ["test", "COVERAGE_FAIL_UNDER=42"]) assert "--cov-fail-under=42" in proc_override.stdout + def test_coverage_badge_target_dry_run(self, logger, tmp_path): + """Coverage-badge target should invoke genbadge via uvx in dry-run output.""" + # Create a mock coverage JSON file so the target proceeds past the guard + tests_dir = tmp_path / "_tests" + tests_dir.mkdir(exist_ok=True) + (tests_dir / "coverage.json").write_text("{}") + + proc = run_make(logger, ["coverage-badge"]) + out = proc.stdout + assert "genbadge coverage" in out + assert "_tests/coverage.json" in out + assert "assets/coverage-badge.svg" in out + class TestMakefileRootFixture: """Tests for root fixture usage in Makefile tests.""" @@ -207,6 +214,13 @@ def test_sync_target_skips_in_rhiza_repo(self, logger): # assert "[INFO] Skipping sync in rhiza repository" in out assert proc.returncode == 0 + def test_sync_experimental_target_skips_in_rhiza_repo(self, logger): + """Sync-experimental target should skip execution in rhiza repository.""" + setup_rhiza_git_repo() + + proc = run_make(logger, ["sync-experimental"], dry_run=False) + assert proc.returncode == 0 + class TestMakeBump: """Tests for the 'make bump' target.""" diff --git a/.rhiza/tests/conftest.py b/.rhiza/tests/conftest.py index af420d1a..419f6d69 100644 --- a/.rhiza/tests/conftest.py +++ b/.rhiza/tests/conftest.py @@ -7,14 +7,17 @@ Security Notes: - S101 (assert usage): Asserts are appropriate in test code for validating conditions -- S603/S607 (subprocess usage): Any subprocess calls use controlled inputs in test environments +- S603 (subprocess without shell=True): All subprocess calls use lists of known commands (git), + not user input, making them safe from shell injection +- S607 (subprocess with partial path): Using 'git' from PATH is acceptable in test fixtures + as the test environment is controlled and git is a required development dependency """ import logging import os import pathlib import shutil -import subprocess # nosec B404 +import subprocess # nosec B404 - subprocess module needed for git operations in test fixtures import sys import pytest diff --git a/.rhiza/tests/integration/test_book_targets.py b/.rhiza/tests/integration/test_book_targets.py index be875c0e..49cbb410 100644 --- a/.rhiza/tests/integration/test_book_targets.py +++ b/.rhiza/tests/integration/test_book_targets.py @@ -80,12 +80,25 @@ def test_book_folder(git_repo, book_makefile): targets = phony_line.split(":")[1].strip().split() all_targets.update(targets) - expected_targets = {"book", "marimushka", "mkdocs-build"} + expected_targets = {"book", "marimushka", "mkdocs-build", "test", "benchmark", "stress", "hypothesis-test", "docs"} assert expected_targets.issubset(all_targets), ( f"Expected phony targets to include {expected_targets}, got {all_targets}" ) +def test_book_noop_targets_defined(book_makefile): + """Test that book.mk defines no-op fallback targets for build resilience. + + These no-op double-colon rules ensure 'make book' succeeds even when + test.mk is not available or tests are not installed. + """ + content = book_makefile.read_text() + for target in ["test", "benchmark", "stress", "hypothesis-test", "docs"]: + assert f"{target}::" in content, ( + f"book.mk should define a no-op '::' fallback for '{target}' to ensure build resilience" + ) + + def test_book_without_logo_file(git_repo, book_makefile): """Test that book target works when LOGO_FILE is not set or empty. diff --git a/.rhiza/tests/integration/test_sbom.py b/.rhiza/tests/integration/test_sbom.py index 46b2a6fe..e406125f 100644 --- a/.rhiza/tests/integration/test_sbom.py +++ b/.rhiza/tests/integration/test_sbom.py @@ -20,6 +20,8 @@ def test_sbom_generation_json(git_repo, logger): "cyclonedx-bom>=7.0.0", "cyclonedx-py", "environment", + "--pyproject", + "pyproject.toml", "--of", "JSON", "-o", @@ -53,6 +55,13 @@ def test_sbom_generation_json(git_repo, logger): assert sbom_data["bomFormat"] == "CycloneDX", "SBOM has incorrect bomFormat" assert "components" in sbom_data, "SBOM missing components field" + # Verify primary component (metadata.component) is present for NTIA compliance + assert "metadata" in sbom_data, "SBOM missing metadata field" + assert "component" in sbom_data["metadata"], "SBOM missing primary component (metadata.component)" + primary = sbom_data["metadata"]["component"] + assert primary.get("name"), "Primary component missing name" + assert primary.get("version"), "Primary component missing version" + def test_sbom_generation_xml(git_repo, logger): """Test that SBOM generation works in XML format.""" @@ -64,6 +73,8 @@ def test_sbom_generation_xml(git_repo, logger): "cyclonedx-bom>=7.0.0", "cyclonedx-py", "environment", + "--pyproject", + "pyproject.toml", "--of", "XML", "-o", @@ -136,6 +147,8 @@ def test_sbom_command_syntax(git_repo, logger): "cyclonedx-bom>=7.0.0", "cyclonedx-py", "environment", + "--pyproject", + "pyproject.toml", "--of", "JSON", "-o", diff --git a/.rhiza/tests/sync/conftest.py b/.rhiza/tests/sync/conftest.py index 146ae689..3e8c8ddb 100644 --- a/.rhiza/tests/sync/conftest.py +++ b/.rhiza/tests/sync/conftest.py @@ -4,8 +4,10 @@ and content validation tests. Security Notes: -- S101 (assert usage): Asserts are appropriate in test code for validating conditions -- S603/S607 (subprocess usage): Any subprocess calls use controlled inputs in test environments +- S101 (assert usage): Asserts are used in pytest tests to validate conditions +- S603/S607 (subprocess usage): Any subprocess calls are for testing sync targets + in isolated environments with controlled inputs +- Test code operates in a controlled environment with trusted inputs """ from __future__ import annotations @@ -69,7 +71,7 @@ def setup_sync_env(logger, root, tmp_path: Path): shutil.copy(root / ".rhiza" / ".rhiza-version", tmp_path / ".rhiza" / ".rhiza-version") # Create a minimal, deterministic .rhiza/.env for tests - env_content = "SCRIPTS_FOLDER=.rhiza/scripts\nCUSTOM_SCRIPTS_FOLDER=.rhiza/customisations/scripts\n" + env_content = "CUSTOM_SCRIPTS_FOLDER=.rhiza/customisations/scripts\n" (tmp_path / ".rhiza" / ".env").write_text(env_content) logger.debug("Copied Makefile from %s to %s", root / "Makefile", tmp_path / "Makefile") diff --git a/.rhiza/tests/sync/test_readme_validation.py b/.rhiza/tests/sync/test_readme_validation.py index 29f1b0ef..821ce65d 100644 --- a/.rhiza/tests/sync/test_readme_validation.py +++ b/.rhiza/tests/sync/test_readme_validation.py @@ -13,26 +13,49 @@ import pytest -# Regex for Python code blocks -CODE_BLOCK = re.compile(r"```python\n(.*?)```", re.DOTALL) +# Regex for Python code blocks — captures optional flags (e.g. "+RHIZA_SKIP") and the code body. +CODE_BLOCK = re.compile(r"```python([^\n]*)\n(.*?)```", re.DOTALL) RESULT = re.compile(r"```result\n(.*?)```", re.DOTALL) -# Regex for Bash code blocks -BASH_BLOCK = re.compile(r"```bash\n(.*?)```", re.DOTALL) +# Regex for Bash code blocks — captures optional flags and the code body. +BASH_BLOCK = re.compile(r"```bash([^\n]*)\n(.*?)```", re.DOTALL) # Bash executable used for syntax checking; subprocess.run below is trusted (noqa: S603). BASH = "bash" +# Flag that marks a code block as intentionally excluded from readme tests. +# Usage: add the flag after the language identifier on the opening fence line, +# e.g. ```python +RHIZA_SKIP or ```bash +RHIZA_SKIP +SKIP_FLAG = "+RHIZA_SKIP" + + +def _should_skip(flags: str) -> bool: + """Return True if the fence flags string contains the +RHIZA_SKIP marker.""" + return SKIP_FLAG in flags + def test_readme_runs(logger, root): """Execute README code blocks and compare output to documented results.""" readme = root / "README.md" logger.info("Reading README from %s", readme) readme_text = readme.read_text(encoding="utf-8") - code_blocks = CODE_BLOCK.findall(readme_text) + all_code_blocks = CODE_BLOCK.findall(readme_text) result_blocks = RESULT.findall(readme_text) - logger.info("Found %d code block(s) and %d result block(s) in README", len(code_blocks), len(result_blocks)) + + code_blocks = [] + for i, (flags, code) in enumerate(all_code_blocks): + if _should_skip(flags): + logger.info("Skipping Python code block %d (%s flag)", i, SKIP_FLAG) + else: + code_blocks.append(code) + + logger.info( + "Found %d code block(s) (%d skipped) and %d result block(s) in README", + len(all_code_blocks), + len(all_code_blocks) - len(code_blocks), + len(result_blocks), + ) code = "".join(code_blocks) # merged code expected = "".join(result_blocks) # merged results @@ -71,12 +94,14 @@ def test_readme_is_readable(self, root): assert isinstance(content, str) def test_readme_code_is_syntactically_valid(self, root): - """Python code blocks in README should be syntactically valid.""" + """Python code blocks in README should be syntactically valid (skipped blocks are excluded).""" readme = root / "README.md" content = readme.read_text(encoding="utf-8") - code_blocks = re.findall(r"\`\`\`python\n(.*?)\`\`\`", content, re.DOTALL) + all_code_blocks = CODE_BLOCK.findall(content) - for i, code in enumerate(code_blocks): + for i, (flags, code) in enumerate(all_code_blocks): + if _should_skip(flags): + continue try: compile(code, f"", "exec") except SyntaxError as e: @@ -94,7 +119,11 @@ def test_bash_blocks_basic_syntax(self, root, logger): logger.info("Found %d bash code block(s) in README", len(bash_blocks)) - for i, code in enumerate(bash_blocks): + for i, (flags, code) in enumerate(bash_blocks): + if _should_skip(flags): + logger.info("Skipping bash block %d (%s flag)", i, SKIP_FLAG) + continue + # Skip directory tree representations and other non-executable blocks if any(marker in code for marker in ["├──", "└──", "│"]): logger.info("Skipping bash block %d (directory tree representation)", i) @@ -120,3 +149,49 @@ def test_bash_blocks_basic_syntax(self, root, logger): if result.returncode != 0: pytest.fail(f"Bash block {i} has syntax errors:\nCode:\n{code}\nError:\n{result.stderr}") + + +class TestSkipFlag: + """Tests for the +RHIZA_SKIP flag that allows individual README code blocks to be excluded.""" + + def test_should_skip_returns_true_for_skip_flag(self): + """+RHIZA_SKIP in flags string should cause _should_skip to return True.""" + assert _should_skip(" +RHIZA_SKIP") is True + assert _should_skip("+RHIZA_SKIP") is True + assert _should_skip(" +RHIZA_SKIP other-flag") is True + + def test_should_skip_returns_false_without_flag(self): + """Absence of +RHIZA_SKIP should cause _should_skip to return False.""" + assert _should_skip("") is False + assert _should_skip(" ") is False + assert _should_skip("other-flag") is False + + def test_python_block_with_skip_flag_is_excluded(self, tmp_path): + """A ```python +RHIZA_SKIP block should not appear in the list of blocks to execute.""" + readme = tmp_path / "README.md" + readme.write_text( + '```python +RHIZA_SKIP\nraise RuntimeError("should not run")\n```\n' + "```python\nprint('hello')\n```\n" + "```result\nhello\n```\n", + encoding="utf-8", + ) + content = readme.read_text(encoding="utf-8") + all_blocks = CODE_BLOCK.findall(content) + assert len(all_blocks) == 2 + executed = [code for flags, code in all_blocks if not _should_skip(flags)] + assert len(executed) == 1 + assert "raise RuntimeError" not in executed[0] + + def test_bash_block_with_skip_flag_is_excluded(self, tmp_path): + """A ```bash +RHIZA_SKIP block should not be syntax-checked.""" + readme = tmp_path / "README.md" + readme.write_text( + "```bash +RHIZA_SKIP\nnot-valid-bash @@@@\n```\n```bash\necho hello\n```\n", + encoding="utf-8", + ) + content = readme.read_text(encoding="utf-8") + all_blocks = BASH_BLOCK.findall(content) + assert len(all_blocks) == 2 + checked = [code for flags, code in all_blocks if not _should_skip(flags)] + assert len(checked) == 1 + assert "not-valid-bash" not in checked[0] diff --git a/.rhiza/tests/sync/test_rhiza_version.py b/.rhiza/tests/sync/test_rhiza_version.py index 4f139dd9..dbaf68dd 100644 --- a/.rhiza/tests/sync/test_rhiza_version.py +++ b/.rhiza/tests/sync/test_rhiza_version.py @@ -135,3 +135,46 @@ def test_workflow_summarise_command_format(self, logger): # The format should be: uvx "rhiza>=VERSION" summarise . assert 'uvx "rhiza>=' in out assert "summarise" in out + + +class TestSyncExperimental: + """Tests for the sync-experimental Makefile target.""" + + def test_sync_experimental_target_exists(self, logger): + """The sync-experimental target should be available in help.""" + proc = run_make(logger, ["help"]) + out = proc.stdout + assert "sync-experimental" in out + + def test_sync_experimental_dry_run(self, logger): + """Sync-experimental target should invoke rhiza sync in dry-run output.""" + proc = run_make(logger, ["sync-experimental"]) + out = proc.stdout + assert "uvx" in out + assert "rhiza" in out + assert "sync" in out + + def test_sync_experimental_uses_beta_version(self, logger): + """Sync-experimental target should pin to rhiza>=0.11.1b1.""" + proc = run_make(logger, ["sync-experimental"]) + out = proc.stdout + assert 'uvx "rhiza>=0.11.1b1" sync' in out + + def test_sync_experimental_skips_in_rhiza_repo(self, logger): + """Sync-experimental target should skip execution in rhiza repository.""" + proc = run_make(logger, ["sync-experimental"], dry_run=True) + assert proc.returncode == 0 + assert "Skipping sync-experimental in rhiza repository" in proc.stdout + + def test_sync_experimental_command_format(self, logger): + """Test that the sync-experimental uvx command format is correct.""" + proc = run_make(logger, ["sync-experimental"]) + out = proc.stdout + # The format should be: uvx "rhiza>=0.11.1b1" sync . + assert 'uvx "rhiza>=0.11.1b1" sync .' in out + + def test_sync_experimental_shows_beta_warning(self, logger): + """Sync-experimental target should display a beta warning.""" + proc = run_make(logger, ["sync-experimental"], dry_run=True) + out = proc.stdout + assert "sync-experimental uses a beta version of rhiza-cli" in out diff --git a/.rhiza/tests/test_utils.py b/.rhiza/tests/test_utils.py index d90db5ef..a7c1da4c 100644 --- a/.rhiza/tests/test_utils.py +++ b/.rhiza/tests/test_utils.py @@ -5,11 +5,18 @@ This file and its associated utilities flow down via a SYNC action from the jebel-quant/rhiza repository (https://github.com/jebel-quant/rhiza). + +Security Notes: +- S101 (assert usage): Asserts are used in test utilities to validate test setup conditions +- S603 (subprocess without shell=True): All subprocess calls use command lists with known + executables (git, make), not user input, preventing shell injection +- S607 (subprocess with partial path): Git and make are resolved from PATH via shutil.which() + with fallbacks, which is safe in controlled test environments """ import re import shutil -import subprocess # nosec B404 +import subprocess # nosec B404 - subprocess module needed for git/make operations in test utilities # Get absolute paths for executables to avoid S607 warnings GIT = shutil.which("git") or "/usr/bin/git" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d8603155..b7bebfc5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -55,6 +55,52 @@ following: - Browse the open issues, and look for the issues tagged "help wanted". +## Commit conventions + +We use [Conventional Commits](https://www.conventionalcommits.org/). Every commit message must have a +structured prefix so tooling can generate changelogs automatically. + +### Format + +``` +(): +``` + +`scope` is optional but encouraged when the change is limited to a specific area. + +### Types + +| Type | When to use | +|------------|--------------------------------------------------| +| `feat` | New feature or capability | +| `fix` | Bug fix | +| `docs` | Documentation only | +| `refactor` | Code change that is neither a fix nor a feature | +| `test` | Adding or updating tests | +| `ci` | CI / build system changes | +| `chore` | Maintenance tasks (deps, tooling, config) | +| `perf` | Performance improvement | +| `security` | Security fix or hardening | + +### Examples + +``` +feat(templates): add devcontainer template for Python 3.13 +fix: resolve path issue in bootstrap script +docs: update CONTRIBUTING with commit conventions +ci: cache uv dependencies in GitHub Actions +``` + +### Breaking changes + +Append `!` after the type/scope and add a `BREAKING CHANGE:` footer: + +``` +feat!: rename make target from `book` to `docs` + +BREAKING CHANGE: `make book` no longer exists; use `make docs`. +``` + ## Code style We use ruff to enforce our Python coding style. diff --git a/Makefile b/Makefile index 52d47948..9002adf8 100644 --- a/Makefile +++ b/Makefile @@ -4,9 +4,43 @@ DOCFORMAT=google DEFAULT_AI_MODEL=claude-sonnet-4.5 LOGO_FILE=.rhiza/assets/rhiza-logo.svg +GH_AW_ENGINE ?= copilot # Default AI engine for gh-aw workflows (copilot, claude, or codex) # Always include the Rhiza API (template-managed) include .rhiza/rhiza.mk # Optional: developer-local extensions (not committed) -include local.mk + +## Custom targets + +.PHONY: adr +adr: install-gh-aw ## Create a new Architecture Decision Record (ADR) using AI assistance + @echo "Creating a new ADR..." + @echo "This will trigger the adr-create workflow." + @echo "" + @read -p "Enter ADR title (e.g., 'Use PostgreSQL for data storage'): " title; \ + echo ""; \ + read -p "Enter brief context (optional, press Enter to skip): " context; \ + echo ""; \ + if [ -z "$$title" ]; then \ + echo "Error: Title is required"; \ + exit 1; \ + fi; \ + if [ -z "$$context" ]; then \ + gh workflow run adr-create.md -f title="$$title"; \ + else \ + gh workflow run adr-create.md -f title="$$title" -f context="$$context"; \ + fi; \ + echo ""; \ + echo "✅ ADR creation workflow triggered!"; \ + echo ""; \ + echo "The workflow will:"; \ + echo " 1. Generate the next ADR number"; \ + echo " 2. Create a comprehensive ADR document"; \ + echo " 3. Update the ADR index"; \ + echo " 4. Open a pull request for review"; \ + echo ""; \ + echo "Check workflow status: gh run list --workflow=adr-create.md"; \ + echo "View latest run: gh run view" + diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 2dd7bead..76d971fb 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -14,7 +14,6 @@ flowchart TB subgraph Core[".rhiza/ Core"] rhizamk[rhiza.mk
Core Logic] maked[make.d/*.mk
Extensions] - scripts[scripts/
Shell Scripts] reqs[requirements/
Dependencies] template[template-bundles.yml
Bundle Config] end @@ -36,7 +35,6 @@ flowchart TB make --> rhizamk local -.-> rhizamk rhizamk --> maked - rhizamk --> scripts maked --> reqs maked --> pyproject ci --> make @@ -166,7 +164,6 @@ flowchart TD rhiza --> rhizamk[rhiza.mk] rhiza --> maked[make.d/] - rhiza --> scripts[scripts/] rhiza --> reqs[requirements/] rhiza --> rtests[tests/] rhiza --> rdocs[docs/] @@ -239,7 +236,6 @@ flowchart TB docs_dir[docs/
7 MD files] templates_dir[templates/
minibook] assets_dir[assets/
Logo] - scripts_dir[scripts/
Utilities] end end diff --git a/docs/CUSTOMIZATION.md b/docs/CUSTOMIZATION.md index 49407d7a..a446e290 100644 --- a/docs/CUSTOMIZATION.md +++ b/docs/CUSTOMIZATION.md @@ -162,3 +162,5 @@ For more details on customizing the documentation, see [book/README.md](../book/ ## 📖 Complete Documentation For detailed information about extending and customizing the Makefile system, see [.rhiza/make.d/README.md](../.rhiza/make.d/README.md). + +For a tutorial walkthrough of these extension points — including the rule about template-managed files, the exclude mechanism, and forking the template for your organisation — see [rhiza-education Lesson 10: Customising Safely](https://github.com/Jebel-Quant/rhiza-education/blob/main/lessons/10-customizing-safely.md). diff --git a/docs/GLOSSARY.md b/docs/GLOSSARY.md index cc0e9368..f6960b7b 100644 --- a/docs/GLOSSARY.md +++ b/docs/GLOSSARY.md @@ -4,6 +4,12 @@ A comprehensive glossary of terms used in the Rhiza template system. ## Core Concepts +### rhiza (template repository) +The GitHub repository (`jebel-quant/rhiza`) that contains the curated set of configuration files, Makefile modules, CI/CD workflows, and other tooling files that downstream projects sync from. This is the *content* — the files you receive. See also: [rhiza-cli](#rhiza-cli). + +### rhiza-cli +A standalone Python package (published on PyPI as `rhiza-cli`) that provides the `rhiza` command-line interface. It is the *engine* that reads `.rhiza/template.yml` and performs operations such as `init`, `materialize`, `bump`, and `release`. Invoked via `uvx rhiza ...` without requiring a permanent installation. Versioned independently from the template repository. See also: [rhiza (template repository)](#rhiza-template-repository). + ### Living Templates A template approach where configuration files remain synchronized with an upstream source over time, as opposed to traditional "one-shot" template generators (like cookiecutter or copier) that generate files once and then disconnect from the source. diff --git a/docs/QUICK_REFERENCE.md b/docs/QUICK_REFERENCE.md index b7a9a80b..ebd09f98 100644 --- a/docs/QUICK_REFERENCE.md +++ b/docs/QUICK_REFERENCE.md @@ -37,6 +37,19 @@ A concise reference for common Rhiza operations. |---------|-------------| | `make sync` | Sync templates from upstream Rhiza | +## GitHub Agentic Workflows (gh-aw) + +| Command | Description | +|---------|-------------| +| `make install-gh-aw` | Install the gh-aw CLI extension | +| `make gh-aw-init` | Initialize repository for gh-aw | +| `make gh-aw-setup` | Guided setup for secrets and engine configuration | +| `make gh-aw-compile` | Compile workflow `.md` files into `.lock.yml` GitHub Actions | +| `make gh-aw-validate` | Validate that `.lock.yml` files are up-to-date | +| `make gh-aw-status` | Show status of all agentic workflows | +| `make gh-aw-run WORKFLOW=` | Run a specific agentic workflow locally | +| `make gh-aw-logs` | Show logs for recent agentic workflow runs | + ## Running Tests ```bash diff --git a/docs/SECURITY.md b/docs/SECURITY.md index e2c57187..b7dc3779 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -6,8 +6,9 @@ We actively support the following versions with security updates: | Version | Supported | | ------- | ------------------ | -| 0.6.x | :white_check_mark: | -| < 0.6 | :x: | +| 0.8.x | :white_check_mark: | +| 0.7.x | :white_check_mark: | +| < 0.7 | :x: | ## Reporting a Vulnerability diff --git a/docs/TESTS.md b/docs/TESTS.md index a058b8e1..7ca50e43 100644 --- a/docs/TESTS.md +++ b/docs/TESTS.md @@ -9,6 +9,35 @@ Rhiza now includes two additional types of testing: 1. **Property-Based Testing** (using Hypothesis) - Tests that verify properties hold across a wide range of generated inputs 2. **Load/Stress Testing** (using pytest-benchmark) - Tests that measure performance and verify stability under load +## README Code Block Testing + +The test file `.rhiza/tests/sync/test_readme_validation.py` automatically executes every `python` +code block in `README.md` and checks that its output matches the adjacent ` ```result ` block. It +also syntax-checks every `bash` block using `bash -n`. + +### Skipping individual blocks with `+RHIZA_SKIP` + +To exclude a specific code block from being executed or syntax-checked, append `+RHIZA_SKIP` to +the opening fence line: + +~~~markdown +```python +RHIZA_SKIP +# This block will NOT be executed or syntax-checked by the readme tests. +# Use it for illustrative examples, environment-specific code, or incomplete snippets. +from my_env import some_function +some_function() +``` + +```bash +RHIZA_SKIP +# This bash block will NOT be syntax-checked. +run-something --only-on-ci +``` +~~~ + +Markdown renderers (including GitHub) ignore everything after the first word on a fence line, so +the block still renders as a normal syntax-highlighted code block. All blocks that do **not** carry +`+RHIZA_SKIP` continue to be validated as before. + ## Property-Based Testing Property-based tests use the [Hypothesis](https://hypothesis.readthedocs.io/) library to automatically generate test cases that verify certain properties always hold true. diff --git a/renovate.json b/renovate.json deleted file mode 100644 index b066033a..00000000 --- a/renovate.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "extends": [ - "config:recommended", - ":enablePreCommit", - ":automergeMinor", - ":dependencyDashboard", - ":maintainLockFilesWeekly", - ":semanticCommits", - ":pinDevDependencies" - ], - "enabledManagers": [ - "pep621", - "pre-commit", - "github-actions", - "gitlabci", - "devcontainer", - "dockerfile", - "custom.regex" - ], - "timezone": "Asia/Dubai", - "schedule": [ - "before 3am" - ], - "packageRules": [ - { - "matchManagers": [ - "pep621" - ], - "matchPackageNames": [ - "python" - ], - "enabled": false - }, - { - "matchManagers": [ - "custom.regex" - ], - "matchPackageNames": [ - "jebel-quant/rhiza" - ], - "automerge": false - } - ], - "customManagers": [ - { - "customType": "regex", - "managerFilePatterns": [ - "/(^|/)\\.rhiza/template\\.yml$/" - ], - "matchStrings": [ - "(?:repository|template-repository):\\s*\"(?[^\"]+)\"[\\s\\S]*?(?:ref|template-branch):\\s*\"(?[^\"]+)\"" - ], - "datasourceTemplate": "github-releases" - } - ] -} diff --git a/tests/benchmarks/conftest.py b/tests/benchmarks/conftest.py index 3566788c..ede56bdb 100644 --- a/tests/benchmarks/conftest.py +++ b/tests/benchmarks/conftest.py @@ -4,6 +4,11 @@ for your benchmark tests. Security Notes: -- S101 (assert usage): Asserts are appropriate in test code for validating conditions -- S603/S607 (subprocess usage): Any subprocess calls use controlled inputs in test environments +- S101 (assert usage): Asserts are the standard way to validate test conditions in pytest. + They provide clear test failure messages and are expected in test code. """ + + +def pytest_html_report_title(report): + """Set the HTML report title.""" + report.title = "Benchmark Tests" diff --git a/tests/benchmarks/test_benchmarks.py b/tests/benchmarks/test_benchmarks.py index e7a6f0f4..eb7db177 100644 --- a/tests/benchmarks/test_benchmarks.py +++ b/tests/benchmarks/test_benchmarks.py @@ -1,8 +1,10 @@ -"""Example benchmark tests. +"""Blueprint benchmark tests for downstream repositories. -This file contains simple example benchmark tests that demonstrate -how to use pytest-benchmark. These are placeholder tests that you -should replace with your own meaningful benchmarks. +This file contains example benchmark tests that demonstrate how to use +pytest-benchmark. These are **placeholder tests you should replace** with +your own meaningful benchmarks. + +The Rhiza project's own tests live in ``.rhiza/tests/``. Uses pytest-benchmark to measure and compare execution times. """