Skip to content

sync rhiza template v0.18.8 - #2

Closed
tschm wants to merge 2 commits into
masterfrom
rhiza2
Closed

tschm wants to merge 2 commits into
masterfrom
rhiza2

Conversation

@tschm

@tschm tschm commented Jun 8, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Bump rhiza ref from v0.18.6 to v0.18.8 in .rhiza/template.yml
  • Run rhiza sync to materialize 77 template files (GitHub workflows, Makefile, tooling config, docs scaffolding, rhiza tests)

Test plan

  • CI passes on this branch

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Chores
    • Established comprehensive Makefile infrastructure with development targets for testing, building, and documentation.
    • Added GitHub Actions workflows for CI/CD, code quality checks, releases, and benchmarking.
    • Configured linting, formatting, and security scanning via pre-commit hooks and Ruff.
    • Added GitHub issue/PR templates and discussion templates for standardized contributions.
    • Established testing framework supporting unit, property-based, and stress testing with coverage tracking.

tschm and others added 2 commits June 8, 2026 12:50
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 8, 2026 08:52
@coderabbitai

coderabbitai Bot commented Jun 8, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Repository-wide configuration, Rhiza Makefile support, GitHub workflow/template files, requirements manifests, utility scripts, docs, and tests were added or updated to standardize project bootstrap, quality checks, releases, and validation.

Changes

Rhiza bootstrap, workflows, and tests

