From 32c126101f93c49632e89fe1f4261ac08f4fdd5e Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Tue, 10 Mar 2026 14:56:50 +0400 Subject: [PATCH 1/9] Delete Rhiza and code quality configuration files --- .github/workflows/rhiza_sync.yml | 100 ------------------------ .rhiza/template.yml | 13 ---- SECURITY.md | 100 ------------------------ renovate.json | 57 -------------- ruff.toml | 130 ------------------------------- 5 files changed, 400 deletions(-) delete mode 100644 .github/workflows/rhiza_sync.yml delete mode 100644 .rhiza/template.yml delete mode 100644 SECURITY.md delete mode 100644 renovate.json delete mode 100644 ruff.toml diff --git a/.github/workflows/rhiza_sync.yml b/.github/workflows/rhiza_sync.yml deleted file mode 100644 index 5206a076..00000000 --- a/.github/workflows/rhiza_sync.yml +++ /dev/null @@ -1,100 +0,0 @@ -name: (RHIZA) SYNC -# This workflow synchronizes the repository with its template. -# IMPORTANT: When workflow files (.github/workflows/rhiza_*.yml) are modified, -# a Personal Access Token (PAT) with 'workflow' scope is required. -# The PAT_TOKEN secret must be set in repository secrets. -# See .github/rhiza/TOKEN_SETUP.md for setup instructions. - -permissions: - contents: write - pull-requests: write - -on: - workflow_dispatch: - inputs: - create-pr: - description: "Create a pull request" - type: boolean - default: true - schedule: - - cron: '0 0 * * 1' # Weekly on Monday - -jobs: - sync: - if: ${{ github.repository != 'jebel-quant/rhiza' }} - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v6.0.2 - with: - token: ${{ secrets.PAT_TOKEN || github.token }} - fetch-depth: 0 - - - name: Define sync branch name - id: branch - run: | - echo "name=rhiza/${{ github.run_id }}" >> "$GITHUB_OUTPUT" - - - name: Check PAT_TOKEN configuration - shell: bash - env: - PAT_TOKEN: ${{ secrets.PAT_TOKEN }} - run: | - if [ -z "$PAT_TOKEN" ]; then - echo "::warning::PAT_TOKEN secret is not configured." - echo "::warning::If this sync modifies workflow files, the push will fail." - echo "::warning::See .github/TOKEN_SETUP.md for setup instructions." - else - echo "βœ“ PAT_TOKEN is configured." - fi - - - name: Install uv - uses: astral-sh/setup-uv@v7.3.1 - - - name: Get Rhiza version - id: rhiza-version - run: | - VERSION=$(cat .rhiza/.rhiza-version 2>/dev/null || echo "0.9.0") - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - - - name: Sync template - id: sync - shell: bash - run: | - set -euo pipefail - - RHIZA_VERSION="${{ steps.rhiza-version.outputs.version }}" - - uvx "rhiza>=${RHIZA_VERSION}" sync . - - git add -A - - if git diff --cached --quiet; then - echo "changes_detected=false" >> "$GITHUB_OUTPUT" - exit 0 - fi - - echo "changes_detected=true" >> "$GITHUB_OUTPUT" - - # Generate PR description based on staged changes - uvx "rhiza>=${RHIZA_VERSION}" summarise --output "${RUNNER_TEMP}/pr-description.md" - - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git config --global url."https://x-access-token:${{ secrets.PAT_TOKEN }}@github.com/".insteadOf "https://github.com/" - - git commit -m "chore: Update via rhiza" - - - name: Create pull request - if: > - (github.event_name == 'schedule' || inputs.create-pr == true) - && steps.sync.outputs.changes_detected == 'true' - uses: peter-evans/create-pull-request@v8.1.0 - with: - token: ${{ secrets.PAT_TOKEN || github.token }} - base: ${{ github.event.repository.default_branch }} - branch: ${{ steps.branch.outputs.name }} - delete-branch: true - title: "chore: Sync with rhiza" - body-path: ${{ runner.temp }}/pr-description.md \ No newline at end of file diff --git a/.rhiza/template.yml b/.rhiza/template.yml deleted file mode 100644 index a0a651da..00000000 --- a/.rhiza/template.yml +++ /dev/null @@ -1,13 +0,0 @@ -template-repository: "jebel-quant/rhiza" -template-branch: "v0.8.6" - -templates: - - core - - github - - legal - - book - - tests - - renovate - -exclude: - - .github/workflows/rhiza_benchmarks.yml diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index c6785594..00000000 --- a/SECURITY.md +++ /dev/null @@ -1,100 +0,0 @@ -# Security Policy - -## Supported Versions - -We actively support the following versions with security updates: - -| Version | Supported | -| ------- | ------------------ | -| 0.8.x | :white_check_mark: | -| 0.7.x | :white_check_mark: | -| < 0.7 | :x: | - -## Reporting a Vulnerability - -We take security vulnerabilities seriously. If you discover a security issue, please report it responsibly. - -### How to Report - -**Do NOT report security vulnerabilities through public GitHub issues.** - -Instead, please report them via one of the following methods: - -1. **GitHub Security Advisories** (Preferred) - - Go to the [Security Advisories](https://github.com/jebel-quant/rhiza/security/advisories) page - - Click "New draft security advisory" - - Fill in the details and submit - -2. **Email** - - Send details to the repository maintainers - - Include "SECURITY" in the subject line - -### What to Include - -Please include the following information in your report: - -- **Description**: A clear description of the vulnerability -- **Impact**: The potential impact of the vulnerability -- **Steps to Reproduce**: Detailed steps to reproduce the issue -- **Affected Versions**: Which versions are affected -- **Suggested Fix**: If you have one (optional) - -### What to Expect - -- **Acknowledgment**: We will acknowledge receipt within 48 hours -- **Initial Assessment**: We will provide an initial assessment within 7 days -- **Resolution Timeline**: We aim to resolve critical issues within 30 days -- **Credit**: We will credit reporters in the security advisory (unless you prefer to remain anonymous) - -### Scope - -This security policy applies to: - -- The Rhiza template system and configuration files -- GitHub Actions workflows provided by this repository -- Shell scripts in `.rhiza/scripts/` -- Python utilities in `.rhiza/utils/` - -### Out of Scope - -The following are generally out of scope: - -- Vulnerabilities in upstream dependencies (report these to the respective projects) -- Issues that require physical access to a user's machine -- Social engineering attacks -- Denial of service attacks that require significant resources - -## Security Measures - -This project implements several security measures: - -### Code Scanning -- **CodeQL**: Automated code scanning for Python and GitHub Actions -- **Bandit**: Python security linter integrated in CI and pre-commit -- **pip-audit**: Dependency vulnerability scanning -- **Secret Scanning**: GitHub secret scanning enabled on this repository - -### Supply Chain Security -- **SLSA Provenance**: Build attestations for release artifacts (public repositories only) -- **Locked Dependencies**: `uv.lock` ensures reproducible builds -- **Dependabot**: Automated dependency updates with security patches (version and security updates) -- **Renovate**: Additional automated dependency update management - -### Release Security -- **OIDC Publishing**: PyPI trusted publishing without stored credentials -- **Signed Commits**: GPG signing supported for releases -- **Tag Protection**: Releases require version tag validation - -## Security Best Practices for Users - -When using Rhiza templates in your projects: - -1. **Keep Updated**: Regularly sync with upstream templates -2. **Review Changes**: Review template sync PRs before merging -3. **Enable Security Features**: Enable CodeQL, secret scanning, and Dependabot in your repositories -4. **Use Locked Dependencies**: Always commit `uv.lock` for reproducible builds -5. **Configure Branch Protection**: Require PR reviews and status checks - -## Acknowledgments - -We thank the security researchers and community members who help keep Rhiza secure. diff --git a/renovate.json b/renovate.json deleted file mode 100644 index 1be6982f..00000000 --- a/renovate.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "extends": [ - "config:recommended", - ":enablePreCommit", - ":automergeMinor", - ":dependencyDashboard", - ":maintainLockFilesWeekly", - ":semanticCommits", - ":pinDevDependencies" - ], - "rebaseWhen": "behind-base-branch", - "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/ruff.toml b/ruff.toml deleted file mode 100644 index e5b39109..00000000 --- a/ruff.toml +++ /dev/null @@ -1,130 +0,0 @@ -# This file is part of the jebel-quant/rhiza repository -# (https://github.com/jebel-quant/rhiza). -# -# Maximum line length for the entire project -line-length = 120 -# Target Python version -target-version = "py311" - -# Exclude directories with Jinja template variables in their names -exclude = ["**/[{][{]*/", "**/*[}][}]*/"] - -[lint] -# Available rule sets in Ruff: -# A: flake8-builtins - Check for python builtins being used as variables or parameters -# B: flake8-bugbear - Find likely bugs and design problems -# C4: flake8-comprehensions - Helps write better list/set/dict comprehensions -# D: pydocstyle - Check docstring style -# E: pycodestyle errors - PEP 8 style guide -# ERA: eradicate - Find commented out code -# F: pyflakes - Detect logical errors -# I: isort - Sort imports -# N: pep8-naming - Check PEP 8 naming conventions -# PT: flake8-pytest-style - Check pytest best practices -# RUF: Ruff-specific rules -# S: flake8-bandit - Find security issues -# SIM: flake8-simplify - Simplify code -# T10: flake8-debugger - Check for debugger imports and calls -# UP: pyupgrade - Upgrade syntax for newer Python -# W: pycodestyle warnings - PEP 8 style guide warnings -# ANN: flake8-annotations - Type annotation checks -# ARG: flake8-unused-arguments - Unused arguments -# BLE: flake8-blind-except - Check for blind except statements -# COM: flake8-commas - Trailing comma enforcement -# DTZ: flake8-datetimez - Ensure timezone-aware datetime objects -# EM: flake8-errmsg - Check error message strings -# FBT: flake8-boolean-trap - Boolean argument checks -# ICN: flake8-import-conventions - Import convention enforcement -# ISC: flake8-implicit-str-concat - Implicit string concatenation -# NPY: NumPy-specific rules -# PD: pandas-specific rules -# PGH: pygrep-hooks - Grep-based checks -# PIE: flake8-pie - Miscellaneous rules -# PL: Pylint rules -# Q: flake8-quotes - Quotation style enforcement -# RSE: flake8-raise - Raise statement checks -# RET: flake8-return - Return statement checks -# SLF: flake8-self - Check for self references -# TCH: flake8-type-checking - Type checking imports -# TID: flake8-tidy-imports - Import tidying -# TRY: flake8-try-except-raise - Try/except/raise checks -# YTT: flake8-2020 - Python 2020+ compatibility - -select = [ - "D", # pydocstyle - Check docstring style - "E", # pycodestyle errors - PEP 8 style guide - "F", # pyflakes - Detect logical errors - "I", # isort - Sort imports - "N", # pep8-naming - Check PEP 8 naming conventions - "W", # pycodestyle warnings - PEP 8 style guide warnings - "UP", # pyupgrade - Upgrade syntax for newer Python -] - -# Ensure docstrings are required for magic methods (e.g., __init__) and -# private/underscored modules by explicitly selecting the relevant pydocstyle -# rules in addition to the D set. This makes the intent clear and future-proof -# if the default pydocstyle selection changes. -extend-select = [ - "D105", # pydocstyle - Require docstrings for magic methods - "D107", # pydocstyle - Require docstrings for __init__ - "B", # flake8-bugbear - Find likely bugs and design problems - "C4", # flake8-comprehensions - Better list/set/dict comprehensions - "SIM", # flake8-simplify - Simplify code - "PT", # flake8-pytest-style - Check pytest best practices - "RUF", # Ruff-specific rules - "S", # flake8-bandit - Find security issues - #"ERA", # eradicate - Find commented out code - #"T10", # flake8-debugger - Check for debugger imports and calls - "TRY", # flake8-try-except-raise - Try/except/raise checks - "ICN", # flake8-import-conventions - Import convention enforcement - #"PIE", # flake8-pie - Miscellaneous rules - #"PL", # Pylint rules -] - -# Resolve incompatible pydocstyle rules: prefer D211 and D212 over D203 and D213 -ignore = [ - "D203", # one-blank-line-before-class (conflicts with D211) - "D213", # multi-line-summary-second-line (conflicts with D212) -] - -[lint.pydocstyle] -convention = "google" - -# Formatting configuration -[format] -# Use double quotes for strings -quote-style = "double" -# Use spaces for indentation -indent-style = "space" -# Automatically detect and use the appropriate line ending -line-ending = "auto" - -# File-specific rule exceptions -[lint.per-file-ignores] -# Test files - allow assert statements and subprocess calls for testing -"**/tests/**/*.py" = [ - "S101", # Allow assert statements in tests - "S603", # Allow subprocess calls without shell=False check - "S607", # Allow starting processes with partial paths in tests - "PLW1510", # Allow subprocess without explicit check parameter -] -"tests/**/*.py" = [ - "ERA001", # Allow commented out code in project tests - "PLR2004", # Allow magic values in project tests - "RUF002", # Allow ambiguous unicode in project tests - "RUF012", # Allow mutable class attributes in project tests -] -# Marimo notebooks - allow flexible coding patterns for interactive exploration -"**/marimo/**/*.py" = [ - "N803", # Allow non-lowercase variable names in notebooks - "S101", # Allow assert statements in notebooks - "PLC0415", # Allow imports not at top-level in notebooks - "B018", # Allow useless expressions in notebooks - "RUF001", # Allow ambiguous unicode in notebooks - "RUF002", # Allow ambiguous unicode in notebooks -] -# Internal utility scripts - specific exceptions for internal tooling -".rhiza/utils/*.py" = [ - "PLW2901", # Allow loop variable overwriting in utility scripts - "TRY003", # Allow long exception messages in utility scripts -] From 6b421cfae3a64876b5e569bf7763864d36e55568 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Tue, 10 Mar 2026 14:58:50 +0400 Subject: [PATCH 2/9] Delete .rhiza/make.d/docs.mk and related documentation targets from Makefile --- .rhiza/make.d/docs.mk | 96 ------------------------------------------- Makefile | 33 --------------- 2 files changed, 129 deletions(-) delete mode 100644 .rhiza/make.d/docs.mk diff --git a/.rhiza/make.d/docs.mk b/.rhiza/make.d/docs.mk deleted file mode 100644 index 803a066a..00000000 --- a/.rhiza/make.d/docs.mk +++ /dev/null @@ -1,96 +0,0 @@ -## docs.mk - Documentation generation targets -# This file is included by the main Makefile. -# It provides targets for generating API documentation using pdoc -# and building/serving MkDocs documentation sites. - -# Declare phony targets (they don't produce files) -.PHONY: docs mkdocs mkdocs-serve mkdocs-build - -# Default output directory for MkDocs (HTML site) -MKDOCS_OUTPUT ?= _mkdocs - -# MkDocs config file location -MKDOCS_CONFIG ?= docs/mkdocs.yml - -# Default pdoc template directory (can be overridden) -PDOC_TEMPLATE_DIR ?= book/pdoc-templates - -##@ Documentation - -# The 'docs' target generates API documentation using pdoc. -# 1. Identifies Python packages within the source folder. -# 2. Detects the docformat (google, numpy, or sphinx) from ruff.toml or defaults to google. -# 3. Installs pdoc and generates HTML documentation in _pdoc. -docs:: install ## create documentation with pdoc - # Clean up previous docs - rm -rf _pdoc; - - @if [ -d "${SOURCE_FOLDER}" ]; then \ - PKGS=""; for d in "${SOURCE_FOLDER}"/*; do [ -d "$$d" ] && PKGS="$$PKGS $$(basename "$$d")"; done; \ - if [ -z "$$PKGS" ]; then \ - printf "${YELLOW}[WARN] No packages found under ${SOURCE_FOLDER}, skipping docs${RESET}\n"; \ - else \ - TEMPLATE_ARG=""; \ - if [ -d "$(PDOC_TEMPLATE_DIR)" ]; then \ - TEMPLATE_ARG="-t $(PDOC_TEMPLATE_DIR)"; \ - printf "$(BLUE)[INFO] Using pdoc templates from $(PDOC_TEMPLATE_DIR)$(RESET)\n"; \ - fi; \ - DOCFORMAT="$(DOCFORMAT)"; \ - if [ -z "$$DOCFORMAT" ]; then \ - if [ -f "ruff.toml" ]; then \ - DOCFORMAT=$$(${UV_BIN} run python -c "import tomllib; print(tomllib.load(open('ruff.toml', 'rb')).get('lint', {}).get('pydocstyle', {}).get('convention', ''))"); \ - fi; \ - if [ -z "$$DOCFORMAT" ]; then \ - DOCFORMAT="google"; \ - fi; \ - printf "${BLUE}[INFO] Detected docformat: $$DOCFORMAT${RESET}\n"; \ - else \ - printf "${BLUE}[INFO] Using provided docformat: $$DOCFORMAT${RESET}\n"; \ - fi; \ - LOGO_ARG=""; \ - if [ -n "$(LOGO_FILE)" ]; then \ - if [ -f "$(LOGO_FILE)" ]; then \ - MIME=$$(file --mime-type -b "$(LOGO_FILE)"); \ - DATA=$$(base64 < "$(LOGO_FILE)" | tr -d '\n'); \ - LOGO_ARG="--logo data:$$MIME;base64,$$DATA"; \ - printf "${BLUE}[INFO] Embedding logo: $(LOGO_FILE)${RESET}\n"; \ - else \ - printf "${YELLOW}[WARN] Logo file $(LOGO_FILE) not found, skipping${RESET}\n"; \ - fi; \ - fi; \ - ${UV_BIN} pip install pdoc && \ - PYTHONPATH="${SOURCE_FOLDER}" ${UV_BIN} run pdoc --docformat $$DOCFORMAT --output-dir _pdoc $$TEMPLATE_ARG $$LOGO_ARG $$PKGS; \ - fi; \ - else \ - printf "${YELLOW}[WARN] Source folder ${SOURCE_FOLDER} not found, skipping docs${RESET}\n"; \ - fi - -# The 'mkdocs-build' target builds the MkDocs documentation site. -# 1. Checks if the mkdocs.yml config file exists. -# 2. Cleans up any previous output. -# 3. Builds the static site using mkdocs with material theme. -mkdocs-build:: install-uv ## build MkDocs documentation site - @printf "${BLUE}[INFO] Building MkDocs site...${RESET}\n" - @if [ -f "$(MKDOCS_CONFIG)" ]; then \ - rm -rf "$(MKDOCS_OUTPUT)"; \ - MKDOCS_OUTPUT_ABS="$$(pwd)/$(MKDOCS_OUTPUT)"; \ - ${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 \ - printf "${YELLOW}[WARN] $(MKDOCS_CONFIG) not found, skipping MkDocs build${RESET}\n"; \ - fi - -# The 'mkdocs-serve' target serves the documentation with live reload. -# 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<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"; \ - exit 1; \ - fi - -# Convenience alias -mkdocs: mkdocs-serve ## alias for mkdocs-serve diff --git a/Makefile b/Makefile index 9002adf8..e38af437 100644 --- a/Makefile +++ b/Makefile @@ -11,36 +11,3 @@ 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" - From 46aa9f1fa51b1c30f1f5c4b7f9749f386eb5b001 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Tue, 10 Mar 2026 15:36:53 +0400 Subject: [PATCH 3/9] fmt --- .rhiza/tests/api/conftest.py | 4 +- .rhiza/tests/api/test_github_targets.py | 8 +++- .rhiza/tests/api/test_makefile_api.py | 14 +++++- .rhiza/tests/api/test_makefile_targets.py | 8 +++- .rhiza/tests/conftest.py | 4 +- .rhiza/tests/deps/test_dependency_health.py | 16 +++++-- .rhiza/tests/integration/test_book_targets.py | 35 ++++++++++++--- .rhiza/tests/integration/test_marimushka.py | 8 +++- .../integration/test_notebook_execution.py | 8 +++- .rhiza/tests/integration/test_sbom.py | 4 +- .../tests/security/test_security_patterns.py | 21 ++++++--- .rhiza/tests/stress/test_git_stress.py | 44 ++++++++++++++----- .rhiza/tests/stress/test_makefile_stress.py | 41 ++++++++++++----- .rhiza/tests/structure/test_project_layout.py | 6 ++- .rhiza/tests/structure/test_requirements.py | 6 ++- .rhiza/tests/sync/conftest.py | 16 +++++-- .rhiza/tests/sync/test_docstrings.py | 35 +++++++++++---- .rhiza/tests/sync/test_readme_validation.py | 16 +++++-- .rhiza/tests/test_utils.py | 6 ++- tests/test_actions/test_all_actions.py | 24 +++++++--- tests/test_actions/test_book_action.py | 31 ++++++++++--- tests/test_actions/test_build_action.py | 36 ++++++++++++--- tests/test_actions/test_coverage_action.py | 34 +++++++++++--- tests/test_actions/test_deptry_action.py | 32 ++++++++++---- tests/test_actions/test_docker_action.py | 36 +++++++++++---- tests/test_actions/test_environment_action.py | 23 +++++++--- tests/test_actions/test_latex_action.py | 22 +++++++--- tests/test_actions/test_pdoc_action.py | 37 ++++++++++++---- tests/test_actions/test_pre_commit_action.py | 38 ++++++++++++---- tests/test_actions/test_tag_action.py | 32 ++++++++++---- tests/test_actions/test_test_action.py | 28 ++++++++---- 31 files changed, 518 insertions(+), 155 deletions(-) diff --git a/.rhiza/tests/api/conftest.py b/.rhiza/tests/api/conftest.py index 798bc75b..ce183a69 100644 --- a/.rhiza/tests/api/conftest.py +++ b/.rhiza/tests/api/conftest.py @@ -76,7 +76,9 @@ def setup_tmp_makefile(logger, root, tmp_path: Path): 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") + logger.debug( + "Copied Makefile from %s to %s", root / "Makefile", tmp_path / "Makefile" + ) # Copy split Makefiles if they exist (maintaining directory structure) for split_file in SPLIT_MAKEFILES: diff --git a/.rhiza/tests/api/test_github_targets.py b/.rhiza/tests/api/test_github_targets.py index c3b19410..464bcfdb 100644 --- a/.rhiza/tests/api/test_github_targets.py +++ b/.rhiza/tests/api/test_github_targets.py @@ -15,7 +15,13 @@ def test_gh_targets_exist(logger): result = run_make(logger, ["help"], dry_run=False) output = result.stdout - expected_targets = ["gh-install", "view-prs", "view-issues", "failed-workflows", "whoami"] + expected_targets = [ + "gh-install", + "view-prs", + "view-issues", + "failed-workflows", + "whoami", + ] for target in expected_targets: assert target in output, f"Target {target} not found in help output" diff --git a/.rhiza/tests/api/test_makefile_api.py b/.rhiza/tests/api/test_makefile_api.py index 80960063..144db326 100644 --- a/.rhiza/tests/api/test_makefile_api.py +++ b/.rhiza/tests/api/test_makefile_api.py @@ -71,8 +71,18 @@ def setup_api_env(logger, root, tmp_path: Path): # Initialize git repo for rhiza tools (required for sync/validate) subprocess.run([GIT, "init"], cwd=tmp_path, check=True, capture_output=True) # nosec # Configure git user for commits if needed (some rhiza checks might need commits) - subprocess.run([GIT, "config", "user.email", "you@example.com"], cwd=tmp_path, check=True, capture_output=True) # nosec - subprocess.run([GIT, "config", "user.name", "Rhiza Test"], cwd=tmp_path, check=True, capture_output=True) # nosec + subprocess.run( + [GIT, "config", "user.email", "you@example.com"], + cwd=tmp_path, + check=True, + capture_output=True, + ) # nosec + subprocess.run( + [GIT, "config", "user.name", "Rhiza Test"], + cwd=tmp_path, + check=True, + capture_output=True, + ) # nosec # Add origin remote to simulate being in the rhiza repo (triggers the skip logic in rhiza.mk) subprocess.run( [GIT, "remote", "add", "origin", "https://github.com/jebel-quant/rhiza.git"], diff --git a/.rhiza/tests/api/test_makefile_targets.py b/.rhiza/tests/api/test_makefile_targets.py index fe95a37d..25f148ec 100644 --- a/.rhiza/tests/api/test_makefile_targets.py +++ b/.rhiza/tests/api/test_makefile_targets.py @@ -272,7 +272,9 @@ def test_bump_execution(self, logger, mock_bin, tmp_path): uvx_bin = mock_bin / "uvx" # Run make bump with dry_run=False to actually execute the shell commands - result = run_make(logger, ["bump", f"UV_BIN={uv_bin}", f"UVX_BIN={uvx_bin}"], dry_run=False) + result = run_make( + logger, ["bump", f"UV_BIN={uv_bin}", f"UVX_BIN={uvx_bin}"], dry_run=False + ) # Verify that the mock tools were called assert "[MOCK] uvx rhiza-tools>=0.3.3 bump" in result.stdout @@ -295,7 +297,9 @@ def test_bump_no_pyproject(self, logger, mock_bin, tmp_path): uv_bin = mock_bin / "uv" uvx_bin = mock_bin / "uvx" - result = run_make(logger, ["bump", f"UV_BIN={uv_bin}", f"UVX_BIN={uvx_bin}"], dry_run=False) + result = run_make( + logger, ["bump", f"UV_BIN={uv_bin}", f"UVX_BIN={uvx_bin}"], dry_run=False + ) # Check for warning message assert "No pyproject.toml found, skipping bump" in result.stdout diff --git a/.rhiza/tests/conftest.py b/.rhiza/tests/conftest.py index 419f6d69..84210486 100644 --- a/.rhiza/tests/conftest.py +++ b/.rhiza/tests/conftest.py @@ -155,7 +155,9 @@ def git_repo(root, tmp_path, monkeypatch): remote_dir.mkdir() subprocess.run([GIT, "init", "--bare", str(remote_dir)], check=True) # nosec B603 # Ensure the remote's default HEAD points to master for predictable behavior - subprocess.run([GIT, "symbolic-ref", "HEAD", "refs/heads/master"], cwd=remote_dir, check=True) # nosec B603 + subprocess.run( + [GIT, "symbolic-ref", "HEAD", "refs/heads/master"], cwd=remote_dir, check=True + ) # nosec B603 # 2. Clone to local subprocess.run([GIT, "clone", str(remote_dir), str(local_dir)], check=True) # nosec B603 diff --git a/.rhiza/tests/deps/test_dependency_health.py b/.rhiza/tests/deps/test_dependency_health.py index e3de07dc..95e82813 100644 --- a/.rhiza/tests/deps/test_dependency_health.py +++ b/.rhiza/tests/deps/test_dependency_health.py @@ -13,7 +13,9 @@ def test_pyproject_has_requires_python(root): pyproject = tomllib.load(f) assert "project" in pyproject, "[project] section missing from pyproject.toml" - assert "requires-python" in pyproject["project"], "requires-python missing from [project] section" + assert "requires-python" in pyproject["project"], ( + "requires-python missing from [project] section" + ) requires_python = pyproject["project"]["requires-python"] assert isinstance(requires_python, str), "requires-python must be a string" @@ -90,11 +92,15 @@ def test_no_duplicate_packages_across_requirements(root): # Find duplicates (excluding allowed ones) duplicates = { - pkg: files for pkg, files in package_locations.items() if len(files) > 1 and pkg not in allowed_duplicates + pkg: files + for pkg, files in package_locations.items() + if len(files) > 1 and pkg not in allowed_duplicates } if duplicates: - duplicate_list = [f"{pkg} ({', '.join(files)})" for pkg, files in duplicates.items()] + duplicate_list = [ + f"{pkg} ({', '.join(files)})" for pkg, files in duplicates.items() + ] msg = f"Packages found in multiple requirements files: {', '.join(duplicate_list)}" raise AssertionError(msg) @@ -108,4 +114,6 @@ def test_dotenv_in_test_requirements(root): content = f.read().lower() # Check for python-dotenv (case-insensitive) - assert "python-dotenv" in content, "python-dotenv not found in tests.txt (required by test suite)" + assert "python-dotenv" in content, ( + "python-dotenv not found in tests.txt (required by test suite)" + ) diff --git a/.rhiza/tests/integration/test_book_targets.py b/.rhiza/tests/integration/test_book_targets.py index 49cbb410..e9ed4757 100644 --- a/.rhiza/tests/integration/test_book_targets.py +++ b/.rhiza/tests/integration/test_book_targets.py @@ -31,7 +31,9 @@ def test_no_book_folder(git_repo, book_makefile): # Targets are now always defined via .rhiza/make.d/ # Use dry-run to verify they exist and can be parsed for target in ["book", "docs", "marimushka"]: - result = subprocess.run([MAKE, "-n", target], cwd=git_repo, capture_output=True, text=True) # nosec + result = subprocess.run( + [MAKE, "-n", target], cwd=git_repo, capture_output=True, text=True + ) # nosec # Target should exist (not "no rule to make target") assert "no rule to make target" not in result.stderr.lower(), ( f"Target {target} should be defined in .rhiza/make.d/" @@ -58,7 +60,9 @@ def test_book_folder_but_no_mk(git_repo, book_makefile): # Targets are now always defined via .rhiza/make.d/ # Use dry-run to verify they exist and can be parsed for target in ["book", "docs", "marimushka"]: - result = subprocess.run([MAKE, "-n", target], cwd=git_repo, capture_output=True, text=True) # nosec + result = subprocess.run( + [MAKE, "-n", target], cwd=git_repo, capture_output=True, text=True + ) # nosec # Target should exist (not "no rule to make target") assert "no rule to make target" not in result.stderr.lower(), ( f"Target {target} should be defined in .rhiza/make.d/" @@ -70,7 +74,9 @@ def test_book_folder(git_repo, book_makefile): content = book_makefile.read_text() # get the list of phony targets from the Makefile - phony_targets = [line.strip() for line in content.splitlines() if line.startswith(".PHONY:")] + phony_targets = [ + line.strip() for line in content.splitlines() if line.startswith(".PHONY:") + ] if not phony_targets: pytest.skip("No .PHONY targets found in book.mk") @@ -80,7 +86,16 @@ 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", "test", "benchmark", "stress", "hypothesis-test", "docs"} + 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}" ) @@ -124,8 +139,12 @@ def test_book_without_logo_file(git_repo, book_makefile): makefile.write_text("\n".join(new_lines)) # Dry-run the book target - it should still be valid - result = subprocess.run([MAKE, "-n", "book"], cwd=git_repo, capture_output=True, text=True) # nosec - assert "no rule to make target" not in result.stderr.lower(), "book target should work without LOGO_FILE" + result = subprocess.run( + [MAKE, "-n", "book"], cwd=git_repo, capture_output=True, text=True + ) # nosec + assert "no rule to make target" not in result.stderr.lower(), ( + "book target should work without LOGO_FILE" + ) # Should not have errors about missing logo variable assert result.returncode == 0, f"Dry-run failed: {result.stderr}" @@ -159,5 +178,7 @@ def test_book_with_missing_logo_file(git_repo, book_makefile): makefile.write_text("\n".join(new_lines)) # Dry-run should still succeed - result = subprocess.run([MAKE, "-n", "book"], cwd=git_repo, capture_output=True, text=True) # nosec + result = subprocess.run( + [MAKE, "-n", "book"], cwd=git_repo, capture_output=True, text=True + ) # nosec assert result.returncode == 0, f"Dry-run failed with missing logo: {result.stderr}" diff --git a/.rhiza/tests/integration/test_marimushka.py b/.rhiza/tests/integration/test_marimushka.py index 6abdaa95..882eab11 100644 --- a/.rhiza/tests/integration/test_marimushka.py +++ b/.rhiza/tests/integration/test_marimushka.py @@ -59,7 +59,9 @@ def test_marimushka_target_success(git_repo): # Override UVX_BIN to use our mock marimushka CLI env["UVX_BIN"] = str(git_repo / "bin" / "marimushka") - result = subprocess.run([MAKE, "marimushka"], env=env, cwd=git_repo, capture_output=True, text=True) # nosec + result = subprocess.run( + [MAKE, "marimushka"], env=env, cwd=git_repo, capture_output=True, text=True + ) # nosec assert result.returncode == 0 assert "Exporting notebooks" in result.stdout @@ -87,7 +89,9 @@ def test_marimushka_no_python_files(git_repo): env["MARIMO_FOLDER"] = "book/marimo/notebooks" env["MARIMUSHKA_OUTPUT"] = "_marimushka" - result = subprocess.run([MAKE, "marimushka"], env=env, cwd=git_repo, capture_output=True, text=True) # nosec + result = subprocess.run( + [MAKE, "marimushka"], env=env, cwd=git_repo, capture_output=True, text=True + ) # nosec assert result.returncode == 0 assert (output_folder / "index.html").exists() diff --git a/.rhiza/tests/integration/test_notebook_execution.py b/.rhiza/tests/integration/test_notebook_execution.py index 12036f9c..47f764d5 100644 --- a/.rhiza/tests/integration/test_notebook_execution.py +++ b/.rhiza/tests/integration/test_notebook_execution.py @@ -57,7 +57,9 @@ def test_notebook_execution(notebook_path: Path): else: uvx_cmd = shutil.which("uvx") if uvx_cmd is None: - pytest.skip("uvx not found (neither ./bin/uvx nor uvx on PATH); skipping marimo notebook tests") + pytest.skip( + "uvx not found (neither ./bin/uvx nor uvx on PATH); skipping marimo notebook tests" + ) cmd = [ uvx_cmd, @@ -72,7 +74,9 @@ def test_notebook_execution(notebook_path: Path): "/dev/null", # We don't need the actual HTML output ] - result = subprocess.run(cmd, capture_output=True, text=True, cwd=notebook_path.parent) # nosec + result = subprocess.run( + cmd, capture_output=True, text=True, cwd=notebook_path.parent + ) # nosec # Ensure process exit code indicates success assert result.returncode == 0, ( diff --git a/.rhiza/tests/integration/test_sbom.py b/.rhiza/tests/integration/test_sbom.py index e406125f..295df852 100644 --- a/.rhiza/tests/integration/test_sbom.py +++ b/.rhiza/tests/integration/test_sbom.py @@ -57,7 +57,9 @@ def test_sbom_generation_json(git_repo, logger): # 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)" + 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" diff --git a/.rhiza/tests/security/test_security_patterns.py b/.rhiza/tests/security/test_security_patterns.py index 27cbc312..2362c387 100644 --- a/.rhiza/tests/security/test_security_patterns.py +++ b/.rhiza/tests/security/test_security_patterns.py @@ -64,7 +64,9 @@ def test_ruff_security_checks_enabled(self) -> None: content = ruff_config.read_text() # Check that "S" is in either select or extend-select - assert '"S"' in content or "'S'" in content, "Ruff security checks (S) should be enabled in ruff.toml" + assert '"S"' in content or "'S'" in content, ( + "Ruff security checks (S) should be enabled in ruff.toml" + ) def test_bandit_configured_in_precommit(self) -> None: """Verify that Bandit is configured in pre-commit hooks. @@ -78,7 +80,9 @@ def test_bandit_configured_in_precommit(self) -> None: assert precommit_config.exists(), ".pre-commit-config.yaml not found" content = precommit_config.read_text() - assert "bandit" in content.lower(), "Bandit should be configured in pre-commit hooks" + assert "bandit" in content.lower(), ( + "Bandit should be configured in pre-commit hooks" + ) def test_security_policy_exists(self) -> None: """Verify that a SECURITY.md file exists at the repository root. @@ -92,7 +96,9 @@ def test_security_policy_exists(self) -> None: github_security = repo_root / ".github" / "SECURITY.md" docs_security = repo_root / "docs" / "SECURITY.md" - assert root_security.exists() or github_security.exists() or docs_security.exists(), ( + assert ( + root_security.exists() or github_security.exists() or docs_security.exists() + ), ( "No SECURITY.md found. Create SECURITY.md in the repository root, " ".github/, or docs/ to publish a responsible disclosure policy." ) @@ -152,7 +158,12 @@ def test_test_security_exceptions_documented(self) -> None: content = conftest.read_text() # Check for security-related comments or docstrings has_security_docs = ( - "S101" in content or "S603" in content or "S607" in content or "security" in content.lower() + "S101" in content + or "S603" in content + or "S607" in content + or "security" in content.lower() ) - assert has_security_docs, f"{conftest} should document security exceptions (S101/S603/S607)" + assert has_security_docs, ( + f"{conftest} should document security exceptions (S101/S603/S607)" + ) diff --git a/.rhiza/tests/stress/test_git_stress.py b/.rhiza/tests/stress/test_git_stress.py index 573f9c43..f76cc022 100644 --- a/.rhiza/tests/stress/test_git_stress.py +++ b/.rhiza/tests/stress/test_git_stress.py @@ -35,12 +35,18 @@ def run_git_status(): ) return result.returncode == 0 - with concurrent.futures.ThreadPoolExecutor(max_workers=concurrent_workers) as executor: - futures = [executor.submit(run_git_status) for _ in range(concurrent_workers * 2)] + with concurrent.futures.ThreadPoolExecutor( + max_workers=concurrent_workers + ) as executor: + futures = [ + executor.submit(run_git_status) for _ in range(concurrent_workers * 2) + ] results = [f.result() for f in concurrent.futures.as_completed(futures)] success_rate = sum(results) / len(results) - assert success_rate == 1.0, f"Expected 100% success rate, got {success_rate * 100:.1f}%" + assert success_rate == 1.0, ( + f"Expected 100% success rate, got {success_rate * 100:.1f}%" + ) @pytest.mark.stress @@ -60,12 +66,16 @@ def run_git_log(): ) return result.returncode == 0 - with concurrent.futures.ThreadPoolExecutor(max_workers=concurrent_workers) as executor: + with concurrent.futures.ThreadPoolExecutor( + max_workers=concurrent_workers + ) as executor: futures = [executor.submit(run_git_log) for _ in range(concurrent_workers * 2)] results = [f.result() for f in concurrent.futures.as_completed(futures)] success_rate = sum(results) / len(results) - assert success_rate == 1.0, f"Expected 100% success rate, got {success_rate * 100:.1f}%" + assert success_rate == 1.0, ( + f"Expected 100% success rate, got {success_rate * 100:.1f}%" + ) @pytest.mark.stress @@ -94,7 +104,9 @@ def test_repeated_git_operations(root: Path, stress_iterations: int): results.append(result.returncode == 0) success_rate = sum(results) / len(results) - assert success_rate == 1.0, f"Expected 100% success rate, got {success_rate * 100:.1f}%" + assert success_rate == 1.0, ( + f"Expected 100% success rate, got {success_rate * 100:.1f}%" + ) @pytest.mark.stress @@ -114,12 +126,16 @@ def run_git_diff(): ) return result.returncode == 0 - with concurrent.futures.ThreadPoolExecutor(max_workers=concurrent_workers) as executor: + with concurrent.futures.ThreadPoolExecutor( + max_workers=concurrent_workers + ) as executor: futures = [executor.submit(run_git_diff) for _ in range(concurrent_workers * 2)] results = [f.result() for f in concurrent.futures.as_completed(futures)] success_rate = sum(results) / len(results) - assert success_rate == 1.0, f"Expected 100% success rate, got {success_rate * 100:.1f}%" + assert success_rate == 1.0, ( + f"Expected 100% success rate, got {success_rate * 100:.1f}%" + ) @pytest.mark.stress @@ -141,7 +157,9 @@ def test_rapid_git_rev_parse(root: Path, stress_iterations: int): results.append(result.returncode == 0 and len(result.stdout.strip()) == 40) success_rate = sum(results) / len(results) - assert success_rate == 1.0, f"Expected 100% success rate, got {success_rate * 100:.1f}%" + assert success_rate == 1.0, ( + f"Expected 100% success rate, got {success_rate * 100:.1f}%" + ) @pytest.mark.stress @@ -161,9 +179,13 @@ def run_git_show(): ) return result.returncode == 0 - with concurrent.futures.ThreadPoolExecutor(max_workers=concurrent_workers) as executor: + with concurrent.futures.ThreadPoolExecutor( + max_workers=concurrent_workers + ) as executor: futures = [executor.submit(run_git_show) for _ in range(concurrent_workers)] results = [f.result() for f in concurrent.futures.as_completed(futures)] success_rate = sum(results) / len(results) - assert success_rate == 1.0, f"Expected 100% success rate, got {success_rate * 100:.1f}%" + assert success_rate == 1.0, ( + f"Expected 100% success rate, got {success_rate * 100:.1f}%" + ) diff --git a/.rhiza/tests/stress/test_makefile_stress.py b/.rhiza/tests/stress/test_makefile_stress.py index d01ea7a3..201a25b8 100644 --- a/.rhiza/tests/stress/test_makefile_stress.py +++ b/.rhiza/tests/stress/test_makefile_stress.py @@ -35,12 +35,16 @@ def run_help(): ) return result.returncode == 0 and "Usage:" in result.stdout - with concurrent.futures.ThreadPoolExecutor(max_workers=concurrent_workers) as executor: + with concurrent.futures.ThreadPoolExecutor( + max_workers=concurrent_workers + ) as executor: futures = [executor.submit(run_help) for _ in range(concurrent_workers * 2)] results = [f.result() for f in concurrent.futures.as_completed(futures)] success_rate = sum(results) / len(results) - assert success_rate == 1.0, f"Expected 100% success rate, got {success_rate * 100:.1f}%" + assert success_rate == 1.0, ( + f"Expected 100% success rate, got {success_rate * 100:.1f}%" + ) @pytest.mark.stress @@ -62,7 +66,9 @@ def test_repeated_help_executions(root: Path, stress_iterations: int): results.append(result.returncode == 0 and "Usage:" in result.stdout) success_rate = sum(results) / len(results) - assert success_rate == 1.0, f"Expected 100% success rate, got {success_rate * 100:.1f}%" + assert success_rate == 1.0, ( + f"Expected 100% success rate, got {success_rate * 100:.1f}%" + ) @pytest.mark.stress @@ -86,12 +92,19 @@ def run_dry_run(target: str): # Test multiple targets concurrently targets = ["install", "test", "fmt", "clean"] * (concurrent_workers // 4 + 1) - with concurrent.futures.ThreadPoolExecutor(max_workers=concurrent_workers) as executor: - futures = [executor.submit(run_dry_run, target) for target in targets[: concurrent_workers * 2]] + with concurrent.futures.ThreadPoolExecutor( + max_workers=concurrent_workers + ) as executor: + futures = [ + executor.submit(run_dry_run, target) + for target in targets[: concurrent_workers * 2] + ] results = [f.result() for f in concurrent.futures.as_completed(futures)] success_rate = sum(results) / len(results) - assert success_rate == 1.0, f"Expected 100% success rate, got {success_rate * 100:.1f}%" + assert success_rate == 1.0, ( + f"Expected 100% success rate, got {success_rate * 100:.1f}%" + ) @pytest.mark.stress @@ -112,7 +125,9 @@ def test_rapid_makefile_parsing(root: Path, stress_iterations: int): results.append(result.returncode == 0) success_rate = sum(results) / len(results) - assert success_rate == 1.0, f"Expected 100% success rate, got {success_rate * 100:.1f}%" + assert success_rate == 1.0, ( + f"Expected 100% success rate, got {success_rate * 100:.1f}%" + ) @pytest.mark.stress @@ -137,9 +152,15 @@ def print_variable(var_name: str): # Repeat the variables to create enough work for concurrent execution variables = ["SHELL"] * concurrent_workers - with concurrent.futures.ThreadPoolExecutor(max_workers=concurrent_workers) as executor: + with concurrent.futures.ThreadPoolExecutor( + max_workers=concurrent_workers + ) as executor: futures = [executor.submit(print_variable, var) for var in variables] - results = [f.result(timeout=30) for f in concurrent.futures.as_completed(futures)] + results = [ + f.result(timeout=30) for f in concurrent.futures.as_completed(futures) + ] success_rate = sum(results) / len(results) - assert success_rate == 1.0, f"Expected 100% success rate, got {success_rate * 100:.1f}%" + assert success_rate == 1.0, ( + f"Expected 100% success rate, got {success_rate * 100:.1f}%" + ) diff --git a/.rhiza/tests/structure/test_project_layout.py b/.rhiza/tests/structure/test_project_layout.py index 1ceac5fb..61a0f104 100644 --- a/.rhiza/tests/structure/test_project_layout.py +++ b/.rhiza/tests/structure/test_project_layout.py @@ -22,7 +22,11 @@ def test_root_resolves_correctly_from_nested_location(self, root): def test_root_contains_expected_directories(self, root): """Root should contain all expected project directories.""" required_dirs = [".rhiza"] - optional_dirs = ["src", "tests", "book"] # src/ is optional (rhiza itself doesn't have one) + optional_dirs = [ + "src", + "tests", + "book", + ] # src/ is optional (rhiza itself doesn't have one) for dirname in required_dirs: assert (root / dirname).exists(), f"Required directory {dirname} not found" diff --git a/.rhiza/tests/structure/test_requirements.py b/.rhiza/tests/structure/test_requirements.py index 1bf9d045..e57c3e18 100644 --- a/.rhiza/tests/structure/test_requirements.py +++ b/.rhiza/tests/structure/test_requirements.py @@ -39,7 +39,11 @@ def test_requirements_files_not_empty(self, root): filepath = requirements_dir / filename content = filepath.read_text() # Filter out comments and empty lines - lines = [line.strip() for line in content.splitlines() if line.strip() and not line.strip().startswith("#")] + lines = [ + line.strip() + for line in content.splitlines() + if line.strip() and not line.strip().startswith("#") + ] assert len(lines) > 0, f"{filename} should contain at least one dependency" def test_readme_exists_in_requirements_folder(self, root): diff --git a/.rhiza/tests/sync/conftest.py b/.rhiza/tests/sync/conftest.py index 9bb3b2f7..9ef0b1e4 100644 --- a/.rhiza/tests/sync/conftest.py +++ b/.rhiza/tests/sync/conftest.py @@ -68,19 +68,27 @@ def setup_sync_env(logger, root, tmp_path: Path): # Copy .rhiza-version if it exists if (root / ".rhiza" / ".rhiza-version").exists(): - shutil.copy(root / ".rhiza" / ".rhiza-version", tmp_path / ".rhiza" / ".rhiza-version") + shutil.copy( + root / ".rhiza" / ".rhiza-version", tmp_path / ".rhiza" / ".rhiza-version" + ) # Create a minimal, deterministic .rhiza/.env for tests 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") + logger.debug( + "Copied Makefile from %s to %s", root / "Makefile", tmp_path / "Makefile" + ) # Create a minimal .rhiza/template.yml - (tmp_path / ".rhiza" / "template.yml").write_text("repository: Jebel-Quant/rhiza\nref: v0.7.1\n") + (tmp_path / ".rhiza" / "template.yml").write_text( + "repository: Jebel-Quant/rhiza\nref: v0.7.1\n" + ) # Sort out pyproject.toml - (tmp_path / "pyproject.toml").write_text('[project]\nname = "test-project"\nversion = "0.1.0"\n') + (tmp_path / "pyproject.toml").write_text( + '[project]\nname = "test-project"\nversion = "0.1.0"\n' + ) # Move into tmp directory for isolation old_cwd = Path.cwd() diff --git a/.rhiza/tests/sync/test_docstrings.py b/.rhiza/tests/sync/test_docstrings.py index 231e57b5..2df32c08 100644 --- a/.rhiza/tests/sync/test_docstrings.py +++ b/.rhiza/tests/sync/test_docstrings.py @@ -49,9 +49,13 @@ def _find_packages(src_path: Path): yield package_dir -def test_doctests(logger, root, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]): +def test_doctests( + logger, root, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +): """Run doctests for each package directory.""" - values = dotenv_values(root / RHIZA_ENV_PATH) if (root / RHIZA_ENV_PATH).exists() else {} + values = ( + dotenv_values(root / RHIZA_ENV_PATH) if (root / RHIZA_ENV_PATH).exists() else {} + ) source_folder = values.get("SOURCE_FOLDER", "src") src_path = root / source_folder @@ -76,7 +80,9 @@ def test_doctests(logger, root, monkeypatch: pytest.MonkeyPatch, capsys: pytest. logger.info("Discovered package: %s", package_name) try: modules = list(_iter_modules_from_path(logger, package_dir, src_path)) - logger.debug("%d module(s) found in package %s", len(modules), package_name) + logger.debug( + "%d module(s) found in package %s", len(modules), package_name + ) for module in modules: logger.debug("Running doctests for module: %s", module.__name__) @@ -85,7 +91,9 @@ def test_doctests(logger, root, monkeypatch: pytest.MonkeyPatch, capsys: pytest. results = doctest.testmod( module, verbose=False, - optionflags=(doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE), + optionflags=( + doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE + ), ) total_tests += results.attempted @@ -97,17 +105,28 @@ def test_doctests(logger, root, monkeypatch: pytest.MonkeyPatch, capsys: pytest. results.attempted, ) total_failures += results.failed - failed_modules.append((module.__name__, results.failed, results.attempted)) + failed_modules.append( + (module.__name__, results.failed, results.attempted) + ) else: - logger.debug("Doctests passed for %s (%d test(s))", module.__name__, results.attempted) + logger.debug( + "Doctests passed for %s (%d test(s))", + module.__name__, + results.attempted, + ) except ImportError as e: - warnings.warn(f"Could not import package {package_name}: {e}", stacklevel=2) + warnings.warn( + f"Could not import package {package_name}: {e}", stacklevel=2 + ) logger.warning("Could not import package %s: %s", package_name, e) continue if failed_modules: - formatted = "\n".join(f" {name}: {failed}/{attempted} failed" for name, failed, attempted in failed_modules) + formatted = "\n".join( + f" {name}: {failed}/{attempted} failed" + for name, failed, attempted in failed_modules + ) msg = ( f"Doctest summary: {total_tests} tests across {len(failed_modules)} module(s)\n" f"Failures: {total_failures}\n" diff --git a/.rhiza/tests/sync/test_readme_validation.py b/.rhiza/tests/sync/test_readme_validation.py index 821ce65d..b38b79a4 100644 --- a/.rhiza/tests/sync/test_readme_validation.py +++ b/.rhiza/tests/sync/test_readme_validation.py @@ -63,7 +63,9 @@ def test_readme_runs(logger, root): # Trust boundary: we execute Python snippets sourced from README.md in this repo. # The README is part of the trusted repository content and reviewed in PRs. logger.debug("Executing README code via %s -c ...", sys.executable) - result = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True, cwd=root) # nosec + result = subprocess.run( + [sys.executable, "-c", code], capture_output=True, text=True, cwd=root + ) # nosec stdout = result.stdout logger.debug("Execution finished with return code %d", result.returncode) @@ -71,8 +73,12 @@ def test_readme_runs(logger, root): logger.debug("Stderr from README code:\n%s", result.stderr) logger.debug("Stdout from README code:\n%s", stdout) - assert result.returncode == 0, f"README code exited with {result.returncode}. Stderr:\n{result.stderr}" - logger.info("README code executed successfully; comparing output to expected result") + assert result.returncode == 0, ( + f"README code exited with {result.returncode}. Stderr:\n{result.stderr}" + ) + logger.info( + "README code executed successfully; comparing output to expected result" + ) assert stdout.strip() == expected.strip() logger.info("README code output matches expected result") @@ -148,7 +154,9 @@ 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}") + pytest.fail( + f"Bash block {i} has syntax errors:\nCode:\n{code}\nError:\n{result.stderr}" + ) class TestSkipFlag: diff --git a/.rhiza/tests/test_utils.py b/.rhiza/tests/test_utils.py index a7c1da4c..6b3e8d00 100644 --- a/.rhiza/tests/test_utils.py +++ b/.rhiza/tests/test_utils.py @@ -30,7 +30,11 @@ def strip_ansi(text: str) -> str: def run_make( - logger, args: list[str] | None = None, check: bool = True, dry_run: bool = True, env: dict[str, str] | None = None + logger, + args: list[str] | None = None, + check: bool = True, + dry_run: bool = True, + env: dict[str, str] | None = None, ) -> subprocess.CompletedProcess: """Run `make` with optional arguments and return the completed process. diff --git a/tests/test_actions/test_all_actions.py b/tests/test_actions/test_all_actions.py index 5e0fd060..f59b5c05 100644 --- a/tests/test_actions/test_all_actions.py +++ b/tests/test_actions/test_all_actions.py @@ -20,9 +20,13 @@ def test_all_actions_exist(all_action_paths): action_name = os.path.basename(os.path.dirname(action_path)) # Handle special case for pre-commit action (hyphen in name) if action_name == "pre-commit": - test_file = os.path.join(os.path.dirname(__file__), "test_pre_commit_action.py") + test_file = os.path.join( + os.path.dirname(__file__), "test_pre_commit_action.py" + ) else: - test_file = os.path.join(os.path.dirname(__file__), f"test_{action_name}_action.py") + test_file = os.path.join( + os.path.dirname(__file__), f"test_{action_name}_action.py" + ) assert os.path.exists(test_file), f"No test file found for {action_name} action" @@ -50,7 +54,9 @@ def _test_action_basic_structure(action_path): # Check runs section runs = action["runs"] - assert runs["using"] == "composite", f"{action_name} action must be a composite action" + assert runs["using"] == "composite", ( + f"{action_name} action must be a composite action" + ) assert "steps" in runs, f"{action_name} action must have steps" # Check steps @@ -78,7 +84,9 @@ def _test_action_description_quality(action_path): # Check description quality assert "description" in action, f"{action_name} action must have a description" description = action["description"] - assert len(description) >= 10, f"{action_name} action description must be at least 10 characters" + assert len(description) >= 10, ( + f"{action_name} action description must be at least 10 characters" + ) assert description.strip() == description, ( f"{action_name} action description must not have leading/trailing whitespace" ) @@ -108,14 +116,18 @@ def _test_action_inputs_documentation(action_path): # Check inputs documentation inputs = action["inputs"] for input_name, input_config in inputs.items(): - assert "description" in input_config, f"{action_name} action input {input_name} must have a description" + assert "description" in input_config, ( + f"{action_name} action input {input_name} must have a description" + ) description = input_config["description"] assert len(description) >= 5, ( f"{action_name} action input {input_name} description must be at least 5 characters" ) # Check if required is specified - assert "required" in input_config, f"{action_name} action input {input_name} must specify if it's required" + assert "required" in input_config, ( + f"{action_name} action input {input_name} must specify if it's required" + ) # If not required, should have a default value if input_config["required"] is False: diff --git a/tests/test_actions/test_book_action.py b/tests/test_actions/test_book_action.py index b265b15c..06384eb9 100644 --- a/tests/test_actions/test_book_action.py +++ b/tests/test_actions/test_book_action.py @@ -16,7 +16,9 @@ def test_book_action_structure(action_path): book_action_path = action_path("book") # Ensure the file exists - assert os.path.exists(book_action_path), f"Action file not found at {book_action_path}" + assert os.path.exists(book_action_path), ( + f"Action file not found at {book_action_path}" + ) # Load the action.yml file with open(book_action_path) as f: @@ -48,20 +50,35 @@ def test_book_action_structure(action_path): assert len(steps) >= 6, "Action must have at least 6 steps" # Check specific steps - python_step = next((step for step in steps if step.get("name", "").startswith("Set up Python")), None) + python_step = next( + (step for step in steps if step.get("name", "").startswith("Set up Python")), + None, + ) assert python_step is not None, "Action must have a Python setup step" - download_step = next((step for step in steps if step.get("name", "").startswith("Download all")), None) + download_step = next( + (step for step in steps if step.get("name", "").startswith("Download all")), + None, + ) assert download_step is not None, "Action must have a download artifacts step" - create_step = next((step for step in steps if step.get("name", "").startswith("Create minibook")), None) + create_step = next( + (step for step in steps if step.get("name", "").startswith("Create minibook")), + None, + ) assert create_step is not None, "Action must have a create minibook step" - inspect_step = next((step for step in steps if step.get("name", "").startswith("Inspect")), None) + inspect_step = next( + (step for step in steps if step.get("name", "").startswith("Inspect")), None + ) assert inspect_step is not None, "Action must have an inspect artifacts step" - upload_step = next((step for step in steps if step.get("name", "").startswith("Upload")), None) + upload_step = next( + (step for step in steps if step.get("name", "").startswith("Upload")), None + ) assert upload_step is not None, "Action must have an upload step" - deploy_step = next((step for step in steps if step.get("name", "").startswith("Deploy")), None) + deploy_step = next( + (step for step in steps if step.get("name", "").startswith("Deploy")), None + ) assert deploy_step is not None, "Action must have a deploy step" diff --git a/tests/test_actions/test_build_action.py b/tests/test_actions/test_build_action.py index 9e595744..2fb4bce5 100644 --- a/tests/test_actions/test_build_action.py +++ b/tests/test_actions/test_build_action.py @@ -16,7 +16,9 @@ def test_build_action_structure(action_path): build_action_path = action_path("build") # Ensure the file exists - assert os.path.exists(build_action_path), f"Action file not found at {build_action_path}" + assert os.path.exists(build_action_path), ( + f"Action file not found at {build_action_path}" + ) # Load the action.yml file with open(build_action_path) as f: @@ -43,20 +45,40 @@ def test_build_action_structure(action_path): assert len(steps) >= 6, "Action must have at least 6 steps" # Check specific steps - checkout_step = next((step for step in steps if step.get("name", "").startswith("Checkout")), None) + checkout_step = next( + (step for step in steps if step.get("name", "").startswith("Checkout")), None + ) assert checkout_step is not None, "Action must have a checkout step" - python_step = next((step for step in steps if step.get("name", "").startswith("Set up Python")), None) + python_step = next( + (step for step in steps if step.get("name", "").startswith("Set up Python")), + None, + ) assert python_step is not None, "Action must have a Python setup step" - version_step = next((step for step in steps if step.get("name", "").startswith("Update version")), None) + version_step = next( + (step for step in steps if step.get("name", "").startswith("Update version")), + None, + ) assert version_step is not None, "Action must have a version update step" - build_step = next((step for step in steps if step.get("name", "").startswith("Build package")), None) + build_step = next( + (step for step in steps if step.get("name", "").startswith("Build package")), + None, + ) assert build_step is not None, "Action must have a build step" - upload_step = next((step for step in steps if step.get("name", "").startswith("Upload")), None) + upload_step = next( + (step for step in steps if step.get("name", "").startswith("Upload")), None + ) assert upload_step is not None, "Action must have an upload step" - release_step = next((step for step in steps if step.get("name", "").startswith("Create GitHub release")), None) + release_step = next( + ( + step + for step in steps + if step.get("name", "").startswith("Create GitHub release") + ), + None, + ) assert release_step is not None, "Action must have a release step" diff --git a/tests/test_actions/test_coverage_action.py b/tests/test_actions/test_coverage_action.py index 9b51cc5e..d282995d 100644 --- a/tests/test_actions/test_coverage_action.py +++ b/tests/test_actions/test_coverage_action.py @@ -16,7 +16,9 @@ def test_coverage_action_structure(action_path): coverage_action_path = action_path("coverage") # Ensure the file exists - assert os.path.exists(coverage_action_path), f"Action file not found at {coverage_action_path}" + assert os.path.exists(coverage_action_path), ( + f"Action file not found at {coverage_action_path}" + ) # Load the action.yml file with open(coverage_action_path) as f: @@ -34,8 +36,12 @@ def test_coverage_action_structure(action_path): assert "source-folder" in inputs, "Action must have source-folder input" # Check required inputs - assert inputs["tests-folder"]["required"] is True, "Tests-folder input must be required" - assert inputs["source-folder"]["required"] is True, "Source-folder input must be required" + assert inputs["tests-folder"]["required"] is True, ( + "Tests-folder input must be required" + ) + assert inputs["source-folder"]["required"] is True, ( + "Source-folder input must be required" + ) # Check runs section runs = action["runs"] @@ -47,8 +53,24 @@ def test_coverage_action_structure(action_path): assert len(steps) >= 2, "Action must have at least 2 steps" # Check specific steps - run_tests_step = next((step for step in steps if step.get("name", "").startswith("Run tests with coverage")), None) + run_tests_step = next( + ( + step + for step in steps + if step.get("name", "").startswith("Run tests with coverage") + ), + None, + ) assert run_tests_step is not None, "Action must have a run tests with coverage step" - upload_results_step = next((step for step in steps if step.get("name", "").startswith("Upload test results")), None) - assert upload_results_step is not None, "Action must have an upload test results step" + upload_results_step = next( + ( + step + for step in steps + if step.get("name", "").startswith("Upload test results") + ), + None, + ) + assert upload_results_step is not None, ( + "Action must have an upload test results step" + ) diff --git a/tests/test_actions/test_deptry_action.py b/tests/test_actions/test_deptry_action.py index e0ed366a..5809c59f 100644 --- a/tests/test_actions/test_deptry_action.py +++ b/tests/test_actions/test_deptry_action.py @@ -16,7 +16,9 @@ def test_deptry_action_structure(action_path): deptry_action_path = action_path("deptry") # Ensure the file exists - assert os.path.exists(deptry_action_path), f"Action file not found at {deptry_action_path}" + assert os.path.exists(deptry_action_path), ( + f"Action file not found at {deptry_action_path}" + ) # Load the action.yml file with open(deptry_action_path) as f: @@ -34,7 +36,9 @@ def test_deptry_action_structure(action_path): assert "options" in inputs, "Action must have options input" # Check required inputs - assert inputs["source-folder"]["required"] is True, "Source-folder input must be required" + assert inputs["source-folder"]["required"] is True, ( + "Source-folder input must be required" + ) # Check runs section runs = action["runs"] @@ -46,16 +50,28 @@ def test_deptry_action_structure(action_path): assert len(steps) >= 3, "Action must have at least 3 steps" # Check specific steps - checkout_step = next((step for step in steps if step.get("name", "").startswith("Checkout")), None) + checkout_step = next( + (step for step in steps if step.get("name", "").startswith("Checkout")), None + ) assert checkout_step is not None, "Action must have a checkout step" - uv_step = next((step for step in steps if step.get("name", "").startswith("Set up uv")), None) + uv_step = next( + (step for step in steps if step.get("name", "").startswith("Set up uv")), None + ) assert uv_step is not None, "Action must have a uv setup step" - deptry_step = next((step for step in steps if step.get("name", "").startswith("Run Deptry")), None) + deptry_step = next( + (step for step in steps if step.get("name", "").startswith("Run Deptry")), None + ) assert deptry_step is not None, "Action must have a run deptry step" # Check deptry command - assert deptry_step["run"].find("uvx deptry") != -1, "Deptry step must use uvx deptry command" - assert deptry_step["run"].find("${{ inputs.source-folder }}") != -1, "Deptry step must use source-folder input" - assert deptry_step["run"].find("${{ inputs.options }}") != -1, "Deptry step must use options input" + assert deptry_step["run"].find("uvx deptry") != -1, ( + "Deptry step must use uvx deptry command" + ) + assert deptry_step["run"].find("${{ inputs.source-folder }}") != -1, ( + "Deptry step must use source-folder input" + ) + assert deptry_step["run"].find("${{ inputs.options }}") != -1, ( + "Deptry step must use options input" + ) diff --git a/tests/test_actions/test_docker_action.py b/tests/test_actions/test_docker_action.py index bcf90f5c..2db05260 100644 --- a/tests/test_actions/test_docker_action.py +++ b/tests/test_actions/test_docker_action.py @@ -16,7 +16,9 @@ def test_docker_action_structure(action_path): docker_action_path = action_path("docker") # Ensure the file exists - assert os.path.exists(docker_action_path), f"Action file not found at {docker_action_path}" + assert os.path.exists(docker_action_path), ( + f"Action file not found at {docker_action_path}" + ) # Load the action.yml file with open(docker_action_path) as f: @@ -40,12 +42,20 @@ def test_docker_action_structure(action_path): # Check required inputs assert inputs["tag"]["required"] is True, "Tag input must be required" - assert inputs["github_token"]["required"] is True, "GitHub token input must be required" - assert inputs["github_actor"]["required"] is True, "GitHub actor input must be required" - assert inputs["github_repository"]["required"] is True, "GitHub repository input must be required" + assert inputs["github_token"]["required"] is True, ( + "GitHub token input must be required" + ) + assert inputs["github_actor"]["required"] is True, ( + "GitHub actor input must be required" + ) + assert inputs["github_repository"]["required"] is True, ( + "GitHub repository input must be required" + ) # Check default values - assert inputs["registry"]["default"] == "ghcr.io", "Registry input must default to ghcr.io" + assert inputs["registry"]["default"] == "ghcr.io", ( + "Registry input must default to ghcr.io" + ) assert inputs["dockerfiles"]["default"] == "docker/Dockerfile", ( "Dockerfiles input must default to docker/Dockerfile" ) @@ -60,17 +70,25 @@ def test_docker_action_structure(action_path): assert len(steps) >= 3, "Action must have at least 3 steps" # Check specific steps - login_step = next((step for step in steps if step.get("name", "").startswith("Log in")), None) + login_step = next( + (step for step in steps if step.get("name", "").startswith("Log in")), None + ) assert login_step is not None, "Action must have a login step" - build_step = next((step for step in steps if step.get("name", "").startswith("Build")), None) + build_step = next( + (step for step in steps if step.get("name", "").startswith("Build")), None + ) assert build_step is not None, "Action must have a build step" - push_step = next((step for step in steps if step.get("name", "").startswith("Push")), None) + push_step = next( + (step for step in steps if step.get("name", "").startswith("Push")), None + ) assert push_step is not None, "Action must have a push step" # Check step details - assert login_step["uses"] == "redhat-actions/podman-login@v1", "Login step must use redhat-actions/podman-login@v1" + assert login_step["uses"] == "redhat-actions/podman-login@v1", ( + "Login step must use redhat-actions/podman-login@v1" + ) assert build_step["uses"] == "redhat-actions/buildah-build@v2", ( "Build step must use redhat-actions/buildah-build@v2" ) diff --git a/tests/test_actions/test_environment_action.py b/tests/test_actions/test_environment_action.py index 339a0bd3..d77a3394 100644 --- a/tests/test_actions/test_environment_action.py +++ b/tests/test_actions/test_environment_action.py @@ -16,7 +16,9 @@ def test_environment_action_structure(action_path): environment_action_path = action_path("environment") # Ensure the file exists - assert os.path.exists(environment_action_path), f"Action file not found at {environment_action_path}" + assert os.path.exists(environment_action_path), ( + f"Action file not found at {environment_action_path}" + ) # Load the action.yml file with open(environment_action_path) as f: @@ -31,7 +33,9 @@ def test_environment_action_structure(action_path): # Check inputs inputs = action["inputs"] assert "python-version" in inputs, "Action must have python-version input" - assert "use-requirements-txt" in inputs, "Action must have use-requirements-txt input" + assert "use-requirements-txt" in inputs, ( + "Action must have use-requirements-txt input" + ) assert "requirements-path" in inputs, "Action must have requirements-path input" # Check runs section @@ -44,10 +48,15 @@ def test_environment_action_structure(action_path): assert len(steps) >= 3, "Action must have at least 3 steps" # Check specific steps - checkout_step = next((step for step in steps if step.get("name", "").startswith("Checkout")), None) + checkout_step = next( + (step for step in steps if step.get("name", "").startswith("Checkout")), None + ) assert checkout_step is not None, "Action must have a checkout step" - python_step = next((step for step in steps if step.get("name", "").startswith("Set up Python")), None) + python_step = next( + (step for step in steps if step.get("name", "").startswith("Set up Python")), + None, + ) assert python_step is not None, "Action must have a Python setup step" # Check conditional steps for requirements.txt and pyproject.toml @@ -55,7 +64,8 @@ def test_environment_action_structure(action_path): ( step for step in steps - if step.get("if", "").find("use-requirements-txt") != -1 and step.get("if", "").find("true") != -1 + if step.get("if", "").find("use-requirements-txt") != -1 + and step.get("if", "").find("true") != -1 ), None, ) @@ -65,7 +75,8 @@ def test_environment_action_structure(action_path): ( step for step in steps - if step.get("if", "").find("use-requirements-txt") != -1 and step.get("if", "").find("false") != -1 + if step.get("if", "").find("use-requirements-txt") != -1 + and step.get("if", "").find("false") != -1 ), None, ) diff --git a/tests/test_actions/test_latex_action.py b/tests/test_actions/test_latex_action.py index a7327a9c..f64349c7 100644 --- a/tests/test_actions/test_latex_action.py +++ b/tests/test_actions/test_latex_action.py @@ -16,7 +16,9 @@ def test_latex_action_structure(action_path): latex_action_path = action_path("latex") # Ensure the file exists - assert os.path.exists(latex_action_path), f"Action file not found at {latex_action_path}" + assert os.path.exists(latex_action_path), ( + f"Action file not found at {latex_action_path}" + ) # Load the action.yml file with open(latex_action_path) as f: @@ -34,8 +36,12 @@ def test_latex_action_structure(action_path): assert "tex-file" in inputs, "Action must have tex-file input" # Check default values - assert inputs["tex-folder"]["default"] == "paper", "Tex-folder input must default to paper" - assert inputs["tex-file"]["default"] == "document.tex", "Tex-file input must default to document.tex" + assert inputs["tex-folder"]["default"] == "paper", ( + "Tex-folder input must default to paper" + ) + assert inputs["tex-file"]["default"] == "document.tex", ( + "Tex-file input must default to document.tex" + ) # Check runs section runs = action["runs"] @@ -44,7 +50,11 @@ def test_latex_action_structure(action_path): # The LaTeX action has a nested steps structure # First, check if steps is a dictionary with a 'steps' key (incorrect structure) - steps = runs["steps"]["steps"] if isinstance(runs["steps"], dict) and "steps" in runs["steps"] else runs["steps"] + steps = ( + runs["steps"]["steps"] + if isinstance(runs["steps"], dict) and "steps" in runs["steps"] + else runs["steps"] + ) assert len(steps) >= 3, "Action must have at least 3 steps" @@ -63,5 +73,7 @@ def test_latex_action_structure(action_path): assert compile_step is not None, "Action must have a Compile LaTeX document step" # Check step details - assert "wtfjoke/setup-tectonic" in install_step["uses"], "Install step must use wtfjoke/setup-tectonic" + assert "wtfjoke/setup-tectonic" in install_step["uses"], ( + "Install step must use wtfjoke/setup-tectonic" + ) assert "tectonic" in compile_step["run"], "Compile step must use tectonic" diff --git a/tests/test_actions/test_pdoc_action.py b/tests/test_actions/test_pdoc_action.py index c6ace0a3..d77e9b4c 100644 --- a/tests/test_actions/test_pdoc_action.py +++ b/tests/test_actions/test_pdoc_action.py @@ -16,7 +16,9 @@ def test_pdoc_action_structure(action_path): pdoc_action_path = action_path("pdoc") # Ensure the file exists - assert os.path.exists(pdoc_action_path), f"Action file not found at {pdoc_action_path}" + assert os.path.exists(pdoc_action_path), ( + f"Action file not found at {pdoc_action_path}" + ) # Load the action.yml file with open(pdoc_action_path) as f: @@ -34,8 +36,12 @@ def test_pdoc_action_structure(action_path): assert "pdoc-arguments" in inputs, "Action must have pdoc-arguments input" # Check default values - assert inputs["source-folder"]["default"] == "cvx", "Source-folder input must default to cvx" - assert inputs["pdoc-arguments"]["default"] == "", "Pdoc-arguments input must default to empty string" + assert inputs["source-folder"]["default"] == "cvx", ( + "Source-folder input must default to cvx" + ) + assert inputs["pdoc-arguments"]["default"] == "", ( + "Pdoc-arguments input must default to empty string" + ) # Check runs section runs = action["runs"] @@ -47,14 +53,29 @@ def test_pdoc_action_structure(action_path): assert len(steps) >= 2, "Action must have at least 2 steps" # Check specific steps - build_step = next((step for step in steps if step.get("name", "").startswith("Install and build")), None) + build_step = next( + ( + step + for step in steps + if step.get("name", "").startswith("Install and build") + ), + None, + ) assert build_step is not None, "Action must have an install and build pdoc step" - upload_step = next((step for step in steps if step.get("name", "").startswith("Upload")), None) + upload_step = next( + (step for step in steps if step.get("name", "").startswith("Upload")), None + ) assert upload_step is not None, "Action must have an upload documentation step" # Check step details - assert build_step["run"].find("uv pip install pdoc") != -1, "Build step must install pdoc" + assert build_step["run"].find("uv pip install pdoc") != -1, ( + "Build step must install pdoc" + ) assert build_step["run"].find("uv run pdoc") != -1, "Build step must run pdoc" - assert build_step["run"].find("${{ inputs.pdoc-arguments }}") != -1, "Build step must use pdoc-arguments input" - assert build_step["run"].find("${{ inputs.source-folder }}") != -1, "Build step must use source-folder input" + assert build_step["run"].find("${{ inputs.pdoc-arguments }}") != -1, ( + "Build step must use pdoc-arguments input" + ) + assert build_step["run"].find("${{ inputs.source-folder }}") != -1, ( + "Build step must use source-folder input" + ) diff --git a/tests/test_actions/test_pre_commit_action.py b/tests/test_actions/test_pre_commit_action.py index 28de5ac2..6d65758d 100644 --- a/tests/test_actions/test_pre_commit_action.py +++ b/tests/test_actions/test_pre_commit_action.py @@ -16,7 +16,9 @@ def test_pre_commit_action_structure(action_path): pre_commit_action_path = action_path("pre-commit") # Ensure the file exists - assert os.path.exists(pre_commit_action_path), f"Action file not found at {pre_commit_action_path}" + assert os.path.exists(pre_commit_action_path), ( + f"Action file not found at {pre_commit_action_path}" + ) # Load the action.yml file with open(pre_commit_action_path) as f: @@ -37,20 +39,40 @@ def test_pre_commit_action_structure(action_path): assert len(steps) >= 3, "Action must have at least 3 steps" # Check specific steps - checkout_step = next((step for step in steps if step.get("name", "").startswith("Checkout")), None) + checkout_step = next( + (step for step in steps if step.get("name", "").startswith("Checkout")), None + ) assert checkout_step is not None, "Action must have a checkout step" - node_step = next((step for step in steps if step.get("name", "").startswith("Install Node")), None) + node_step = next( + (step for step in steps if step.get("name", "").startswith("Install Node")), + None, + ) assert node_step is not None, "Action must have a Node.js setup step" - pre_commit_step = next((step for step in steps if step.get("uses", "").startswith("pre-commit/action")), None) + pre_commit_step = next( + ( + step + for step in steps + if step.get("uses", "").startswith("pre-commit/action") + ), + None, + ) assert pre_commit_step is not None, "Action must have a pre-commit step" # Check step details - assert checkout_step["uses"] == "actions/checkout@v6", "Checkout step must use actions/checkout@v4" - assert node_step["uses"] == "actions/setup-node@v6", "Node step must use actions/setup-node@v6" - assert node_step["with"]["node-version"] == "24", "Node step must use Node.js version 22" - assert pre_commit_step["uses"] == "pre-commit/action@v3.0.1", "Pre-commit step must use pre-commit/action@v3.0.1" + assert checkout_step["uses"] == "actions/checkout@v6", ( + "Checkout step must use actions/checkout@v4" + ) + assert node_step["uses"] == "actions/setup-node@v6", ( + "Node step must use actions/setup-node@v6" + ) + assert node_step["with"]["node-version"] == "24", ( + "Node step must use Node.js version 22" + ) + assert pre_commit_step["uses"] == "pre-commit/action@v3.0.1", ( + "Pre-commit step must use pre-commit/action@v3.0.1" + ) assert pre_commit_step["with"]["extra_args"] == "--verbose --all-files", ( "Pre-commit step must run on all files with verbose output" ) diff --git a/tests/test_actions/test_tag_action.py b/tests/test_actions/test_tag_action.py index 0cabf1e1..eef95748 100644 --- a/tests/test_actions/test_tag_action.py +++ b/tests/test_actions/test_tag_action.py @@ -16,7 +16,9 @@ def test_tag_action_structure(action_path): tag_action_path = action_path("tag") # Ensure the file exists - assert os.path.exists(tag_action_path), f"Action file not found at {tag_action_path}" + assert os.path.exists(tag_action_path), ( + f"Action file not found at {tag_action_path}" + ) # Load the action.yml file with open(tag_action_path) as f: @@ -31,9 +33,9 @@ def test_tag_action_structure(action_path): # Check outputs outputs = action["outputs"] assert "new_tag" in outputs, "Action must have new_tag output" - assert outputs["new_tag"]["value"].find("steps.tag_version.outputs.new_tag") != -1, ( - "New tag output must reference tag_version step output" - ) + assert ( + outputs["new_tag"]["value"].find("steps.tag_version.outputs.new_tag") != -1 + ), "New tag output must reference tag_version step output" # Check runs section runs = action["runs"] @@ -45,11 +47,21 @@ def test_tag_action_structure(action_path): assert len(steps) >= 2, "Action must have at least 2 steps" # Check specific steps - tag_step = next((step for step in steps if step.get("name", "").startswith("Bump version")), None) + tag_step = next( + (step for step in steps if step.get("name", "").startswith("Bump version")), + None, + ) assert tag_step is not None, "Action must have a bump version and tag step" assert tag_step["id"] == "tag_version", "Bump version step must have id tag_version" - release_step = next((step for step in steps if step.get("name", "").startswith("Create GitHub release")), None) + release_step = next( + ( + step + for step in steps + if step.get("name", "").startswith("Create GitHub release") + ), + None, + ) assert release_step is not None, "Action must have a create GitHub release step" # Check step details @@ -60,7 +72,9 @@ def test_tag_action_structure(action_path): assert release_step["uses"] == "softprops/action-gh-release@v2", ( "Release step must use softprops/action-gh-release@v2" ) - assert release_step["with"]["tag_name"] == "${{ steps.tag_version.outputs.new_tag }}", ( - "Release step must use new_tag output from tag step" + assert ( + release_step["with"]["tag_name"] == "${{ steps.tag_version.outputs.new_tag }}" + ), "Release step must use new_tag output from tag step" + assert release_step["with"]["generate_release_notes"] is True, ( + "Release step must generate release notes" ) - assert release_step["with"]["generate_release_notes"] is True, "Release step must generate release notes" diff --git a/tests/test_actions/test_test_action.py b/tests/test_actions/test_test_action.py index 25fa14a3..a61cb40d 100644 --- a/tests/test_actions/test_test_action.py +++ b/tests/test_actions/test_test_action.py @@ -16,7 +16,9 @@ def test_test_action_structure(action_path): test_action_path = action_path("test") # Ensure the file exists - assert os.path.exists(test_action_path), f"Action file not found at {test_action_path}" + assert os.path.exists(test_action_path), ( + f"Action file not found at {test_action_path}" + ) # Load the action.yml file with open(test_action_path) as f: @@ -33,7 +35,9 @@ def test_test_action_structure(action_path): assert "tests-folder" in inputs, "Action must have tests-folder input" # Check default values - assert inputs["tests-folder"]["default"] == "tests", "Tests-folder input must default to tests" + assert inputs["tests-folder"]["default"] == "tests", ( + "Tests-folder input must default to tests" + ) # Check runs section runs = action["runs"] @@ -45,13 +49,21 @@ def test_test_action_structure(action_path): assert len(steps) >= 1, "Action must have at least 1 step" # Check specific steps - run_tests_step = next((step for step in steps if step.get("name", "").startswith("Run tests")), None) + run_tests_step = next( + (step for step in steps if step.get("name", "").startswith("Run tests")), None + ) assert run_tests_step is not None, "Action must have a run tests step" # Check step details - assert run_tests_step["shell"] == "${{ runner.os == 'Windows' && 'pwsh' || 'bash' }}", ( - "Run tests step must use cross-platform shell selection" + assert ( + run_tests_step["shell"] == "${{ runner.os == 'Windows' && 'pwsh' || 'bash' }}" + ), "Run tests step must use cross-platform shell selection" + assert run_tests_step["run"].find("uv pip install") != -1, ( + "Run tests step must install pytest" + ) + assert run_tests_step["run"].find("uv run pytest") != -1, ( + "Run tests step must run pytest" + ) + assert run_tests_step["run"].find("${{ inputs.tests-folder }}") != -1, ( + "Run tests step must use tests-folder input" ) - assert run_tests_step["run"].find("uv pip install") != -1, "Run tests step must install pytest" - assert run_tests_step["run"].find("uv run pytest") != -1, "Run tests step must run pytest" - assert run_tests_step["run"].find("${{ inputs.tests-folder }}") != -1, "Run tests step must use tests-folder input" From 1813f6a9b922ecabb7dcf9eb2006d56ddc0eb992 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Tue, 10 Mar 2026 15:39:38 +0400 Subject: [PATCH 4/9] Delete `configure-git-auth` action and related references --- .github/actions/configure-git-auth/README.md | 80 ------------------- .github/actions/configure-git-auth/action.yml | 21 ----- .github/workflows/rhiza_ci.yml | 25 +----- .github/workflows/rhiza_pre-commit.yml | 5 -- .github/workflows/rhiza_release.yml | 5 -- 5 files changed, 1 insertion(+), 135 deletions(-) delete mode 100644 .github/actions/configure-git-auth/README.md delete mode 100644 .github/actions/configure-git-auth/action.yml diff --git a/.github/actions/configure-git-auth/README.md b/.github/actions/configure-git-auth/README.md deleted file mode 100644 index 4b6faeb7..00000000 --- a/.github/actions/configure-git-auth/README.md +++ /dev/null @@ -1,80 +0,0 @@ -# Configure Git Auth for Private Packages - -This composite action configures git to use token authentication for private GitHub packages. - -## Usage - -Add this step before installing dependencies that include private GitHub packages: - -```yaml -- name: Configure git auth for private packages - uses: ./.github/actions/configure-git-auth - with: - token: ${{ secrets.GH_PAT }} -``` - -The `GH_PAT` secret should be a Personal Access Token with `repo` scope. - -## What It Does - -This action runs: - -```bash -git config --global url."https://@github.com/".insteadOf "https://github.com/" -``` - -This tells git to automatically inject the token into all HTTPS GitHub URLs, enabling access to private repositories. - -## When to Use - -Use this action when your project has dependencies defined in `pyproject.toml` like: - -```toml -[tool.uv.sources] -private-package = { git = "https://github.com/your-org/private-package.git", rev = "v1.0.0" } -``` - -## Token Requirements - -By default, this action will use the workflow’s built-in `GITHUB_TOKEN` (`github.token`) if no `token` input is provided or if the provided value is empty (it uses `inputs.token || github.token` internally). - -The `GITHUB_TOKEN` is usually sufficient when: - -- installing dependencies hosted in the **same repository** as the workflow, or -- accessing **public** repositories. - -The default `GITHUB_TOKEN` typically does **not** have permission to read other private repositories, even within the same organization. For that scenario, you should create a Personal Access Token (PAT) with `repo` scope and store it as `secrets.GH_PAT`, then pass it to the action via the `token` input. - -If you configure the step as in the example (`token: ${{ secrets.GH_PAT }}`) and `secrets.GH_PAT` is not defined, GitHub Actions passes an empty string to the action. The composite action then falls back to `github.token`, so the configuration step itself still succeeds. However, any subsequent step that tries to access private repositories that are not covered by the permissions of `GITHUB_TOKEN` will fail with an authentication error. -## Example Workflow - -```yaml -name: CI - -on: [push, pull_request] - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - name: Install uv - uses: astral-sh/setup-uv@v7 - - - name: Configure git auth for private packages - uses: ./.github/actions/configure-git-auth - with: - token: ${{ secrets.GH_PAT }} - - - name: Install dependencies - run: uv sync --frozen - - - name: Run tests - run: uv run pytest -``` - -## See Also - -- [PRIVATE_PACKAGES.md](../../../.rhiza/docs/PRIVATE_PACKAGES.md) - Complete guide to using private packages -- [TOKEN_SETUP.md](../../../.rhiza/docs/TOKEN_SETUP.md) - Setting up Personal Access Tokens diff --git a/.github/actions/configure-git-auth/action.yml b/.github/actions/configure-git-auth/action.yml deleted file mode 100644 index d4d898fb..00000000 --- a/.github/actions/configure-git-auth/action.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: 'Configure Git Auth for Private Packages' -description: 'Configure git to use token authentication for private GitHub packages' - -inputs: - token: - description: 'GitHub token to use for authentication' - required: false - -runs: - using: composite - steps: - - name: Configure git authentication - shell: bash - env: - GH_TOKEN: ${{ inputs.token || github.token }} - run: | - # Configure git to use token authentication for GitHub URLs - # This allows uv/pip to install private packages from GitHub - git config --global url."https://${GH_TOKEN}@github.com/".insteadOf "https://github.com/" - - echo "βœ“ Git configured to use token authentication for GitHub" diff --git a/.github/workflows/rhiza_ci.yml b/.github/workflows/rhiza_ci.yml index 1cb5b803..319dfd50 100644 --- a/.github/workflows/rhiza_ci.yml +++ b/.github/workflows/rhiza_ci.yml @@ -37,7 +37,7 @@ jobs: uses: ./.github/actions/configure-git-auth with: token: ${{ secrets.GH_PAT }} - + - id: versions env: UV_EXTRA_INDEX_URL: ${{ secrets.UV_EXTRA_INDEX_URL }} @@ -80,26 +80,3 @@ jobs: UV_EXTRA_INDEX_URL: ${{ secrets.UV_EXTRA_INDEX_URL }} run: | make test - - - docs-coverage: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v6.0.2 - - - name: Install uv - uses: astral-sh/setup-uv@v7.3.1 - with: - version: "0.10.8" - - - name: Configure git auth for private packages - uses: ./.github/actions/configure-git-auth - with: - token: ${{ secrets.GH_PAT }} - - - name: Check docs coverage - env: - UV_EXTRA_INDEX_URL: ${{ secrets.UV_EXTRA_INDEX_URL }} - run: | - make docs-coverage diff --git a/.github/workflows/rhiza_pre-commit.yml b/.github/workflows/rhiza_pre-commit.yml index df38fa03..9125d9ba 100644 --- a/.github/workflows/rhiza_pre-commit.yml +++ b/.github/workflows/rhiza_pre-commit.yml @@ -32,11 +32,6 @@ jobs: steps: - uses: actions/checkout@v6.0.2 - - name: Configure git auth for private packages - uses: ./.github/actions/configure-git-auth - with: - token: ${{ secrets.GH_PAT }} - # Cache pre-commit environments and hooks - name: Cache pre-commit environments uses: actions/cache@v5 diff --git a/.github/workflows/rhiza_release.yml b/.github/workflows/rhiza_release.yml index 58b72650..8ac86f43 100644 --- a/.github/workflows/rhiza_release.yml +++ b/.github/workflows/rhiza_release.yml @@ -124,11 +124,6 @@ jobs: with: version: "0.10.8" - - name: Configure git auth for private packages - uses: ./.github/actions/configure-git-auth - with: - token: ${{ secrets.GH_PAT }} - - name: Verify version matches tag if: hashFiles('pyproject.toml') != '' run: | From 705c96233644647fbb8af64298fbd5713a415347 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Tue, 10 Mar 2026 15:40:23 +0400 Subject: [PATCH 5/9] Delete `configure-git-auth` action and related references --- .github/workflows/rhiza_ci.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/rhiza_ci.yml b/.github/workflows/rhiza_ci.yml index 319dfd50..3cf64413 100644 --- a/.github/workflows/rhiza_ci.yml +++ b/.github/workflows/rhiza_ci.yml @@ -70,11 +70,6 @@ jobs: version: "0.10.8" python-version: ${{ matrix.python-version }} - - name: Configure git auth for private packages - uses: ./.github/actions/configure-git-auth - with: - token: ${{ secrets.GH_PAT }} - - name: Run tests env: UV_EXTRA_INDEX_URL: ${{ secrets.UV_EXTRA_INDEX_URL }} From 248a427ea92b7d3d36aec1e202bf5676086cabd4 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Tue, 10 Mar 2026 15:41:39 +0400 Subject: [PATCH 6/9] Delete `configure-git-auth` action and related references --- .github/workflows/rhiza_ci.yml | 5 -- .github/workflows/rhiza_release.yml | 102 +--------------------------- 2 files changed, 1 insertion(+), 106 deletions(-) diff --git a/.github/workflows/rhiza_ci.yml b/.github/workflows/rhiza_ci.yml index 3cf64413..67727b59 100644 --- a/.github/workflows/rhiza_ci.yml +++ b/.github/workflows/rhiza_ci.yml @@ -33,11 +33,6 @@ jobs: with: version: "0.10.8" - - name: Configure git auth for private packages - uses: ./.github/actions/configure-git-auth - with: - token: ${{ secrets.GH_PAT }} - - id: versions env: UV_EXTRA_INDEX_URL: ${{ secrets.UV_EXTRA_INDEX_URL }} diff --git a/.github/workflows/rhiza_release.yml b/.github/workflows/rhiza_release.yml index 8ac86f43..29d6ff37 100644 --- a/.github/workflows/rhiza_release.yml +++ b/.github/workflows/rhiza_release.yml @@ -285,98 +285,11 @@ jobs: repository-url: ${{ vars.PYPI_REPOSITORY_URL }} password: ${{ secrets.PYPI_TOKEN }} - devcontainer: - name: Publish Devcontainer Image - runs-on: ubuntu-latest - environment: release - needs: [tag, build, draft-release] - outputs: - should_publish: ${{ steps.check_publish.outputs.should_publish }} - image_name: ${{ steps.image_name.outputs.image_name }} - steps: - - name: Checkout Code - uses: actions/checkout@v6.0.2 - with: - fetch-depth: 0 - - - name: Check if devcontainer should be published - id: check_publish - env: - PUBLISH_DEVCONTAINER: ${{ vars.PUBLISH_DEVCONTAINER }} - run: | - if [[ "$PUBLISH_DEVCONTAINER" == "true" ]] && [[ -d ".devcontainer" ]]; then - echo "should_publish=true" >> "$GITHUB_OUTPUT" - echo "πŸš€ Will build and publish devcontainer image" - else - echo "should_publish=false" >> "$GITHUB_OUTPUT" - echo "⏭️ Skipping devcontainer publish (PUBLISH_DEVCONTAINER not true or .devcontainer missing)" - fi - - - name: Set registry - if: steps.check_publish.outputs.should_publish == 'true' - id: registry - run: | - REGISTRY="${{ vars.DEVCONTAINER_REGISTRY }}" - if [ -z "$REGISTRY" ]; then - REGISTRY="ghcr.io" - fi - echo "registry=$REGISTRY" >> "$GITHUB_OUTPUT" - - - name: Login to Container Registry - if: steps.check_publish.outputs.should_publish == 'true' - uses: docker/login-action@v4.0.0 - with: - registry: ${{ steps.registry.outputs.registry }} - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Get lowercase repository owner - if: steps.check_publish.outputs.should_publish == 'true' - id: repo_owner - run: echo "owner_lc=$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT" - - - name: Get Image Name - if: steps.check_publish.outputs.should_publish == 'true' - id: image_name - run: | - # Check if custom name is provided, otherwise use default - if [ -z "${{ vars.DEVCONTAINER_IMAGE_NAME }}" ]; then - REPO_NAME_LC=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]') - # Sanitize repo name: replace invalid characters with hyphens - # Docker image names must match [a-z0-9]+([._-][a-z0-9]+)* - # Replace leading dots and multiple consecutive separators - REPO_NAME_SANITIZED=$(echo "$REPO_NAME_LC" | sed 's/^[._-]*//; s/[._-][._-]*/-/g') - IMAGE_NAME="$REPO_NAME_SANITIZED/devcontainer" - echo "Using default image name component: $IMAGE_NAME" - else - IMAGE_NAME="${{ vars.DEVCONTAINER_IMAGE_NAME }}" - echo "Using custom image name component: $IMAGE_NAME" - fi - - # Validate the image component matches [a-z0-9]+([._-][a-z0-9]+)* with optional / separators - if ! echo "$IMAGE_NAME" | grep -qE '^[a-z0-9]+([._-][a-z0-9]+)*(/[a-z0-9]+([._-][a-z0-9]+)*)*$'; then - echo "::error::Invalid image name component: $IMAGE_NAME" - echo "::error::Each component must match [a-z0-9]+([._-][a-z0-9]+)* separated by /" - exit 1 - fi - - IMAGE_NAME="${{ steps.registry.outputs.registry }}/${{ steps.repo_owner.outputs.owner_lc }}/$IMAGE_NAME" - echo "βœ… Final image name: $IMAGE_NAME" - echo "image_name=$IMAGE_NAME" >> "$GITHUB_OUTPUT" - - - name: Build and Publish Devcontainer Image - if: steps.check_publish.outputs.should_publish == 'true' - uses: devcontainers/ci@v0.3 - with: - configFile: .devcontainer/devcontainer.json - push: always - imageName: ${{ steps.image_name.outputs.image_name }} - imageTag: ${{ needs.tag.outputs.tag }} finalise-release: name: Finalise Release runs-on: ubuntu-latest - needs: [tag, pypi, devcontainer] + needs: [tag, pypi] if: needs.pypi.result == 'success' || needs.devcontainer.result == 'success' steps: - name: Checkout Code @@ -399,19 +312,6 @@ jobs: - name: Set up Python uses: actions/setup-python@v6.2.0 - - name: Generate Devcontainer Link - id: devcontainer_link - if: needs.devcontainer.outputs.should_publish == 'true' && needs.devcontainer.result == 'success' - run: | - FULL_IMAGE="${{ needs.devcontainer.outputs.image_name }}:${{ needs.tag.outputs.tag }}" - { - echo "message<> "$GITHUB_OUTPUT" - - name: Generate PyPI Link id: pypi_link if: needs.pypi.outputs.should_publish == 'true' && needs.pypi.result == 'success' From b1506c73e8dc5a448935d0af454f152af3c3f222 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Tue, 10 Mar 2026 15:43:05 +0400 Subject: [PATCH 7/9] Delete .rhiza/docs directory and associated documentation files and dependencies --- .rhiza/docs/ASSETS.md | 14 -- .rhiza/docs/CONFIG.md | 45 ------ .rhiza/docs/LFS.md | 161 --------------------- .rhiza/docs/PRIVATE_PACKAGES.md | 233 ------------------------------ .rhiza/docs/RELEASING.md | 99 ------------- .rhiza/docs/TOKEN_SETUP.md | 102 ------------- .rhiza/docs/WORKFLOWS.md | 248 -------------------------------- .rhiza/requirements/docs.txt | 3 - .rhiza/requirements/marimo.txt | 2 - 9 files changed, 907 deletions(-) delete mode 100644 .rhiza/docs/ASSETS.md delete mode 100644 .rhiza/docs/CONFIG.md delete mode 100644 .rhiza/docs/LFS.md delete mode 100644 .rhiza/docs/PRIVATE_PACKAGES.md delete mode 100644 .rhiza/docs/RELEASING.md delete mode 100644 .rhiza/docs/TOKEN_SETUP.md delete mode 100644 .rhiza/docs/WORKFLOWS.md delete mode 100644 .rhiza/requirements/docs.txt delete mode 100644 .rhiza/requirements/marimo.txt diff --git a/.rhiza/docs/ASSETS.md b/.rhiza/docs/ASSETS.md deleted file mode 100644 index 44090a1d..00000000 --- a/.rhiza/docs/ASSETS.md +++ /dev/null @@ -1,14 +0,0 @@ -# Assets - -The `.rhiza/assets/` directory contains static assets used in the Rhiza project, such as logos, images, and other media files. - -## Contents - -- `rhiza-logo.svg`: The official Rhiza project logo. - -## Usage - -These assets are primarily used in: -- The main `README.md` file. -- Generated documentation and the companion book. -- Project presentations. diff --git a/.rhiza/docs/CONFIG.md b/.rhiza/docs/CONFIG.md deleted file mode 100644 index 3c41a263..00000000 --- a/.rhiza/docs/CONFIG.md +++ /dev/null @@ -1,45 +0,0 @@ -# Rhiza Configuration - -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 - -### CI/CD & Infrastructure -- **[TOKEN_SETUP.md](TOKEN_SETUP.md)** - Instructions for setting up the `PAT_TOKEN` secret required for the SYNC workflow -- **[PRIVATE_PACKAGES.md](PRIVATE_PACKAGES.md)** - Guide for using private GitHub packages as dependencies -- **[WORKFLOWS.md](WORKFLOWS.md)** - Development workflows and dependency management -- **[RELEASING.md](RELEASING.md)** - Release process and version management -- **[LFS.md](LFS.md)** - Git LFS configuration and make targets -- **[ASSETS.md](ASSETS.md)** - Information about `.rhiza/assets/` directory - -## Structure - -- **utils/** - Python utilities for version management - -GitHub-specific composite actions are located in `.github/rhiza/actions/`. - -## Workflows - -The repository uses several automated workflows (located in `.github/workflows/`): - -- **SYNC** (`rhiza_sync.yml`) - Synchronizes with the template repository - - **Requires:** `PAT_TOKEN` secret with `workflow` scope when modifying workflow files - - See [TOKEN_SETUP.md](TOKEN_SETUP.md) for configuration -- **CI** (`rhiza_ci.yml`) - Continuous integration tests -- **Pre-commit** (`rhiza_pre-commit.yml`) - Code quality checks -- **Book** (`rhiza_book.yml`) - Documentation deployment -- **Release** (`rhiza_release.yml`) - Package publishing -- **Deptry** (`rhiza_deptry.yml`) - Dependency checks -- **Marimo** (`rhiza_marimo.yml`) - Interactive notebooks - -## Template Synchronization - -This repository is synchronized with the template repository defined in `template.yml`. - -The synchronization includes: -- GitHub workflows and actions -- Development tools configuration (`.editorconfig`, `ruff.toml`, etc.) -- Testing infrastructure -- Documentation templates - -See `template.yml` for the complete list of synchronized files and exclusions. diff --git a/.rhiza/docs/LFS.md b/.rhiza/docs/LFS.md deleted file mode 100644 index 9427c67d..00000000 --- a/.rhiza/docs/LFS.md +++ /dev/null @@ -1,161 +0,0 @@ -# Git LFS (Large File Storage) Configuration - -This document describes the Git LFS integration in the Rhiza framework. - -## Overview - -Git LFS (Large File Storage) is an extension to Git that allows you to version large files efficiently. Instead of storing large binary files directly in the Git repository, LFS stores them on a remote server and keeps only small pointer files in the repository. - -## Available Make Targets - -### `make lfs-install` - -Installs Git LFS and configures it for the current repository. - -**Features:** -- **Cross-platform support**: Works on macOS (both Intel and ARM) and Linux -- **macOS**: Downloads and installs the latest git-lfs binary to `.local/bin/` -- **Linux**: Installs git-lfs via apt-get package manager -- **Automatic configuration**: Runs `git lfs install` to set up LFS hooks - -**Usage:** -```bash -make lfs-install -``` - -**Note for macOS users:** The git-lfs binary is installed locally in `.local/bin/` and added to PATH for the installation. This approach avoids requiring system-level package managers like Homebrew. - -### `make lfs-pull` - -Downloads all Git LFS files for the current branch. - -**Usage:** -```bash -make lfs-pull -``` - -This is useful after cloning a repository or checking out a branch that contains LFS-tracked files. - -### `make lfs-track` - -Lists all file patterns currently tracked by Git LFS. - -**Usage:** -```bash -make lfs-track -``` - -### `make lfs-status` - -Shows the status of Git LFS files in the repository. - -**Usage:** -```bash -make lfs-status -``` - -## Typical Workflow - -1. **Initial setup** (first time only): - ```bash - make lfs-install - ``` - -2. **Track large files** (configure which files to store in LFS): - ```bash - git lfs track "*.psd" - git lfs track "*.zip" - git lfs track "data/*.csv" - ``` - -3. **Check tracking status**: - ```bash - make lfs-track - ``` - -4. **Pull LFS files** (after cloning or checking out): - ```bash - make lfs-pull - ``` - -5. **Check LFS status**: - ```bash - make lfs-status - ``` - -## CI/CD Integration - -### GitHub Actions - -When using Git LFS with GitHub Actions, add the `lfs: true` option to your checkout step: - -```yaml -- uses: actions/checkout@v4 - with: - lfs: true -``` - -### GitLab CI - -For GitLab CI, install and pull LFS files in your before_script: - -```yaml -before_script: - - apt-get update && apt-get install -y git-lfs || exit 1 - - git lfs pull -``` - -## Configuration Files - -Git LFS uses `.gitattributes` to track which files should be managed by LFS. Example: - -``` -# .gitattributes -*.psd filter=lfs diff=lfs merge=lfs -text -*.zip filter=lfs diff=lfs merge=lfs -text -data/*.csv filter=lfs diff=lfs merge=lfs -text -``` - -## Resources - -- [Git LFS Official Documentation](https://git-lfs.github.com/) -- [Git LFS Tutorial](https://github.com/git-lfs/git-lfs/wiki/Tutorial) -- [Git LFS GitHub Repository](https://github.com/git-lfs/git-lfs) - -## Troubleshooting - -### Permission denied during installation (Linux) - -If you encounter permission errors on Linux during `make lfs-install`, the installation requires elevated privileges. The command will prompt for sudo access automatically. If it fails, you can run: - -```bash -sudo apt-get update && sudo apt-get install -y git-lfs -git lfs install -``` - -Alternatively, if you don't have sudo access, git-lfs can be installed manually by downloading the binary from the [releases page](https://github.com/git-lfs/git-lfs/releases). - -### Failed to detect git-lfs version (macOS) - -If the installation fails with "Failed to detect git-lfs version", ensure you have internet connectivity and can access the GitHub API: - -```bash -curl -s https://api.github.com/repos/git-lfs/git-lfs/releases/latest -``` - -If the GitHub API is blocked, you can manually download and install git-lfs from [git-lfs.github.com](https://git-lfs.github.com/). - -### LFS files not downloading - -If LFS files are not downloading, ensure: -1. Git LFS is installed: `git lfs version` -2. LFS is initialized: `git lfs install` -3. Pull LFS files explicitly: `make lfs-pull` - -### Checking LFS storage usage - -To see how much storage your LFS files are using: - -```bash -git lfs ls-files --size -``` diff --git a/.rhiza/docs/PRIVATE_PACKAGES.md b/.rhiza/docs/PRIVATE_PACKAGES.md deleted file mode 100644 index f7a98daa..00000000 --- a/.rhiza/docs/PRIVATE_PACKAGES.md +++ /dev/null @@ -1,233 +0,0 @@ -# Using Private GitHub Packages - -This document explains how to configure your project to use private GitHub packages from the same organization as dependencies. - -## Quick Start - -If you're using Rhiza's template workflows, git authentication for private packages is **already configured**! All Rhiza workflows automatically include the necessary git configuration to access private repositories in the same organization. - -Simply add your private package to `pyproject.toml`: - -```toml -[tool.uv.sources] -my-package = { git = "https://github.com/jebel-quant/my-package.git", rev = "v1.0.0" } -``` - -The workflows will handle authentication automatically using `GITHUB_TOKEN`. - -## Detailed Guide - -### Problem - -When your project depends on private GitHub repositories, you need to authenticate to access them. SSH keys work locally but are complex to set up in CI/CD environments. HTTPS with tokens is simpler and more secure for automated workflows. - -## Solution - -Use HTTPS URLs with token authentication instead of SSH for git dependencies. - -### 1. Configure Dependencies in pyproject.toml - -Instead of using SSH URLs like `git@github.com:org/repo.git`, use HTTPS URLs: - -```toml -[tool.uv.sources] -my-package = { git = "https://github.com/jebel-quant/my-package.git", rev = "v1.0.0" } -another-package = { git = "https://github.com/jebel-quant/another-package.git", tag = "v2.0.0" } -``` - -**Key points:** -- Use `https://github.com/` instead of `git@github.com:` -- Specify version using `rev`, `tag`, or `branch` parameter -- No token is included in the URL itself (git config handles authentication) - -### 2. Git Authentication in CI (Already Configured!) - -**If you're using Rhiza's template workflows, this is already set up for you.** All Rhiza workflows (CI, book, release, etc.) automatically include git authentication steps. - -You can verify this by checking any Rhiza workflow file (e.g., `.github/workflows/rhiza_ci.yml`): - -```yaml -- name: Configure git auth for private packages - uses: ./.github/actions/configure-git-auth -``` - -Or for container-based workflows: - -```yaml -- name: Configure git auth for private packages - run: | - git config --global url."https://${{ github.token }}@github.com/".insteadOf "https://github.com/" -``` - -**For custom workflows** (not synced from Rhiza), add the git authentication step yourself: - -```yaml -- name: Configure git auth for private packages - run: | - git config --global url."https://${{ github.token }}@github.com/".insteadOf "https://github.com/" -``` - -This configuration tells git to automatically inject the `GITHUB_TOKEN` into all HTTPS GitHub URLs. - -### 3. Using the Composite Action (Custom Workflows) - -For custom workflows, you can use Rhiza's composite action instead of inline commands: - -```yaml -- name: Configure git auth for private packages - uses: ./.github/actions/configure-git-auth -``` - -This is cleaner and more maintainable than inline git config commands. - -### 4. Complete Workflow Example - -Here's a complete example of a GitHub Actions workflow that uses private packages: - -```yaml -name: CI with Private Packages - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - test: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Install uv - uses: astral-sh/setup-uv@v7 - with: - version: "0.9.28" - - - name: Configure git auth for private packages - run: | - git config --global url."https://${{ github.token }}@github.com/".insteadOf "https://github.com/" - - - name: Install dependencies - run: | - uv sync --frozen - - - name: Run tests - run: | - uv run pytest -``` - -## Token Scopes - -### Same Repository - -The default `GITHUB_TOKEN` automatically has access to the **same repository** where the workflow runs: -- βœ… Is automatically provided by GitHub Actions -- βœ… Is scoped to the workflow run (secure) -- βœ… No manual token management required - -This is sufficient if your private packages are defined within the same repository. - -### Same Organization (Requires PAT) - -**Important:** The default `GITHUB_TOKEN` typically does **not** have permission to read other private repositories, even within the same organization. This is GitHub's default security behavior. - -To access private packages in other repositories within your organization, you need a Personal Access Token (PAT): - -1. Create a PAT with `repo` scope (see [TOKEN_SETUP.md](TOKEN_SETUP.md) for instructions) -2. Add it as a repository secret (e.g., `PRIVATE_PACKAGES_TOKEN`) -3. Use it in the git config - -**Note:** Some organizations configure settings to allow `GITHUB_TOKEN` cross-repository access, but this is not the default and should not be assumed. Using a PAT is the recommended approach for reliability. - -### Different Organization - -If your private packages are in a **different organization**, you need a Personal Access Token (PAT): - -1. Create a PAT with `repo` scope (see [TOKEN_SETUP.md](TOKEN_SETUP.md) for instructions) -2. Add it as a repository secret (e.g., `PRIVATE_PACKAGES_TOKEN`) -3. Use it in the git config: - -```yaml -- name: Configure git auth for private packages - run: | - git config --global url."https://${{ secrets.PRIVATE_PACKAGES_TOKEN }}@github.com/".insteadOf "https://github.com/" -``` - -## Local Development - -For local development, you have several options: - -### Option 1: Use GitHub CLI (Recommended) - -```bash -# Install gh CLI -brew install gh # macOS -# or: apt install gh # Ubuntu/Debian - -# Authenticate -gh auth login - -# Configure git -gh auth setup-git -``` - -The GitHub CLI automatically handles git authentication for private repositories. - -### Option 2: Use Personal Access Token - -```bash -# Create a PAT with 'repo' scope at: -# https://github.com/settings/tokens - -# Configure git -git config --global url."https://YOUR_TOKEN@github.com/".insteadOf "https://github.com/" -``` - -**Security Note:** Be careful not to commit this configuration. It's better to use `gh` CLI or SSH keys for local development. - -### Option 3: Use SSH (Local Only) - -For local development, you can continue using SSH: - -```toml -[tool.uv.sources] -my-package = { git = "ssh://git@github.com/jebel-quant/my-package.git", rev = "v1.0.0" } -``` - -However, this won't work in CI without additional SSH key setup. - -## Troubleshooting - -### Error: "fatal: could not read Username" - -This means git cannot find authentication credentials. Ensure: -1. The git config step runs **before** `uv sync` -2. The token has proper permissions -3. The repository URL uses HTTPS format - -### Error: "Repository not found" or "403 Forbidden" - -This means the token doesn't have access to the repository. Check: -1. The repository is in the same organization (for `GITHUB_TOKEN`) -2. Or use a PAT with `repo` scope (for different organizations) -3. The token hasn't expired - -### Error: "Couldn't resolve host 'github.com'" - -This is a network issue, not authentication. Check your network connection. - -## Best Practices - -1. **Use HTTPS URLs** in `pyproject.toml` for better CI/CD compatibility -2. **Rely on `GITHUB_TOKEN`** for same-org packages (automatic and secure) -3. **Pin versions** using `rev`, `tag`, or specific commit SHA for reproducibility -4. **Use `gh` CLI** for local development (easier than managing tokens) -5. **Keep tokens secure** - never commit them to the repository - -## Related Documentation - -- [TOKEN_SETUP.md](TOKEN_SETUP.md) - Setting up Personal Access Tokens -- [GitHub Actions: Automatic token authentication](https://docs.github.com/en/actions/security-guides/automatic-token-authentication) -- [uv: Git dependencies](https://docs.astral.sh/uv/concepts/dependencies/#git-dependencies) diff --git a/.rhiza/docs/RELEASING.md b/.rhiza/docs/RELEASING.md deleted file mode 100644 index 5f4c51f6..00000000 --- a/.rhiza/docs/RELEASING.md +++ /dev/null @@ -1,99 +0,0 @@ -# Release Guide - -This guide covers the release process for Rhiza-based projects. - -## πŸš€ The Release Process - -The release process can be done in two separate steps (**Bump** then **Release**), or in a single step using **Publish**. - -### Option A: One-Step Publish (Recommended) - -Bump the version and release in a single flow: - -```bash -make publish -``` - -This combines the bump and release steps below into one interactive command. - -### Option B: Two-Step Process - -#### 1. Bump Version - -First, update the version in `pyproject.toml`: - -```bash -make bump -``` - -This command will interactively guide you through: -1. Selecting a bump type (patch, minor, major) or entering a specific version -2. Warning you if you're not on the default branch -3. Showing the current and new version -4. Prompting whether to commit the changes -5. Prompting whether to push the changes - -The script ensures safety by: -- Checking for uncommitted changes before bumping -- Validating that the tag doesn't already exist -- Verifying the version format - -#### 2. Release - -Once the version is bumped and committed, run the release command: - -```bash -make release -``` - -This command will interactively guide you through: -1. Checking if your branch is up-to-date with the remote -2. If your local branch is ahead, showing the unpushed commits and prompting you to push them -3. Creating a git tag (e.g., `v1.2.4`) -4. Pushing the tag to the remote, which triggers the GitHub Actions release workflow - -The script provides safety checks by: -- Warning if you're not on the default branch -- Verifying no uncommitted changes exist -- Checking if the tag already exists locally or on remote -- Showing the number of commits since the last tag - -### Checking Release Status - -After releasing, you can check the status of the release workflow and the latest release: - -```bash -make release-status -``` - -This will display: -- The last 5 release workflow runs with their status and conclusion -- The latest GitHub release details (tag, author, published time, status, URL) - -> **Note:** `release-status` is currently supported for GitHub repositories only. GitLab support is planned for a future release. - -## What Happens After Release - -The release workflow (`.github/workflows/rhiza_release.yml`) triggers on the tag push and: - -1. **Validates** - Checks the tag format and ensures no duplicate releases -2. **Builds** - Builds the Python package (if `pyproject.toml` exists) -3. **Drafts** - Creates a draft GitHub release with artifacts -4. **PyPI** - Publishes to PyPI (if not marked private) -5. **Devcontainer** - Publishes devcontainer image (if `PUBLISH_DEVCONTAINER=true`) -6. **Finalizes** - Publishes the GitHub release with links to PyPI and container images - -## Configuration Options - -### PyPI Publishing - -- Automatic if package is registered as a Trusted Publisher -- Use `PYPI_REPOSITORY_URL` and `PYPI_TOKEN` for custom feeds -- Mark as private with `Private :: Do Not Upload` in `pyproject.toml` - -### Devcontainer Publishing - -- Set repository variable `PUBLISH_DEVCONTAINER=true` to enable -- Override registry with `DEVCONTAINER_REGISTRY` variable (defaults to ghcr.io) -- Requires `.devcontainer/devcontainer.json` to exist -- Image published as `{registry}/{owner}/{repository}/devcontainer:vX.Y.Z` diff --git a/.rhiza/docs/TOKEN_SETUP.md b/.rhiza/docs/TOKEN_SETUP.md deleted file mode 100644 index 89959b66..00000000 --- a/.rhiza/docs/TOKEN_SETUP.md +++ /dev/null @@ -1,102 +0,0 @@ -# GitHub Personal Access Token (PAT) Setup - -This document explains how to set up a Personal Access Token (PAT) for the repository's automated workflows. - -## Why is PAT_TOKEN needed? - -The repository uses the `SYNC` workflow (`.github/workflows/rhiza_sync.yml`) to automatically synchronize with a template repository. When this workflow modifies files in `.github/workflows/`, GitHub requires special permissions that the default `GITHUB_TOKEN` doesn't have. - -According to GitHub's security policy: -- The default `GITHUB_TOKEN` **cannot** create or update workflow files (`.github/workflows/*.yml`) -- A Personal Access Token with the `workflow` scope **is required** to push changes to workflow files - -## Creating a PAT with workflow scope - -Follow these steps to create a properly scoped Personal Access Token: - -### 1. Navigate to GitHub Settings - -1. Go to [GitHub.com](https://github.com) -2. Click your profile picture (top-right corner) -3. Click **Settings** -4. Scroll down and click **Developer settings** (bottom of left sidebar) -5. Click **Personal access tokens** β†’ **Tokens (classic)** - -### 2. Generate a new token - -1. Click **Generate new token** β†’ **Generate new token (classic)** -2. Give your token a descriptive name, e.g., `TinyCTA Workflow Sync Token` -3. Set an expiration date (recommended: 90 days or less for security) - -### 3. Select the required scopes - -**Required scopes:** -- βœ… `repo` (Full control of private repositories) - - This automatically includes all repo sub-scopes -- βœ… `workflow` (Update GitHub Action workflows) - - **This is critical** - without this scope, pushing workflow changes will fail - -**Optional but recommended:** -- `write:packages` (if the workflow publishes packages) - -### 4. Generate and copy the token - -1. Click **Generate token** at the bottom -2. **Important:** Copy the token immediately - you won't be able to see it again! -3. Store it securely (e.g., in a password manager) - -### 5. Add the token to repository secrets - -1. Navigate to your repository on GitHub -2. Click **Settings** tab -3. Click **Secrets and variables** β†’ **Actions** (left sidebar) -4. Click **New repository secret** -5. Name: `PAT_TOKEN` -6. Value: Paste the token you copied -7. Click **Add secret** - -## Verifying the setup - -After adding the `PAT_TOKEN` secret: - -1. Navigate to **Actions** tab in your repository -2. Find the **SYNC** workflow -3. Click **Run workflow** to manually trigger it -4. If workflow files are modified, the workflow should successfully push them - -## Troubleshooting - -### Error: "refusing to allow a GitHub App to create or update workflow" - -This error means either: -- The `PAT_TOKEN` secret is not set -- The `PAT_TOKEN` exists but lacks the `workflow` scope - -**Solution:** Create a new token with the `workflow` scope and update the `PAT_TOKEN` secret. - -### Error: "push_succeeded=false" - -This usually indicates: -- The token has expired -- The token was revoked -- The token lacks necessary permissions - -**Solution:** Generate a new token following the steps above and update the secret. - -## Security best practices - -1. **Limit scope:** Only grant the minimum required scopes (`repo` and `workflow`) -2. **Set expiration:** Use short-lived tokens (30-90 days) and rotate them regularly -3. **Monitor usage:** Regularly review your token usage in GitHub settings -4. **Revoke unused tokens:** Delete tokens that are no longer needed -5. **Use separate tokens:** Don't reuse tokens across multiple projects - -## Alternative: GitHub App (Advanced) - -For organizations, consider using a GitHub App instead of PAT: -- More secure and granular permissions -- Better audit logging -- No expiration issues -- Requires more setup complexity - -Refer to [GitHub's documentation](https://docs.github.com/en/apps) for details on creating GitHub Apps. diff --git a/.rhiza/docs/WORKFLOWS.md b/.rhiza/docs/WORKFLOWS.md deleted file mode 100644 index 9025fe29..00000000 --- a/.rhiza/docs/WORKFLOWS.md +++ /dev/null @@ -1,248 +0,0 @@ -# Development Workflows - -This guide covers recommended day-to-day development workflows for Rhiza projects. - -## Dependency Management - -Rhiza uses [uv](https://docs.astral.sh/uv/) for fast, reliable Python dependency management. - -> πŸ“š **For detailed information about dependency version constraints and rationale**, see [docs/DEPENDENCIES.md](../../docs/DEPENDENCIES.md) - -### Adding Dependencies - -**Recommended: Use `uv add`** β€” handles everything in one step: - -```bash -# Add a runtime dependency -uv add requests - -# Add a development dependency -uv add --dev pytest-xdist - -# Add with version constraint -uv add "pandas>=2.0" -``` - -This command: -1. Updates `pyproject.toml` -2. Resolves and updates `uv.lock` -3. Installs the package into your active venv - -### Manual Editing - -If you prefer to edit `pyproject.toml` directly: - -```bash -# After editing pyproject.toml, sync your environment -uv sync -``` - -> ⚠️ **Important:** Editing `pyproject.toml` alone does **not** update `uv.lock` or your venv. You must run `uv sync` afterward. - -**Safety nets:** -- `make install` checks if `uv.lock` is in sync with `pyproject.toml` and fails with a helpful message if not -- A pre-commit hook runs `uv lock` to ensure the lock file is updated before committing -- CI will fail if you forget to update the lock file - -### Removing Dependencies - -```bash -uv remove requests -``` - -### Command Reference - -| Goal | Command | -|------|---------| -| Add a runtime dependency | `uv add ` | -| Add a dev dependency | `uv add --dev ` | -| Remove a dependency | `uv remove ` | -| Sync after manual edits | `uv sync` | -| Update lock file only | `uv lock` | -| Upgrade a package | `uv lock --upgrade-package ` | -| Upgrade all packages | `uv lock --upgrade` | - -## Development Cycle - -### Starting Work - -```bash -# Ensure your environment is up to date -make install - -# Create a feature branch -git checkout -b feature/my-feature -``` - -### Making Changes - -1. **Write code** in `src/` -2. **Write tests** in `tests/` -3. **Run tests frequently:** - ```bash - make test - ``` -4. **Format before committing:** - ```bash - make fmt - ``` - -### Pre-Commit Checklist - -Before committing, run these checks: - -```bash -make fmt # Format and lint -make test # Run all tests -make deptry # Check for dependency issues -``` - -Or run all pre-commit hooks at once: - -```bash -make pre-commit -``` - -### Committing Changes - -Use [Conventional Commits](https://www.conventionalcommits.org/) format: - -```bash -git commit -m "feat: add new widget component" -git commit -m "fix: resolve null pointer in parser" -git commit -m "docs: update API reference" -git commit -m "chore: update dependencies" -``` - -Common prefixes: -- `feat:` β€” New feature -- `fix:` β€” Bug fix -- `docs:` β€” Documentation only -- `test:` β€” Adding/updating tests -- `chore:` β€” Maintenance tasks -- `refactor:` β€” Code refactoring - -### Skipping CI - -For documentation-only or trivial changes: - -```bash -git commit -m "docs: fix typo [skip ci]" -``` - -## Running Python Code - -Always use `uv run` to ensure the correct environment: - -```bash -# Run a script -uv run python scripts/my_script.py - -# Run a module -uv run python -m mymodule - -# Run tests directly -uv run pytest tests/test_specific.py -v - -# Interactive Python -uv run python -``` - -## Testing Workflows - -### Run All Tests - -```bash -make test -``` - -### Run Specific Tests - -```bash -# Single file -uv run pytest tests/test_rhiza/test_makefile.py -v - -# Single test function -uv run pytest tests/test_rhiza/test_makefile.py::test_specific_function -v - -# Tests matching a pattern -uv run pytest -k "test_pattern" -v - -# With print output -uv run pytest -v -s -``` - -### Run with Coverage - -```bash -make test # Coverage is included by default -``` - -## Releasing - -See [RELEASING.md](RELEASING.md) for the complete release workflow. - -Quick reference: - -```bash -# Bump version and release in one step (recommended) -make publish - -# Bump version (interactive) -make bump - -# Bump specific version -make bump BUMP=patch # 1.0.0 β†’ 1.0.1 -make bump BUMP=minor # 1.0.0 β†’ 1.1.0 -make bump BUMP=major # 1.0.0 β†’ 2.0.0 - -# Create and push release tag (without bump) -make release - -# Check release workflow status and latest release -make release-status -``` - -## Template Synchronization - -Keep your project in sync with upstream Rhiza templates: - -```bash -make sync -``` - -This updates shared configurations while preserving your customizations in `local.mk`. - -## Troubleshooting - -### Environment Out of Sync - -If your environment seems broken or out of date: - -```bash -# Full reinstall -rm -rf .venv -make install -``` - -### Lock File Conflicts - -If `uv.lock` has merge conflicts: - -```bash -# Accept current pyproject.toml as source of truth -git checkout --theirs uv.lock # or --ours depending on your situation -uv lock -``` - -### Dependency Check Failures - -If `make deptry` reports issues: - -```bash -# Missing dependencies β€” add them -uv add - -# Unused dependencies β€” remove them -uv remove -``` diff --git a/.rhiza/requirements/docs.txt b/.rhiza/requirements/docs.txt deleted file mode 100644 index 7d12b67f..00000000 --- a/.rhiza/requirements/docs.txt +++ /dev/null @@ -1,3 +0,0 @@ -# Documentation dependencies for rhiza -pdoc>=16.0.0 -interrogate>=1.7.0 diff --git a/.rhiza/requirements/marimo.txt b/.rhiza/requirements/marimo.txt deleted file mode 100644 index 032c8723..00000000 --- a/.rhiza/requirements/marimo.txt +++ /dev/null @@ -1,2 +0,0 @@ -# Marimo dependencies for rhiza -marimo>=0.18.0 From 3c7436d4c0b5d5b9a0fb04befbb44eb0c9fa759b Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Tue, 10 Mar 2026 15:44:52 +0400 Subject: [PATCH 8/9] Remove devcontainer dependency from release workflow --- .github/workflows/rhiza_release.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/rhiza_release.yml b/.github/workflows/rhiza_release.yml index 29d6ff37..bcc0e31e 100644 --- a/.github/workflows/rhiza_release.yml +++ b/.github/workflows/rhiza_release.yml @@ -290,7 +290,7 @@ jobs: name: Finalise Release runs-on: ubuntu-latest needs: [tag, pypi] - if: needs.pypi.result == 'success' || needs.devcontainer.result == 'success' + if: needs.pypi.result == 'success' steps: - name: Checkout Code uses: actions/checkout@v6.0.2 @@ -344,6 +344,5 @@ jobs: draft: false append_body: true body: | - ${{ steps.devcontainer_link.outputs.message }} ${{ steps.pypi_link.outputs.message }} From c88c1154e08c36e8f282004aebb96b71fa6d5874 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Tue, 10 Mar 2026 15:46:59 +0400 Subject: [PATCH 9/9] Remove references to marimo and docs dependencies from requirements README --- .rhiza/requirements/README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.rhiza/requirements/README.md b/.rhiza/requirements/README.md index 4feff9a7..7e80ec4f 100644 --- a/.rhiza/requirements/README.md +++ b/.rhiza/requirements/README.md @@ -5,8 +5,6 @@ This folder contains the development dependencies for the Rhiza project, organiz ## Files - **tests.txt** - Testing dependencies (pytest, pytest-cov, pytest-html) -- **marimo.txt** - Marimo notebook dependencies -- **docs.txt** - Documentation generation dependencies (pdoc) - **tools.txt** - Development tools (pre-commit, python-dotenv) ## Usage @@ -17,8 +15,6 @@ To install specific requirement files manually: ```bash uv pip install -r .rhiza/requirements/tests.txt -uv pip install -r .rhiza/requirements/marimo.txt -uv pip install -r .rhiza/requirements/docs.txt uv pip install -r .rhiza/requirements/tools.txt ```