Layer / File(s) Summary
Repo config and GitHub templates
.*, .github/*, pytest.ini, ruff.toml
Repository formatting, ignore, release, secret-scanning, Dependabot, PR/discussion templates, and workflow configuration files are added or updated.
Rhiza Makefiles and task modules
Makefile, .rhiza/rhiza.mk, .rhiza/make.d/*
The root Makefile and new Rhiza make fragments define shared variables, bootstrap/install/clean, quality, release, testing, docs, book, marimo, doctor, and custom task targets plus hook placeholders.
Requirements manifests and docs
.rhiza/requirements/*
The requirements directory readme and dependency manifests are added for docs, tests, marimo, and tooling.
Test utilities and fixtures
.rhiza/tests/*/conftest.py, .rhiza/tests/test_utils.py
Pytest utilities, mock git/make fixtures, and API/sync/stress conftest files are added for Makefile, repository, and environment-based tests.
Test suites and validation checks
.rhiza/tests/**/*test*.py, .rhiza/tests/shell/test_scripts.sh
API, integration, structure, sync, and shell tests validate Makefile targets, variable overrides, GitHub workflows, docs examples, project layout, pyproject metadata, README code blocks, doctests, and the shell scripts.
Utility scripts and documentation
.rhiza/utils/*, docs/**/*, .rhiza/completions/*, .rhiza/template.yml, .rhiza/.cfg.toml, .rhiza/.env, .rhiza/.gitignore, .rhiza/.rhiza-version, .python-version
The new CLI utilities, MkDocs base config, development docs, and README/index updates are added, along with version and template metadata files.

Sequence Diagram(s)

Skipped.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐰 I hop through configs, docs, and tests with glee,
Make targets, workflows, and scripts all agree.
The burrow is tidy, the carrots are neat,
My whiskers twitch at each little feat.
A fluffy deploy of order and cheer —
Hop hop, the Rhiza garden is here!

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch rhiza2

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This pull request updates the repository to the Rhiza template v0.18.8 and materializes the corresponding template-managed files (tooling config, Makefile API, docs scaffolding, and Rhiza validation tests) to align the repo with the latest template conventions.

Changes:

  • Bumps .rhiza/template.yml ref to v0.18.8 and records the synced state in .rhiza/template.lock.
  • Introduces/updates repo-wide developer tooling configuration (Ruff, pytest, pre-commit, editorconfig, gitignore, Semgrep/Bandit).
  • Adds a substantial Rhiza-managed Makefile framework, docs baseline, and a comprehensive .rhiza/tests/ suite to validate template integration.

Reviewed changes

Copilot reviewed 75 out of 79 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
ruff.toml Adds Ruff lint/format configuration for the repo.
pytest.ini Adds pytest defaults (paths, logging, markers, timeout).
Makefile Switches to Rhiza-managed Makefile API include and sets key overrides.
docs/mkdocs-base.yml Adds base MkDocs configuration used by Rhiza book tooling.
docs/index.md Adds docs homepage content via snippet include.
docs/development/TESTS.md Adds documentation for property/benchmark/stress testing approach.
docs/development/MARIMO.md Adds documentation for Marimo notebook usage and CI integration.
.rhiza/utils/suppression_audit.py Adds a utility to audit inline suppressions and report density/grade.
.rhiza/utils/pip_audit_policy.py Adds a tiered pip-audit policy helper (fail runtime vulns, warn tooling).
.rhiza/tests/utils/test_git_repo_fixture.py Adds tests validating the git_repo fixture behavior.
.rhiza/tests/test_utils.py Adds shared helpers for Rhiza tests (make runner, ansi stripping, git setup).
.rhiza/tests/sync/test_readme_validation.py Adds tests that validate runnable README code blocks (+ skip flag support).
.rhiza/tests/sync/test_docstrings.py Adds doctest discovery runner across source modules (skips when absent).
.rhiza/tests/sync/conftest.py Adds sync-test fixtures for isolated temp environments.
.rhiza/tests/structure/test_requirements.py Adds tests enforcing .rhiza/requirements structure/contents.
.rhiza/tests/structure/test_pyproject.py Adds pyproject structure/version policy tests (including tag matching).
.rhiza/tests/structure/test_project_layout.py Adds tests enforcing expected repo layout/config presence.
.rhiza/tests/stress/README.md Adds documentation for stress test suite and execution options.
.rhiza/tests/stress/conftest.py Adds stress test CLI options/fixtures (iterations/workers).
.rhiza/tests/stress/init.py Declares stress test module.
.rhiza/tests/shell/test_scripts.sh Adds shell-script validation test harness (shebang/strict mode/syntax).
.rhiza/tests/README.md Adds overview documentation for the Rhiza test suite organization.
.rhiza/tests/integration/test_virtual_env_unexport.py Adds integration test asserting VIRTUAL_ENV is not exported to uv.
.rhiza/tests/integration/test_test_mk.py Adds integration test covering missing-tests behavior in test.mk.
.rhiza/tests/integration/test_docs_targets.py Adds integration tests for book targets and MKDOCS_EXTRA_PACKAGES.
.rhiza/tests/integration/test_book_targets.py Adds resilience tests for book-related Makefile targets.
.rhiza/tests/conftest.py Adds root fixtures and a sandboxed git repo fixture used across .rhiza/tests.
.rhiza/tests/api/test_makefile_targets.py Adds API tests validating Make targets and dry-run behavior.
.rhiza/tests/api/test_makefile_api.py Adds tests verifying Makefile API extension/override behavior.
.rhiza/tests/api/test_make_variable_overrides.py Adds tests for important Make variable overrides (coverage/licenses/python).
.rhiza/tests/api/test_github_targets.py Adds tests ensuring GitHub-related Make targets exist and are runnable.
.rhiza/tests/api/conftest.py Adds API test environment setup and split Makefile list.
.rhiza/template.yml Updates Rhiza template reference from v0.18.6 to v0.18.8.
.rhiza/template.lock Records the synced template SHA/ref and materialized file list.
.rhiza/semgrep.yml Adds Semgrep rules (numpy security/repro/stability checks).
.rhiza/rhiza.mk Adds the core Rhiza Makefile entrypoint and workflow targets/hooks.
.rhiza/requirements/tools.txt Adds pinned tool requirements used by Rhiza targets/tests.
.rhiza/requirements/tests.txt Adds test requirements (pytest, coverage, hypothesis, benchmark deps).
.rhiza/requirements/README.md Documents the purpose and usage of requirements files.
.rhiza/requirements/marimo.txt Adds Marimo dependency requirements.
.rhiza/requirements/docs.txt Adds docs tooling requirements (interrogate/mike/zensical).
.rhiza/make.d/test.mk Adds test/benchmark/security/docs-coverage/stress/mutation Make targets.
.rhiza/make.d/releasing.mk Adds bump/release/publish Make targets with DRY_RUN support.
.rhiza/make.d/quality.mk Adds fmt/deptry/license/semgrep/suppression-audit and quality meta target.
.rhiza/make.d/marimo.mk Adds marimo and marimo-validate Make targets.
.rhiza/make.d/doctor.mk Adds make doctor prerequisite checks with version enforcement.
.rhiza/make.d/custom-task.mk Adds example custom task + post-install hook extension point.
.rhiza/make.d/custom-env.mk Adds example custom environment overrides.
.rhiza/make.d/bootstrap.mk Adds install/bootstrap targets including uv install and dependency sync.
.rhiza/make.d/book.mk Adds book build/serve targets and reports/notebook export integration.
.rhiza/completions/rhiza-completion.zsh Adds zsh completion for make targets/vars.
.rhiza/completions/rhiza-completion.bash Adds bash completion for make targets/vars.
.rhiza/completions/README.md Documents installing/using shell completion scripts.
.rhiza/.rhiza-version Sets rhiza-tools version used by Makefile workflows.
.rhiza/.gitignore Adds Rhiza-specific ignore rules (notably keeping .env).
.rhiza/.env Adds Rhiza env defaults (SOURCE_FOLDER/MARIMO_FOLDER/CI matrix).
.rhiza/.cfg.toml Adds bumpversion configuration used by Rhiza release tooling.
.python-version Adds/updates Python version pin for toolchain/workflows.
.pre-commit-config.yaml Adds pre-commit hooks for linting/formatting/security/config validation.
.gitignore Replaces/expands ignore rules to match Rhiza template conventions.
.github/workflows/rhiza_weekly.yml Adds weekly workflow stub delegating to Rhiza reusable workflow.
.github/workflows/rhiza_sync.yml Adds sync workflow stub and schedule to keep template current.
.github/workflows/rhiza_release.yml Adds full release workflow (SBOM, changelog, publish, devcontainer).
.github/workflows/rhiza_marimo.yml Adds Marimo CI workflow stub.
.github/workflows/rhiza_codeql.yml Adds CodeQL workflow stub.
.github/workflows/rhiza_ci.yml Adds CI workflow stub.
.github/workflows/rhiza_book.yml Adds docs/book workflow stub for GitHub Pages publishing.
.github/workflows/rhiza_benchmark.yml Adds benchmark workflow stub.
.github/secret_scanning.yml Adds secret scanning configuration and ignore paths.
.github/release.yml Adds release-notes categorization configuration.
.github/pull_request_template.md Adds a PR template with testing/checklist prompts.
.github/ISSUE_TEMPLATE/feature_request.yml Adds feature request issue form.
.github/ISSUE_TEMPLATE/bug_report.yml Adds bug report issue form.
.github/DISCUSSION_TEMPLATE/q-and-a.yml Adds discussion template for Q&A.
.github/dependabot.yml Adds Dependabot configuration for uv and GitHub Actions.
.editorconfig Adds EditorConfig rules for consistent formatting across file types.
.bandit Adds Bandit config (e.g., skip assert warnings).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .python-version
Comment on lines +1 to +2

3.12
Comment thread ruff.toml
Comment on lines +5 to +8
line-length = 120
# Target Python version
target-version = "py311"

Comment thread docs/mkdocs-base.yml
Comment on lines +101 to +105
custom_checkbox: true
- pymdownx.tilde
- pymdownx.snippets:
base_path: ["."]

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 15

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.rhiza/tests/api/test_make_variable_overrides.py (1)

157-162: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

This test does not verify TESTS_FOLDER usage yet.

Line 158 claims path validation, but Line 161 only checks that pytest appears. That can pass even if Makefile ignores TESTS_FOLDER.

Proposed fix
     proc = run_make(logger, ["test"])
-    # The default tests folder must appear somewhere in the pytest invocation
-    assert "pytest" in proc.stdout
+    assert "pytest" in proc.stdout
+    assert " tests" in proc.stdout or " tests/" in proc.stdout
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.rhiza/tests/api/test_make_variable_overrides.py around lines 157 - 162, The
test test_pytest_uses_tests_folder currently only asserts that "pytest" appears
in proc.stdout but does not verify that the Makefile used the TESTS_FOLDER path;
update the test to parse proc.stdout (from run_make) and assert that the pytest
invocation includes the TESTS_FOLDER value (or a placeholder variable like
TESTS_FOLDER) or the actual tests path string—e.g., locate the command string in
proc.stdout and assert it contains TESTS_FOLDER (or the expected path) so the
test fails if the Makefile ignores TESTS_FOLDER.
🟡 Minor comments (9)
.github/DISCUSSION_TEMPLATE/q-and-a.yml-7-7 (1)

7-7: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix wording typo in the welcome copy.

“share how your use-case” reads incorrectly; should be “share your use case”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/DISCUSSION_TEMPLATE/q-and-a.yml at line 7, Fix the wording typo in
the welcome copy string "Welcome! Use this space to ask questions, share how
your use-case, or explore ideas with the community." by changing "share how your
use-case" to "share your use case" so the sentence reads correctly; update the
YAML value for the welcome message (the string starting with "Welcome! Use this
space...") in .github/DISCUSSION_TEMPLATE/q-and-a.yml accordingly, keeping
punctuation and spacing consistent.
.rhiza/completions/rhiza-completion.bash-31-32 (1)

31-32: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add missing variable completions for Bash parity.

Bash completion currently omits COVERAGE_FAIL_UNDER= and PYTHON_VERSION= even though they’re exposed in the docs/Zsh path, so shell behavior diverges.

Suggested patch
-    local vars="DRY_RUN=1 BUMP=patch BUMP=minor BUMP=major ENV=dev ENV=staging ENV=prod"
+    local vars="DRY_RUN=1 BUMP=patch BUMP=minor BUMP=major ENV=dev ENV=staging ENV=prod COVERAGE_FAIL_UNDER= PYTHON_VERSION="
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.rhiza/completions/rhiza-completion.bash around lines 31 - 32, The Bash
completion list in the vars variable (currently "DRY_RUN=1 BUMP=patch BUMP=minor
BUMP=major ENV=dev ENV=staging ENV=prod") is missing the COVERAGE_FAIL_UNDER=
and PYTHON_VERSION= entries; update the vars string in
.rhiza/completions/rhiza-completion.bash to include "COVERAGE_FAIL_UNDER=" and
"PYTHON_VERSION=" (following the same key=example pattern) so opts="$opts $vars"
will expose those completions and match the Zsh/docs behavior.
.rhiza/tests/integration/test_book_targets.py-34-38 (1)

34-38: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Also assert successful dry-run exit codes in these target loops.

Line 36/63 only checks one stderr phrase, so unrelated failures can still pass these tests.

Proposed fix
         result = subprocess.run([MAKE, "-n", target], cwd=git_repo, capture_output=True, text=True)  # nosec
+        assert result.returncode == 0, f"Dry-run failed for {target}: {result.stderr}"
         # 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/"
         )

Also applies to: 61-65

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.rhiza/tests/integration/test_book_targets.py around lines 34 - 38, The test
currently only checks that the stderr doesn't contain "no rule to make target"
after calling subprocess.run([MAKE, "-n", target], cwd=git_repo,
capture_output=True, text=True) but doesn't assert the dry-run exit code; update
the assertions in both target loops (the calls to subprocess.run using MAKE and
"-n") to also assert that result.returncode == 0 (or otherwise explicitly check
for a successful exit code) so unrelated failures with nonzero return codes fail
the test as well.
.rhiza/tests/README.md-80-145 (1)

80-145: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use a single code-fence style to satisfy MD048.

Line 80, Line 87, Line 98, Line 110, Line 115, Line 120, and Line 145 use backtick fences while the repo lint rule expects tilde fences. This will keep markdownlint warnings active.

Suggested fix pattern
-```bash
+~~~bash
 uv run pytest .rhiza/tests/
 # or
 make test
-```
+~~~
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.rhiza/tests/README.md around lines 80 - 145, Replace the backtick-style
code fences (``` and ```bash) in .rhiza/tests/README.md with tilde-style fences
(~~~ and ~~~bash) to satisfy MD048; specifically update each fenced block shown
in the diff (the examples starting with ```bash, the category lists, stress test
examples, specific test file, verbose/coverage blocks, and the Import Patterns
block) so the opening and closing fences use tildes (~~~) instead of backticks,
ensuring language tags (e.g., bash) are preserved.

Source: Linters/SAST tools

.rhiza/tests/api/test_make_variable_overrides.py-77-85 (1)

77-85: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

This test can silently pass without validating file-based version resolution.

Line 80 gates all assertions on python_version_file.exists(). If setup regresses and the file is missing, this test still passes.

Proposed fix
     python_version_file = tmp_path / ".python-version"
-    if python_version_file.exists():
-        version = python_version_file.read_text().strip()
-        proc = run_make(logger, ["print-PYTHON_VERSION"], dry_run=False)
-        out = strip_ansi(proc.stdout)
-        assert version in out, f"Expected {version} in PYTHON_VERSION output; got: {out}"
+    assert python_version_file.exists(), "Expected setup fixture to copy .python-version"
+    version = python_version_file.read_text().strip()
+    proc = run_make(logger, ["print-PYTHON_VERSION"], dry_run=False)
+    out = strip_ansi(proc.stdout)
+    assert version in out, f"Expected {version} in PYTHON_VERSION output; got: {out}"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.rhiza/tests/api/test_make_variable_overrides.py around lines 77 - 85, The
test test_python_version_read_from_python_version_file currently skips
assertions when python_version_file doesn't exist; change it to ensure
deterministic behavior by either creating the .python-version file in tmp_path
(write a known version string) before calling run_make, or assert
python_version_file.exists() immediately so the test fails if setup is wrong;
update references to python_version_file, run_make, and strip_ansi accordingly
so the test always validates file-based resolution rather than silently passing.
.rhiza/tests/api/test_make_variable_overrides.py-141-146 (1)

141-146: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Assertion does not test the stated override behavior.

Line 142 says this checks non-overridability to empty, but Line 143 never attempts an override. This currently only checks output shape.

Proposed fix
-    proc = run_make(logger, ["print-UV_NO_MODIFY_PATH"], dry_run=False)
+    proc = run_make(
+        logger,
+        ["print-UV_NO_MODIFY_PATH", "UV_NO_MODIFY_PATH="],
+        dry_run=False,
+    )
     out = strip_ansi(proc.stdout)
-    assert "UV_NO_MODIFY_PATH" in out
+    assert "Value of UV_NO_MODIFY_PATH" in out
+    assert "\n1" in out
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.rhiza/tests/api/test_make_variable_overrides.py around lines 141 - 146, The
test test_uv_no_modify_path_cannot_be_overridden_to_empty currently only prints
UV_NO_MODIFY_PATH without attempting an override; update it to call run_make in
a way that tries to override UV_NO_MODIFY_PATH to an empty string (e.g., passing
an override argument that sets "UV_NO_MODIFY_PATH=") and then assert the printed
output from run_make (use strip_ansi(proc.stdout)) still contains
"UV_NO_MODIFY_PATH" and is not empty/was not replaced by an empty value; keep
using the same helper run_make and dry_run=False to exercise the real behavior.
.rhiza/tests/structure/test_project_layout.py-55-57 (1)

55-57: ⚠️ Potential issue | 🟡 Minor

Avoid pytest.skip() in the optional-files loop (.rhiza/tests/structure/test_project_layout.py).
pytest.skip() raises a Skip exception and stops execution of the current test on the first missing optional file, preventing checks for the rest. For missing optional files, continue instead.

Suggested patch
         for filename in optional_files:
             if not (root / filename).exists():
-                pytest.skip(f"Optional file {filename} not present in this project")
+                continue
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.rhiza/tests/structure/test_project_layout.py around lines 55 - 57, The loop
over optional_files in test_project_layout.py incorrectly calls pytest.skip()
which aborts the test on the first missing optional file; change the behavior in
the loop (the block iterating optional_files) to use continue instead of
pytest.skip() so the test will simply skip that filename and continue checking
the remaining optional_files (locate the for filename in optional_files loop and
replace pytest.skip(...) with a continue).
.rhiza/tests/sync/test_readme_validation.py-60-61 (1)

60-61: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Concatenating Python blocks without separators can corrupt syntax.

Merging with an empty separator can join trailing/leading tokens across blocks.

Suggested fix
-    code = "".join(code_blocks)  # merged code
+    code = "\n\n".join(code_blocks)  # merged code
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.rhiza/tests/sync/test_readme_validation.py around lines 60 - 61, The test
merges code and expected result blocks using "".join which can fuse tokens
across block boundaries; change the joins to include a safe separator (e.g. "\n"
or "\n\n") so use code = "\n\n".join(code_blocks) (or "\n".join if single-line
separation is preferred) and similarly expected = "\n\n".join(result_blocks) to
preserve line breaks and avoid corrupting Python syntax in the merged strings.
docs/development/TESTS.md-157-163 (1)

157-163: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add language identifiers to unlabeled fenced code blocks.

Line 157 and Line 250 use fenced blocks without a language, which triggers MD040 and can fail markdown linting depending on CI settings. Use explicit labels (for example, text / ini / bash) to keep docs lint-clean.

Also applies to: 250-257

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/development/TESTS.md` around lines 157 - 163, The fenced code blocks
showing the benchmark table (the block starting with
"--------------------------------------------------- benchmark: 1 tests
---------------------------------------------------") and the other unlabeled
block later should be given explicit language identifiers to satisfy MD040; edit
those backticks to include a label such as "text" or "ini" (e.g., change ``` to
```text) so the benchmark table and related examples are lint-clean while
keeping the block contents unchanged.

Source: Linters/SAST tools

🧹 Nitpick comments (1)
.rhiza/requirements/tools.txt (1)

7-7: ⚡ Quick win

Pin ty to an exact version for reproducible tooling installs.

Using >= here makes local/CI behavior drift over time compared with the other pinned tool deps.

Proposed change
-ty>=0.0.30
+ty==0.0.30
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.rhiza/requirements/tools.txt at line 7, Replace the loose version specifier
for the tool package 'ty' with an exact pin to ensure reproducible installs:
change the entry currently written as 'ty>=0.0.30' to use an exact equality pin
(e.g., 'ty==0.0.30') in the tools requirements so it matches the other pinned
tool dependencies.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.bandit:
- Line 2: Remove the global "skips = B101" entry from the .bandit config and
instead scope the suppression to test code by adding inline suppressions only in
test files that intentionally use assert (e.g., add "# nosec" on the specific
assert lines in your test functions); this preserves B101 checks for runtime
code while allowing asserts in tests—search for the literal "skips = B101" to
remove it and update test files that relied on the global suppression to use "#
nosec" on the relevant assert statements.

In @.github/secret_scanning.yml:
- Around line 14-20: The current paths-ignore block is too broad (entries
docs/**/*.md and book/**) and disables secret scanning for large human-authored
content; update the paths-ignore to remove or tighten those two patterns and
replace them with narrow, synthetic-only patterns (e.g., patterns targeting
fixture/example files under docs and book such as docs/**/fixtures/**,
docs/**/*.example.* or book/**/examples/**) so that paths-ignore still contains
the original fixture skips but no longer excludes all docs or the entire book;
edit the entries under the paths-ignore key (referencing the symbols
paths-ignore, docs/**/*.md, book/**) accordingly.

In @.github/workflows/rhiza_benchmark.yml:
- Around line 17-19: The workflow's push trigger currently only lists the branch
name "main" under the push->branches key, so pushes to the repository's active
branch "master" won't trigger the benchmark; update the YAML push trigger (the
branches array under the push event) to include "master" as well (e.g., add
"master" alongside "main" or replace "main" with "master" depending on intended
behavior) so the workflow will run on pushes to the repository's active branch.

In @.github/workflows/rhiza_release.yml:
- Around line 105-110: Top-level workflow permissions currently grant write to
contents, id-token, packages, and attestations for all jobs; change the
top-level permissions to least-privilege (e.g., contents: read and minimal
defaults) and move write scopes into only the jobs that require them (the
release/publish/attestation jobs). Specifically, remove or downgrade the write
entries under the top-level permissions block and add per-job permissions
entries for the jobs that perform release/publish/attestation so that contents:
write, id-token: write, packages: write, and attestations: write appear only in
the corresponding release/publish/attestation job definitions (referencing the
job names that run release/publish/attestation).
- Line 583: The workflow exports UV_EXTRA_INDEX_URL using
secrets.uv_extra_index_url which mismatches the declared secret key
UV_EXTRA_INDEX_URL (GitHub secrets are case-sensitive); update the expression to
use secrets.UV_EXTRA_INDEX_URL (or rename the declared secret to lowercase to
match) so the export line export UV_EXTRA_INDEX_URL="${{
secrets.UV_EXTRA_INDEX_URL }}" correctly references the existing secret.
- Around line 119-588: The workflow uses mutable action tags (e.g.,
actions/checkout@v6.0.2, astral-sh/setup-uv@v7.6.0, actions/setup-python@v6.2.0,
actions/attest@v4.1.0, actions/upload-artifact@v7.0.1,
actions/download-artifact@v8.0.1, actions/attest-build-provenance@v4,
ncipollo/release-action@v1.21.0, pypa/gh-action-pypi-publish@release/v1,
devcontainers/ci@v0.3, docker/login-action@v4.2.0,
jebel-quant/rhiza/.github/actions/configure-git-auth@v0.15.2); replace each
uses: reference with the corresponding immutable commit SHA (the full 40-char
commit) for that action version to prevent retargeting. For each occurrence
(search for the unique action strings above in the workflow), update the uses
value to the same repository@<commit-sha>, verify the SHA matches the tag
version in the action’s upstream repo, and run a workflow test to ensure no
breaking changes from the pinning.

In @.github/workflows/rhiza_weekly.yml:
- Line 32: Replace the blanket "secrets: inherit" used on the reusable workflow
call with an explicit secrets mapping: identify the reusable workflow call that
uses "uses:" and replace "secrets: inherit" with only the required secrets
mapped like "MY_SECRET: ${{ secrets.MY_SECRET }}" (and any other specific
secrets needed). Ensure you remove "secrets: inherit" and list each secret
explicitly to enforce least privilege.
- Line 31: Replace the mutable tag reference in the reusable workflow invocation
(the line containing "uses:
jebel-quant/rhiza/.github/workflows/rhiza_weekly.yml@v0.18.4") with the
corresponding full commit SHA for that workflow; update the "uses:" value to end
with the full 40-character commit SHA instead of "`@v0.18.4`" (you can optionally
retain the tag as a comment for human context).

In @.rhiza/.rhiza-version:
- Line 1: The repository's Rhiza template version is out of sync (file
.rhiza/.rhiza-version contains 0.16.1 while .rhiza/template.yml and GitHub
workflow reusable actions reference v0.18.x); update .rhiza/.rhiza-version to
match the template ref series (e.g., set it to v0.18.8) and confirm that
.rhiza/template.yml and workflows (the referenced reusable workflows/actions)
consistently use the same v0.18.x tag, or alternatively add a clear
compatibility note in the repo explaining that 0.16.1 is intentionally
compatible with v0.18.8 if you choose not to change the version.

In @.rhiza/make.d/book.mk:
- Around line 31-38: The loop over notebooks using the glob "for nb in
$(MARIMO_FOLDER)/*.py" can iterate the literal pattern when no .py files exist;
modify the loop to skip non-existent matches by adding a guard such as testing
each candidate with "[ -e "$$nb" ] || continue" at the start of the loop (or
pre-check the glob with "set -- $(MARIMO_FOLDER)/*.py; [ -e "$1" ] || continue")
so the export step (the line invoking "${UV_BIN} run marimo export html ...") is
only run for real files.

In @.rhiza/make.d/bootstrap.mk:
- Line 109: Replace the unsafe force-delete command "git branch -vv | awk '/:
gone]/{print $$1}' | xargs -r git branch -D" with a safer flow: identify stale
branches (the same grep/awk can remain), but pass them to "git branch -d"
instead of "-D" and/or first filter to only merged branches using "git branch
--merged" to avoid deleting unmerged work; ensure you skip the current branch
(e.g., check "$(git rev-parse --abbrev-ref HEAD)") and only delete when the
branch is merged or confirmed, so update the invocation that currently uses
xargs git branch -D to use a safe deletion command and optional confirmation.

In @.rhiza/make.d/quality.mk:
- Around line 18-24: The nested shell if/fi sequence in the Make recipe is
missing a command separator between the inner "fi" and the outer "fi", causing a
shell parse error; change the inner "fi \\" to include a semicolon before the
backslash (i.e. "fi; \\") so the shell receives a proper separator; locate the
block that uses MARIMO_FOLDER, SOURCE_FOLDER and calls $(UVX_BIN) -p
${PYTHON_VERSION} deptry and update the inner "fi" line to "fi; \\".

In @.rhiza/tests/structure/test_pyproject.py:
- Around line 145-151: The classifiers fixture currently calls pytest.skip when
the "classifiers" key is missing which hides missing required configuration;
change the fixture (def classifiers) to assert or raise a test failure (e.g.,
use pytest.fail or a direct assert) when project.get("classifiers") is empty so
the test fails loudly instead of skipping; apply the same change to the other
fixtures in this test file that currently call pytest.skip (the second similar
fixture) so missing required sections cause test failures rather than silent
skips.

In @.rhiza/tests/sync/conftest.py:
- Around line 78-80: The test fixture is writing an outdated template ref ("ref:
v0.7.1") into the minimal .rhiza/template.yml; update the string passed to
(tmp_path / ".rhiza" / "template.yml").write_text(...) to use the current
template ref "v0.18.8" so tests validate against the correct baseline and keep
the rest of the file content unchanged.

In `@docs/index.md`:
- Line 1: The README.md snippet reference in docs/index.md won't be found
because mkdocs-base.yml restricts snippet lookup to base_path: ["."] with
docs_dir: docs; update configuration so the snippet lookup includes the
repository root or the README location (for example expand base_path to include
".." or add the root path), or move/copy the README.md into the docs/ folder and
update the reference; edit docs/mkdocs-base.yml (look for base_path and
docs_dir) to add the correct path where README.md resides so the snippet in
docs/index.md resolves during the build.

---

Outside diff comments:
In @.rhiza/tests/api/test_make_variable_overrides.py:
- Around line 157-162: The test test_pytest_uses_tests_folder currently only
asserts that "pytest" appears in proc.stdout but does not verify that the
Makefile used the TESTS_FOLDER path; update the test to parse proc.stdout (from
run_make) and assert that the pytest invocation includes the TESTS_FOLDER value
(or a placeholder variable like TESTS_FOLDER) or the actual tests path
string—e.g., locate the command string in proc.stdout and assert it contains
TESTS_FOLDER (or the expected path) so the test fails if the Makefile ignores
TESTS_FOLDER.

---

Minor comments:
In @.github/DISCUSSION_TEMPLATE/q-and-a.yml:
- Line 7: Fix the wording typo in the welcome copy string "Welcome! Use this
space to ask questions, share how your use-case, or explore ideas with the
community." by changing "share how your use-case" to "share your use case" so
the sentence reads correctly; update the YAML value for the welcome message (the
string starting with "Welcome! Use this space...") in
.github/DISCUSSION_TEMPLATE/q-and-a.yml accordingly, keeping punctuation and
spacing consistent.

In @.rhiza/completions/rhiza-completion.bash:
- Around line 31-32: The Bash completion list in the vars variable (currently
"DRY_RUN=1 BUMP=patch BUMP=minor BUMP=major ENV=dev ENV=staging ENV=prod") is
missing the COVERAGE_FAIL_UNDER= and PYTHON_VERSION= entries; update the vars
string in .rhiza/completions/rhiza-completion.bash to include
"COVERAGE_FAIL_UNDER=" and "PYTHON_VERSION=" (following the same key=example
pattern) so opts="$opts $vars" will expose those completions and match the
Zsh/docs behavior.

In @.rhiza/tests/api/test_make_variable_overrides.py:
- Around line 77-85: The test test_python_version_read_from_python_version_file
currently skips assertions when python_version_file doesn't exist; change it to
ensure deterministic behavior by either creating the .python-version file in
tmp_path (write a known version string) before calling run_make, or assert
python_version_file.exists() immediately so the test fails if setup is wrong;
update references to python_version_file, run_make, and strip_ansi accordingly
so the test always validates file-based resolution rather than silently passing.
- Around line 141-146: The test
test_uv_no_modify_path_cannot_be_overridden_to_empty currently only prints
UV_NO_MODIFY_PATH without attempting an override; update it to call run_make in
a way that tries to override UV_NO_MODIFY_PATH to an empty string (e.g., passing
an override argument that sets "UV_NO_MODIFY_PATH=") and then assert the printed
output from run_make (use strip_ansi(proc.stdout)) still contains
"UV_NO_MODIFY_PATH" and is not empty/was not replaced by an empty value; keep
using the same helper run_make and dry_run=False to exercise the real behavior.

In @.rhiza/tests/integration/test_book_targets.py:
- Around line 34-38: The test currently only checks that the stderr doesn't
contain "no rule to make target" after calling subprocess.run([MAKE, "-n",
target], cwd=git_repo, capture_output=True, text=True) but doesn't assert the
dry-run exit code; update the assertions in both target loops (the calls to
subprocess.run using MAKE and "-n") to also assert that result.returncode == 0
(or otherwise explicitly check for a successful exit code) so unrelated failures
with nonzero return codes fail the test as well.

In @.rhiza/tests/README.md:
- Around line 80-145: Replace the backtick-style code fences (``` and ```bash)
in .rhiza/tests/README.md with tilde-style fences (~~~ and ~~~bash) to satisfy
MD048; specifically update each fenced block shown in the diff (the examples
starting with ```bash, the category lists, stress test examples, specific test
file, verbose/coverage blocks, and the Import Patterns block) so the opening and
closing fences use tildes (~~~) instead of backticks, ensuring language tags
(e.g., bash) are preserved.

In @.rhiza/tests/structure/test_project_layout.py:
- Around line 55-57: The loop over optional_files in test_project_layout.py
incorrectly calls pytest.skip() which aborts the test on the first missing
optional file; change the behavior in the loop (the block iterating
optional_files) to use continue instead of pytest.skip() so the test will simply
skip that filename and continue checking the remaining optional_files (locate
the for filename in optional_files loop and replace pytest.skip(...) with a
continue).

In @.rhiza/tests/sync/test_readme_validation.py:
- Around line 60-61: The test merges code and expected result blocks using
"".join which can fuse tokens across block boundaries; change the joins to
include a safe separator (e.g. "\n" or "\n\n") so use code =
"\n\n".join(code_blocks) (or "\n".join if single-line separation is preferred)
and similarly expected = "\n\n".join(result_blocks) to preserve line breaks and
avoid corrupting Python syntax in the merged strings.

In `@docs/development/TESTS.md`:
- Around line 157-163: The fenced code blocks showing the benchmark table (the
block starting with "---------------------------------------------------
benchmark: 1 tests ---------------------------------------------------") and the
other unlabeled block later should be given explicit language identifiers to
satisfy MD040; edit those backticks to include a label such as "text" or "ini"
(e.g., change ``` to ```text) so the benchmark table and related examples are
lint-clean while keeping the block contents unchanged.

---

Nitpick comments:
In @.rhiza/requirements/tools.txt:
- Line 7: Replace the loose version specifier for the tool package 'ty' with an
exact pin to ensure reproducible installs: change the entry currently written as
'ty>=0.0.30' to use an exact equality pin (e.g., 'ty==0.0.30') in the tools
requirements so it matches the other pinned tool dependencies.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b3e7ce44-1164-4b60-be58-408c3b286fcd

📥 Commits

Reviewing files that changed from the base of the PR and between 4bbcf45 and 1c8d9fc.

⛔ Files ignored due to path filters (3)
  • .rhiza/assets/rhiza-logo.svg is excluded by !**/*.svg
  • .rhiza/template.lock is excluded by !**/*.lock
  • docs/assets/rhiza-logo.svg is excluded by !**/*.svg
📒 Files selected for processing (76)
  • .bandit
  • .editorconfig
  • .github/DISCUSSION_TEMPLATE/q-and-a.yml
  • .github/ISSUE_TEMPLATE/bug_report.yml
  • .github/ISSUE_TEMPLATE/feature_request.yml
  • .github/dependabot.yml
  • .github/pull_request_template.md
  • .github/release.yml
  • .github/secret_scanning.yml
  • .github/workflows/rhiza_benchmark.yml
  • .github/workflows/rhiza_book.yml
  • .github/workflows/rhiza_ci.yml
  • .github/workflows/rhiza_codeql.yml
  • .github/workflows/rhiza_marimo.yml
  • .github/workflows/rhiza_release.yml
  • .github/workflows/rhiza_sync.yml
  • .github/workflows/rhiza_weekly.yml
  • .gitignore
  • .pre-commit-config.yaml
  • .python-version
  • .rhiza/.cfg.toml
  • .rhiza/.env
  • .rhiza/.gitignore
  • .rhiza/.rhiza-version
  • .rhiza/completions/README.md
  • .rhiza/completions/rhiza-completion.bash
  • .rhiza/completions/rhiza-completion.zsh
  • .rhiza/make.d/book.mk
  • .rhiza/make.d/bootstrap.mk
  • .rhiza/make.d/custom-env.mk
  • .rhiza/make.d/custom-task.mk
  • .rhiza/make.d/doctor.mk
  • .rhiza/make.d/marimo.mk
  • .rhiza/make.d/quality.mk
  • .rhiza/make.d/releasing.mk
  • .rhiza/make.d/test.mk
  • .rhiza/requirements/README.md
  • .rhiza/requirements/docs.txt
  • .rhiza/requirements/marimo.txt
  • .rhiza/requirements/tests.txt
  • .rhiza/requirements/tools.txt
  • .rhiza/rhiza.mk
  • .rhiza/semgrep.yml
  • .rhiza/template.yml
  • .rhiza/tests/README.md
  • .rhiza/tests/api/conftest.py
  • .rhiza/tests/api/test_github_targets.py
  • .rhiza/tests/api/test_make_variable_overrides.py
  • .rhiza/tests/api/test_makefile_api.py
  • .rhiza/tests/api/test_makefile_targets.py
  • .rhiza/tests/conftest.py
  • .rhiza/tests/integration/test_book_targets.py
  • .rhiza/tests/integration/test_docs_targets.py
  • .rhiza/tests/integration/test_test_mk.py
  • .rhiza/tests/integration/test_virtual_env_unexport.py
  • .rhiza/tests/shell/test_scripts.sh
  • .rhiza/tests/stress/README.md
  • .rhiza/tests/stress/__init__.py
  • .rhiza/tests/stress/conftest.py
  • .rhiza/tests/structure/test_project_layout.py
  • .rhiza/tests/structure/test_pyproject.py
  • .rhiza/tests/structure/test_requirements.py
  • .rhiza/tests/sync/conftest.py
  • .rhiza/tests/sync/test_docstrings.py
  • .rhiza/tests/sync/test_readme_validation.py
  • .rhiza/tests/test_utils.py
  • .rhiza/tests/utils/test_git_repo_fixture.py
  • .rhiza/utils/pip_audit_policy.py
  • .rhiza/utils/suppression_audit.py
  • Makefile
  • docs/development/MARIMO.md
  • docs/development/TESTS.md
  • docs/index.md
  • docs/mkdocs-base.yml
  • pytest.ini
  • ruff.toml

Comment thread .bandit
@@ -0,0 +1,2 @@
[bandit]
skips = B101

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Scope Bandit B101 suppression to tests instead of disabling it globally.

skips = B101 turns off assert-misuse detection repo-wide, which weakens security checks for runtime code. Keep B101 enabled globally and exclude/assert-ignore only test paths.

Suggested adjustment
 [bandit]
-skips = B101
+# Keep B101 enabled globally; suppress in tests via CLI/config path excludes.
+# Example (if supported in your Bandit invocation):
+# exclude_dirs = tests,.rhiza/tests
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.bandit at line 2, Remove the global "skips = B101" entry from the .bandit
config and instead scope the suppression to test code by adding inline
suppressions only in test files that intentionally use assert (e.g., add "#
nosec" on the specific assert lines in your test functions); this preserves B101
checks for runtime code while allowing asserts in tests—search for the literal
"skips = B101" to remove it and update test files that relied on the global
suppression to use "# nosec" on the relevant assert statements.

Comment on lines +14 to +20
paths-ignore:
# Ignore test fixtures that may contain example/fake secrets
- ".rhiza/tests/**"
- "tests/**"
# Ignore documentation that references example tokens/keys
- "docs/**/*.md"
- "book/**"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid excluding all docs and book content from secret scanning.

Line 19 and Line 20 disable scanning for large human-authored areas where accidental token pastes often happen. Keep fixture-style exclusions, but narrow docs/book ignores to specific synthetic test files only.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/secret_scanning.yml around lines 14 - 20, The current paths-ignore
block is too broad (entries docs/**/*.md and book/**) and disables secret
scanning for large human-authored content; update the paths-ignore to remove or
tighten those two patterns and replace them with narrow, synthetic-only patterns
(e.g., patterns targeting fixture/example files under docs and book such as
docs/**/fixtures/**, docs/**/*.example.* or book/**/examples/**) so that
paths-ignore still contains the original fixture skips but no longer excludes
all docs or the entire book; edit the entries under the paths-ignore key
(referencing the symbols paths-ignore, docs/**/*.md, book/**) accordingly.

Comment on lines +17 to +19
push:
branches:
- main

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Benchmark workflow trigger misses the active branch.

Line 19 only listens to main, but this repository’s PR target branch is master (June 8, 2026), so benchmark runs can be skipped on normal pushes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/rhiza_benchmark.yml around lines 17 - 19, The workflow's
push trigger currently only lists the branch name "main" under the
push->branches key, so pushes to the repository's active branch "master" won't
trigger the benchmark; update the YAML push trigger (the branches array under
the push event) to include "master" as well (e.g., add "master" alongside "main"
or replace "main" with "master" depending on intended behavior) so the workflow
will run on pushes to the repository's active branch.

Comment on lines +105 to +110
permissions:
contents: write # Needed to create releases
id-token: write # Needed for OIDC authentication with PyPI
packages: write # Needed to publish devcontainer image
attestations: write # Needed for SLSA provenance attestations (public repos only)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reduce workflow-level write permissions and grant write only per job.

Lines 106-109 currently give write scopes to every job. Move these to only jobs that truly need them (release/publish/attestation steps) to enforce least privilege.

🧰 Tools
🪛 zizmor (1.25.2)

[error] 106-106: overly broad permissions (excessive-permissions): contents: write is overly broad at the workflow level

(excessive-permissions)


[error] 107-107: overly broad permissions (excessive-permissions): id-token: write is overly broad at the workflow level

(excessive-permissions)


[error] 108-108: overly broad permissions (excessive-permissions): packages: write is overly broad at the workflow level

(excessive-permissions)


[error] 109-109: overly broad permissions (excessive-permissions): attestations: write is overly broad at the workflow level

(excessive-permissions)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/rhiza_release.yml around lines 105 - 110, Top-level
workflow permissions currently grant write to contents, id-token, packages, and
attestations for all jobs; change the top-level permissions to least-privilege
(e.g., contents: read and minimal defaults) and move write scopes into only the
jobs that require them (the release/publish/attestation jobs). Specifically,
remove or downgrade the write entries under the top-level permissions block and
add per-job permissions entries for the jobs that perform
release/publish/attestation so that contents: write, id-token: write, packages:
write, and attestations: write appear only in the corresponding
release/publish/attestation job definitions (referencing the job names that run
release/publish/attestation).

Source: Linters/SAST tools

Comment on lines +119 to +588
uses: actions/checkout@v6.0.2
with:
fetch-depth: 0

- name: Set Tag Variable
id: set_tag
run: |
if [[ -n "${{ inputs.tag }}" ]]; then
TAG="${{ inputs.tag }}"
else
TAG="${GITHUB_REF#refs/tags/}"
fi
echo "tag=$TAG" >> "$GITHUB_OUTPUT"

- name: Validate Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.set_tag.outputs.tag }}
run: |
if gh release view "$TAG" >/dev/null 2>&1; then
DRAFT_STATUS=$(gh release view "$TAG" --json isDraft --jq '.isDraft')
if [ "$DRAFT_STATUS" != "true" ]; then
echo "::error::Release '$TAG' already exists and is not a draft. Please use a new tag."
exit 1
else
echo "::warning::Release '$TAG' exists as a draft. Will proceed to update it."
fi
fi

- name: Install uv
uses: astral-sh/setup-uv@v7.6.0

- name: Ensure release version is newer than latest published
env:
TAG: ${{ steps.set_tag.outputs.tag }}
run: |
# Backstop for issue #1126: refuse a tag that is not strictly newer than the
# highest version already released. A diverged branch with a stale pyproject.toml
# can otherwise publish an older version (e.g. v0.3.3 after v0.4.0). The bump
# tooling enforces the same rule; this guards manually-pushed tags too.
git tag -l 'v*' | uv run --with packaging --no-project python3 -c '
import sys
from packaging.version import Version, InvalidVersion

new = sys.argv[1].lstrip("v")
try:
new_v = Version(new)
except InvalidVersion:
print(f"::error::Tag {sys.argv[1]} is not a valid version")
sys.exit(1)

latest = None
for line in sys.stdin:
tag = line.strip().lstrip("v")
if not tag:
continue
try:
candidate = Version(tag)
except InvalidVersion:
continue
if candidate == new_v:
continue
if latest is None or candidate > latest:
latest = candidate

if latest is not None and new_v <= latest:
print(
f"::error::Refusing to release v{new_v}: it is not newer than the latest "
f"released version v{latest} (issue #1126). Sync with the default branch and bump again."
)
sys.exit(1)
print(f"v{new_v} is newer than the latest released version v{latest}" if latest
else f"v{new_v} is the first release")
' "$TAG"

build:
name: Build
runs-on: ubuntu-latest
needs: tag
steps:
- name: Checkout Code
uses: actions/checkout@v6.0.2
with:
fetch-depth: 0

- name: Install uv
uses: astral-sh/setup-uv@v7.6.0
with:
version: "0.11.16"

- name: Configure git auth for private packages
uses: jebel-quant/rhiza/.github/actions/configure-git-auth@v0.15.2
with:
token: ${{ secrets.GH_PAT }}

- name: Verify version matches tag
if: hashFiles('pyproject.toml') != ''
run: |
TAG_VERSION="${{ needs.tag.outputs.tag }}"
TAG_VERSION=${TAG_VERSION#v}
PROJECT_VERSION=$(uv version --short)

# Normalize tag version to PEP 440 format for comparison.
# Tags use semver format (e.g., 0.11.1-beta.1) while uv version --short
# returns PEP 440 normalized format (e.g., 0.11.1b1).
NORMALIZED_TAG=$(uv run --with packaging --no-project python3 -c "from packaging.version import Version; print(Version('$TAG_VERSION'))")

if [[ "$PROJECT_VERSION" != "$NORMALIZED_TAG" ]]; then
echo "::error::Version mismatch: pyproject.toml has '$PROJECT_VERSION' but tag is '$NORMALIZED_TAG' (from tag '$TAG_VERSION')"
exit 1
fi
echo "Version verified: $PROJECT_VERSION matches tag (normalized: $NORMALIZED_TAG)"

- name: Detect buildable Python package
id: buildable
run: |
if [[ -f pyproject.toml ]] && grep -q '^\[build-system\]' pyproject.toml; then
echo "buildable=true" >> "$GITHUB_OUTPUT"
else
echo "buildable=false" >> "$GITHUB_OUTPUT"
fi

- name: Build
if: steps.buildable.outputs.buildable == 'true'
run: |
printf "[INFO] Building package...\n"
uv build

- name: Install Python for SBOM generation
if: hashFiles('pyproject.toml') != ''
uses: actions/setup-python@v6.2.0
with:
python-version-file: .python-version

- name: Sync environment for SBOM generation
if: hashFiles('pyproject.toml') != ''
run: |
export UV_EXTRA_INDEX_URL="${{ secrets.UV_EXTRA_INDEX_URL }}"
uv sync --all-extras --all-groups --frozen

- name: Generate SBOM (CycloneDX)
if: hashFiles('pyproject.toml') != ''
run: |
printf "[INFO] Generating SBOM in CycloneDX format...\n"
# Note: uvx caches the tool environment, so the second call is fast
uvx --from 'cyclonedx-bom>=7.0.0' cyclonedx-py environment --pyproject pyproject.toml --of JSON -o sbom.cdx.json
uvx --from 'cyclonedx-bom>=7.0.0' cyclonedx-py environment --pyproject pyproject.toml --of XML -o sbom.cdx.xml
printf "[INFO] SBOM generation complete\n"
printf "Generated files:\n"
ls -lh sbom.cdx.*

- name: Attest SBOM
# Attest only the JSON format as it's the canonical machine-readable format.
# The XML format is provided for compatibility but doesn't need separate attestation.
if: hashFiles('pyproject.toml') != '' && github.event.repository.private == false
uses: actions/attest@v4.1.0
with:
subject-path: sbom.cdx.json
sbom-path: sbom.cdx.json

- name: Upload SBOM artifacts
if: hashFiles('pyproject.toml') != ''
uses: actions/upload-artifact@v7.0.1
with:
name: sbom
path: |
sbom.cdx.json
sbom.cdx.xml

- name: Generate SLSA provenance attestations
if: steps.buildable.outputs.buildable == 'true' && github.event.repository.private == false
uses: actions/attest-build-provenance@v4
with:
subject-path: dist/*

- name: Upload dist artifact
if: steps.buildable.outputs.buildable == 'true'
uses: actions/upload-artifact@v7.0.1
with:
name: dist
path: dist

# Don't try to upload artifacts to GitHub Release at all
draft-release:
name: Draft GitHub Release
runs-on: ubuntu-latest
needs: [tag, build]

steps:
- name: Checkout Code
uses: actions/checkout@v6.0.2
with:
fetch-depth: 0

- name: Install uv
uses: astral-sh/setup-uv@v7.6.0

- name: Generate release notes with git-cliff
run: uvx git-cliff --latest --output RELEASE_NOTES.md

- name: Download SBOM artifact
# Downloads sbom.cdx.json and sbom.cdx.xml into sbom/ directory
uses: actions/download-artifact@v8.0.1
with:
name: sbom
path: sbom
continue-on-error: true

- name: Create GitHub Release with artifacts
uses: ncipollo/release-action@v1.21.0
with:
tag: ${{ needs.tag.outputs.tag }}
name: ${{ needs.tag.outputs.tag }}
bodyFile: RELEASE_NOTES.md
draft: true
allowUpdates: true
artifacts: "sbom/*"

update-changelog:
name: Update CHANGELOG.md
runs-on: ubuntu-latest
needs: [tag, draft-release]
steps:
- name: Checkout Default Branch
uses: actions/checkout@v6.0.2
with:
ref: ${{ github.event.repository.default_branch }}
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}

- name: Install uv
uses: astral-sh/setup-uv@v7.6.0

- name: Generate CHANGELOG.md with git-cliff
run: uvx git-cliff --output CHANGELOG.md

- name: Commit and push CHANGELOG.md
env:
TAG: ${{ needs.tag.outputs.tag }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add CHANGELOG.md
git diff --staged --quiet || git commit -m "chore: update CHANGELOG.md for $TAG [skip ci]"
git push origin ${{ github.event.repository.default_branch }}

# Decide at step-level whether to publish
pypi:
name: Publish to PyPI
runs-on: ubuntu-latest
environment: release
needs: [tag, build, draft-release]
outputs:
should_publish: ${{ steps.check_dist.outputs.should_publish }}

steps:
- name: Checkout Code
uses: actions/checkout@v6.0.2
with:
fetch-depth: 0

- name: Download dist artifact
uses: actions/download-artifact@v8.0.1
with:
name: dist
path: dist
# Continue if dist folder is not found. Don't fail the job.
continue-on-error: true

- name: Check if dist contains artifacts and not marked as private
id: check_dist
run: |
if [[ ! -d dist ]]; then
echo "::warning::No folder dist/. Skipping PyPI publish."
echo "should_publish=false" >> "$GITHUB_OUTPUT"
else
if grep -R "Private :: Do Not Upload" pyproject.toml; then
echo "should_publish=false" >> "$GITHUB_OUTPUT"
else
echo "should_publish=true" >> "$GITHUB_OUTPUT"
fi
fi
cat "$GITHUB_OUTPUT"

# this should not take place, as "Private :: Do Not Upload" set in pyproject.toml
# repository-url and password only used for custom feeds, not for PyPI with OIDC
- name: Publish to PyPI
if: ${{ steps.check_dist.outputs.should_publish == 'true' }}
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist/
skip-existing: true
repository-url: ${{ vars.PYPI_REPOSITORY_URL }}
password: ${{ secrets.PYPI_TOKEN }}


conda:
name: Generate Conda Recipe
runs-on: ubuntu-latest
needs: [tag, pypi]
outputs:
should_generate: ${{ steps.check_conda.outputs.should_generate }}

steps:
- name: Checkout Code
uses: actions/checkout@v6.0.2
with:
fetch-depth: 0

- name: Check if conda recipe should be generated
id: check_conda
env:
PUBLISH_CONDA: ${{ vars.PUBLISH_CONDA }}
run: |
PUBLISH_CONDA="${PUBLISH_CONDA:-true}"
if [[ "$PUBLISH_CONDA" == "true" ]] && [[ "${{ needs.pypi.outputs.should_publish }}" == "true" ]] && [[ "${{ needs.pypi.result }}" == "success" ]]; then
echo "should_generate=true" >> "$GITHUB_OUTPUT"
else
echo "should_generate=false" >> "$GITHUB_OUTPUT"
echo "⏭️ Skipping conda recipe generation (PUBLISH_CONDA not true, or PyPI publish disabled or failed)"
fi

- name: Set up Python
if: steps.check_conda.outputs.should_generate == 'true'
uses: actions/setup-python@v6.2.0
with:
python-version-file: .python-version

- name: Install grayskull
if: steps.check_conda.outputs.should_generate == 'true'
run: python -m pip install --upgrade grayskull

- name: Generate conda recipe with grayskull
if: steps.check_conda.outputs.should_generate == 'true'
run: |
PACKAGE_NAME=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['name'])")
mkdir -p /tmp/conda-recipe
cd /tmp/conda-recipe

grayskull pypi "$PACKAGE_NAME" --strict-conda-forge

RECIPE_PATH=$(find . -type f -path "*/meta.yaml" | head -n 1)
if [[ -z "$RECIPE_PATH" ]]; then
echo "::error::grayskull did not produce a meta.yaml file"
exit 1
fi

mkdir -p "$GITHUB_WORKSPACE/conda-recipe"
cp "$RECIPE_PATH" "$GITHUB_WORKSPACE/conda-recipe/meta.yaml"

- name: Upload conda recipe artifact
if: steps.check_conda.outputs.should_generate == 'true'
uses: actions/upload-artifact@v7.0.1
with:
name: conda-recipe
path: conda-recipe/meta.yaml

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.2.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, conda, devcontainer]
if: needs.pypi.result == 'success' || needs.conda.result == 'success' || needs.devcontainer.result == 'success'
steps:
- name: Checkout Code
uses: actions/checkout@v6.0.2
with:
fetch-depth: 0

- name: Install uv
uses: astral-sh/setup-uv@v7.6.0
with:
version: "0.11.16"

- name: "Sync the virtual environment for ${{ github.repository }}"
shell: bash
run: |
export UV_EXTRA_INDEX_URL="${{ secrets.uv_extra_index_url }}"
# will just use .python-version?
uv sync --all-extras --all-groups --frozen

- name: Set up Python
uses: actions/setup-python@v6.2.0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# List non-SHA action references in this workflow.
rg -nP '^\s*uses:\s*[^@]+@([A-Za-z0-9._/\-]+)$' .github/workflows/rhiza_release.yml \
  | rg -v '@[0-9a-f]{40}$'

Repository: tschm/pycharting

Length of output: 1935


Pin GitHub Actions to full commit SHAs in .github/workflows/rhiza_release.yml
Release pipeline security policy: this workflow uses mutable action tags (retargetable). Pin (at least) these actions to immutable commit SHAs instead of tags: actions/checkout@v6.0.2, astral-sh/setup-uv@v7.6.0, actions/setup-python@v6.2.0, actions/attest@v4.1.0, actions/upload-artifact@v7.0.1, actions/download-artifact@v8.0.1, actions/attest-build-provenance@v4, ncipollo/release-action@v1.21.0, pypa/gh-action-pypi-publish@release/v1, devcontainers/ci@v0.3, docker/login-action@v4.2.0, and jebel-quant/rhiza/.github/actions/configure-git-auth@v0.15.2.

🧰 Tools
🪛 zizmor (1.25.2)

[warning] 199-202: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[warning] 308-311: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[warning] 342-347: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[warning] 375-378: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[warning] 423-426: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[warning] 485-488: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[warning] 570-573: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 126-126: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[error] 127-127: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[info] 217-217: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[error] 363-363: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[info] 434-434: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[info] 507-507: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[info] 531-531: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[error] 532-532: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[info] 540-540: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[info] 551-551: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[info] 551-551: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[error] 119-119: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 149-149: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 200-200: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 205-205: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 210-210: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 249-249: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 274-274: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 281-281: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 290-290: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 296-296: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 309-309: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 314-314: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 321-321: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 328-328: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 343-343: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 350-350: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 376-376: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 381-381: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 407-407: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 424-424: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 443-443: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 471-471: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 486-486: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 515-515: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 557-557: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 571-571: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 576-576: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 588-588: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 149-149: runtime artifacts potentially vulnerable to a cache poisoning attack (cache-poisoning): enables caching by default

(cache-poisoning)


[error] 205-205: runtime artifacts potentially vulnerable to a cache poisoning attack (cache-poisoning): enables caching by default

(cache-poisoning)


[error] 314-314: runtime artifacts potentially vulnerable to a cache poisoning attack (cache-poisoning): enables caching by default

(cache-poisoning)


[error] 350-350: runtime artifacts potentially vulnerable to a cache poisoning attack (cache-poisoning): enables caching by default

(cache-poisoning)


[error] 576-576: runtime artifacts potentially vulnerable to a cache poisoning attack (cache-poisoning): enables caching by default

(cache-poisoning)


[info] 328-328: action functionality is already included by the runner (superfluous-actions): use gh release in a script step

(superfluous-actions)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/rhiza_release.yml around lines 119 - 588, The workflow
uses mutable action tags (e.g., actions/checkout@v6.0.2,
astral-sh/setup-uv@v7.6.0, actions/setup-python@v6.2.0, actions/attest@v4.1.0,
actions/upload-artifact@v7.0.1, actions/download-artifact@v8.0.1,
actions/attest-build-provenance@v4, ncipollo/release-action@v1.21.0,
pypa/gh-action-pypi-publish@release/v1, devcontainers/ci@v0.3,
docker/login-action@v4.2.0,
jebel-quant/rhiza/.github/actions/configure-git-auth@v0.15.2); replace each
uses: reference with the corresponding immutable commit SHA (the full 40-char
commit) for that action version to prevent retargeting. For each occurrence
(search for the unique action strings above in the workflow), update the uses
value to the same repository@<commit-sha>, verify the SHA matches the tag
version in the action’s upstream repo, and run a workflow test to ensure no
breaking changes from the pinning.

Source: Linters/SAST tools


@git fetch --prune

@git branch -vv | awk '/: gone]/{print $$1}' | xargs -r git branch -D

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid force-deleting stale branches in clean.

Using git branch -D can remove unmerged local commits and cause irreversible local data loss for developers running a routine cleanup.

Suggested patch
-	`@git` branch -vv | awk '/: gone]/{print $$1}' | xargs -r git branch -D
+	`@git` branch -vv | awk '/: gone]/{print $$1}' | sed 's/^\*//' | awk 'NF' | xargs -r git branch -d
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@git branch -vv | awk '/: gone]/{print $$1}' | xargs -r git branch -D
`@git` branch -vv | awk '/: gone]/{print $$1}' | sed 's/^\*//' | awk 'NF' | xargs -r git branch -d
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.rhiza/make.d/bootstrap.mk at line 109, Replace the unsafe force-delete
command "git branch -vv | awk '/: gone]/{print $$1}' | xargs -r git branch -D"
with a safer flow: identify stale branches (the same grep/awk can remain), but
pass them to "git branch -d" instead of "-D" and/or first filter to only merged
branches using "git branch --merged" to avoid deleting unmerged work; ensure you
skip the current branch (e.g., check "$(git rev-parse --abbrev-ref HEAD)") and
only delete when the branch is merged or confirmed, so update the invocation
that currently uses xargs git branch -D to use a safe deletion command and
optional confirmation.

Comment thread .rhiza/make.d/quality.mk
Comment on lines +18 to +24
@if [ -d ${MARIMO_FOLDER} ]; then \
if [ -d ${SOURCE_FOLDER} ]; then \
$(UVX_BIN) -p ${PYTHON_VERSION} deptry ${MARIMO_FOLDER} ${SOURCE_FOLDER} --ignore DEP004; \
else \
$(UVX_BIN) -p ${PYTHON_VERSION} deptry ${MARIMO_FOLDER} --ignore DEP004; \
fi \
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix missing command separator in nested if block.

Line 23 ends the inner block as fi \ and then immediately continues to the outer fi, yielding a shell parse error (fi fi) when this recipe is evaluated.

Proposed fix
-		fi \
+		fi; \
 	fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@if [ -d ${MARIMO_FOLDER} ]; then \
if [ -d ${SOURCE_FOLDER} ]; then \
$(UVX_BIN) -p ${PYTHON_VERSION} deptry ${MARIMO_FOLDER} ${SOURCE_FOLDER} --ignore DEP004; \
else \
$(UVX_BIN) -p ${PYTHON_VERSION} deptry ${MARIMO_FOLDER} --ignore DEP004; \
fi \
fi
`@if` [ -d ${MARIMO_FOLDER} ]; then \
if [ -d ${SOURCE_FOLDER} ]; then \
$(UVX_BIN) -p ${PYTHON_VERSION} deptry ${MARIMO_FOLDER} ${SOURCE_FOLDER} --ignore DEP004; \
else \
$(UVX_BIN) -p ${PYTHON_VERSION} deptry ${MARIMO_FOLDER} --ignore DEP004; \
fi; \
fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.rhiza/make.d/quality.mk around lines 18 - 24, The nested shell if/fi
sequence in the Make recipe is missing a command separator between the inner
"fi" and the outer "fi", causing a shell parse error; change the inner "fi \\"
to include a semicolon before the backslash (i.e. "fi; \\") so the shell
receives a proper separator; locate the block that uses MARIMO_FOLDER,
SOURCE_FOLDER and calls $(UVX_BIN) -p ${PYTHON_VERSION} deptry and update the
inner "fi" line to "fi; \\".

Comment on lines +145 to +151
@pytest.fixture(scope="class")
def classifiers(self, project: dict) -> list[str]:
"""Return the classifiers list."""
cl = project.get("classifiers", [])
if not cl:
pytest.skip("No classifiers declared in [project]")
return cl

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Required sections are currently skippable, which can hide config regressions.

These fixtures skip when data is missing, so required constraints can silently pass instead of failing.

Suggested fix
 `@pytest.fixture`(scope="class")
 def classifiers(self, project: dict) -> list[str]:
     """Return the classifiers list."""
     cl = project.get("classifiers", [])
-    if not cl:
-        pytest.skip("No classifiers declared in [project]")
+    if not isinstance(cl, list) or not cl:
+        pytest.fail("[project].classifiers must be a non-empty list")
     return cl
@@
 `@pytest.fixture`(scope="class")
 def dependency_groups(self, pyproject: dict) -> dict:
     """Return the [dependency-groups] table."""
     dg = pyproject.get("dependency-groups")
-    if not isinstance(dg, dict):
-        pytest.skip("[dependency-groups] not present")
+    if not isinstance(dg, dict):
+        pytest.fail("pyproject.toml is missing a [dependency-groups] table")
     return dg

Also applies to: 169-175

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.rhiza/tests/structure/test_pyproject.py around lines 145 - 151, The
classifiers fixture currently calls pytest.skip when the "classifiers" key is
missing which hides missing required configuration; change the fixture (def
classifiers) to assert or raise a test failure (e.g., use pytest.fail or a
direct assert) when project.get("classifiers") is empty so the test fails loudly
instead of skipping; apply the same change to the other fixtures in this test
file that currently call pytest.skip (the second similar fixture) so missing
required sections cause test failures rather than silent skips.

Comment on lines +78 to +80
# Create a minimal .rhiza/template.yml
(tmp_path / ".rhiza" / "template.yml").write_text("repository: Jebel-Quant/rhiza\nref: v0.7.1\n")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Sync fixture hardcodes an outdated template ref.

This fixture writes ref: v0.7.1, while this PR updates the template reference to v0.18.8. That drift can make sync tests validate against the wrong template baseline.

Suggested fix
-    # Create a minimal .rhiza/template.yml
-    (tmp_path / ".rhiza" / "template.yml").write_text("repository: Jebel-Quant/rhiza\nref: v0.7.1\n")
+    # Reuse repository template reference to avoid version drift in sync tests
+    template_src = root / ".rhiza" / "template.yml"
+    if template_src.exists():
+        shutil.copy(template_src, tmp_path / ".rhiza" / "template.yml")
+    else:
+        (tmp_path / ".rhiza" / "template.yml").write_text(
+            "repository: Jebel-Quant/rhiza\nref: v0.18.8\n"
+        )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.rhiza/tests/sync/conftest.py around lines 78 - 80, The test fixture is
writing an outdated template ref ("ref: v0.7.1") into the minimal
.rhiza/template.yml; update the string passed to (tmp_path / ".rhiza" /
"template.yml").write_text(...) to use the current template ref "v0.18.8" so
tests validate against the correct baseline and keep the rest of the file
content unchanged.

Comment thread docs/index.md
@@ -0,0 +1,2 @@
--8<-- "README.md"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

README snippet path is likely unresolved with current MkDocs snippet base path.

Line 1 includes README.md, but docs/mkdocs-base.yml currently constrains snippet lookup to base_path: ["."] with docs_dir: docs. That usually resolves inside docs/, so root README.md won’t be found and docs build may fail.

Proposed fix
- --8<-- "README.md"
+ --8<-- "../README.md"

Or broaden snippet lookup in docs/mkdocs-base.yml:

   - pymdownx.snippets:
-      base_path: ["."]
+      base_path: [".", ".."]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
--8<-- "README.md"
--8<-- "../README.md"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/index.md` at line 1, The README.md snippet reference in docs/index.md
won't be found because mkdocs-base.yml restricts snippet lookup to base_path:
["."] with docs_dir: docs; update configuration so the snippet lookup includes
the repository root or the README location (for example expand base_path to
include ".." or add the root path), or move/copy the README.md into the docs/
folder and update the reference; edit docs/mkdocs-base.yml (look for base_path
and docs_dir) to add the correct path where README.md resides so the snippet in
docs/index.md resolves during the build.

@tschm tschm closed this Jun 15, 2026
@tschm
tschm deleted the rhiza2 branch June 15, 2026 07:10
@tschm
tschm restored the rhiza2 branch July 13, 2026 20:33
@tschm
tschm deleted the rhiza2 branch July 17, 2026 13:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants