Skip to content

Add release-to-pypi-uv composite action#90

Merged
leynos merged 41 commits intomainfrom
python-lib-release-action
Sep 24, 2025
Merged

Add release-to-pypi-uv composite action#90
leynos merged 41 commits intomainfrom
python-lib-release-action

Conversation

@leynos
Copy link
Copy Markdown
Owner

@leynos leynos commented Sep 18, 2025

Summary

  • add Release to PyPI (uv) composite action with scripted helpers
  • document the action and capture scripting guidance in docs
  • register the action in the root README

Testing

  • UV_CACHE_DIR=/home/leynos/Projects/shared-actions/.cache/uv make test

Summary by Sourcery

Introduce a new composite action ‘release-to-pypi-uv’ to streamline release workflows with uv, bolster error handling and retries in existing actions, integrate cmd-mox for more robust command‐doubles testing, enforce lint/formatting in CI, and enrich documentation and tests across the repository

New Features:

  • Add release-to-pypi-uv composite GitHub Action with step scripts for tag resolution, manual confirmation, GitHub release validation, TOML version checking, publishing, and summary writing
  • Extend setup-windows-gnu action with configurable download retry logic and optional aarch64 compiler requirement

Enhancements:

  • Refactor rust-build-release action to centralize runtime probing, support custom PROBE_TIMEOUT, and improve platform target detection
  • Revamp generate-coverage run_rust logic to handle missing streams, errors, and ensure stream cleanup
  • Integrate cmd-mox into rust-build-release and utils tests with new echo recorder fixture
  • Update Makefile and CI workflows to include ruff lint/format checks, bunx action-validator, and Bun setup

Build:

  • Add cmd-mox to dev dependencies in pyproject.toml

CI:

  • Update workflows to install and configure Bun, run formatting and lint checks before testing

Documentation:

  • Add release-to-pypi-uv action documentation and update root README
  • Add cmd-mox usage guide and update scripting-standards for Cyclopts adoption

Tests:

  • Add extensive unit and integration tests for the new release-to-pypi-uv scripts and action metadata
  • Enhance existing rust-build-release tests with cmd-mox for cross‐version, container runtime and timeout scenarios

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Sep 18, 2025

Reviewer's Guide

This PR adds a new composite GitHub Action for releasing Python packages via uv, enriches scripting standards and documentation, and refactors several existing action scripts and tests for consistency and robustness. The release-to-pypi-uv action defines inputs, steps (determine tag, optional confirmation, GitHub release check, TOML version validation, build, publish, summary), and relies on uv­-run scripts implemented with Typer. Alongside, the PR introduces helper fixtures for command mocking (cmd-mox, echo_recorder), updates the rust-build-release runtime detection and coverage runner (run_rust.py) with safer stream handling, and tightens CI lint/format targets and workflow steps.

Sequence diagram for the release-to-pypi-uv composite action workflow

sequenceDiagram
  participant Workflow
  participant Action
  participant Scripts
  participant PyPI
  participant GitHubAPI
  participant Summary
  Workflow->>Action: Start release-to-pypi-uv
  Action->>Scripts: determine_release.py (resolve tag/version)
  Action->>Scripts: confirm_release.py (if confirmation required)
  Action->>Scripts: check_github_release.py (validate release)
  Scripts->>GitHubAPI: GET /repos/:repo/releases/tags/:tag
  GitHubAPI-->>Scripts: Release info
  Action->>Scripts: validate_toml_versions.py (check TOML versions)
  Action->>Scripts: uv build (build dists)
  Action->>Scripts: publish_release.py (publish)
  Scripts->>PyPI: uv publish
  Action->>Scripts: write_summary.py (write summary)
  Scripts->>Summary: Update step summary
Loading

File-Level Changes

Change Details Files
Introduce release-to-pypi-uv composite action and supporting scripts
  • Add action.yml defining inputs, outputs, and composite step sequence
  • Implement scripts: determine_release, confirm_release, check_github_release, validate_toml_versions, publish_release, write_summary
  • Register new action in root README and add dedicated README, CHANGELOG.md
  • Write extensive tests for each helper script under tests/ release-to-pypi-uv
.github/actions/release-to-pypi-uv/action.yml
.github/actions/release-to-pypi-uv/scripts/*.py
README.md
CHANGELOG.md
.github/actions/release-to-pypi-uv/tests/**/*
Refactor runtime detection in rust-build-release
  • Extract _run_probe, _get_probe_timeout, and _platform_default_host_target in runtime.py
  • Replace direct subprocess calls in runtime_available and detect_host_target with these helpers
  • Parametrize and simplify probe timeout overrides
  • Update associated tests to use echo_recorder and _patch_run_validated_timeout
.github/actions/rust-build-release/src/runtime.py
.github/actions/rust-build-release/tests/test_runtime.py
Harden coverage runner stream handling in generate-coverage
  • Introduce _safe_close_text_stream to suppress cleanup errors
  • Wrap stdout/stderr guards in try/finally to always close streams
  • Rework Windows pump logic for exceptions and thread cleanup
.github/actions/generate-coverage/scripts/run_rust.py
.github/actions/generate-coverage/tests/test_scripts.py
Enhance test fixtures and CI tooling
  • Add echo_recorder fixture and expand shared_actions_conftest helpers for cmd-mox
  • Guard cmd-mox tests on Windows and uv presence
  • Update Makefile and CI workflow: add ruff auto-fix rules, check-fmt, lint steps, setup-bun
  • Adjust pyproject.toml to pin cmd-mox dependency and update lint rules
.github/actions/rust-build-release/tests/conftest.py
conftest.py
Makefile
.github/workflows/ci.yml
pyproject.toml

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Sep 18, 2025

Note

Reviews paused

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Summary by CodeRabbit

  • New Features

    • Added a composite GitHub Action to publish Python packages to PyPI via uv, with tag resolution, optional manual confirmation, Python version selection, TOML version validation, and a step summary.
    • Added user-facing documentation and changelog entries.
    • Introduced comprehensive CmdMox guides and design docs.
  • Bug Fixes

    • Improved error messaging when cargo output streams are unavailable.
  • Tests

    • Added extensive tests covering action inputs, validation, release checks, publishing, and summaries.
  • Chores

    • Enhanced CI with formatting/lint checks and action validation; updated Makefile formatting flow; ignored .crush directory.
  • Style

    • Minor formatting clean-ups.

Walkthrough

Add a composite GitHub Action "Release to PyPI (uv)" with supporting Typer scripts for tag resolution, manual confirmation, GitHub Release verification (with retries), TOML version validation, publishing via uv, and step-summary writing; include docs, changelog, tests, CmdMox-based test refactors, CI steps, and dev-tooling updates.

Changes

Cohort / File(s) Summary of changes
Release to PyPI (uv) — Action definition & docs
.github/actions/release-to-pypi-uv/action.yml, .github/actions/release-to-pypi-uv/README.md, .github/actions/release-to-pypi-uv/CHANGELOG.md
Add composite action with inputs/outputs (including python-version) and sequenced steps; add README and CHANGELOG entries v1.0.0–v1.0.2.
Release to PyPI (uv) — Scripts
.github/actions/release-to-pypi-uv/scripts/determine_release.py, .../confirm_release.py, .../check_github_release.py, .../validate_toml_versions.py, .../publish_release.py, .../write_summary.py
Add Typer CLIs: resolve tag/version, confirm release, verify GitHub Release with retries, validate pyproject.toml versions (dynamic handling), publish via uv (optional index), and append GitHub step summary.
Release to PyPI (uv) — Tests & helpers
.github/actions/release-to-pypi-uv/tests/__init__.py, .../tests/_helpers.py, .../tests/test_determine_release.py, .../tests/test_validate_toml_versions.py, .../tests/test_check_github_release.py, .../tests/test_confirm_release.py, .../tests/test_publish_release.py, .../tests/test_write_summary.py, .../tests/test_action_python_version.py
Add test package, loader helper and comprehensive unit/E2E tests covering tag resolution, TOML validation, GitHub Release checks, manual confirmation, publishing, summary writing and python-version wiring.
Rust build action — Tests refactor to CmdMox
.github/actions/rust-build-release/tests/test_cross_install.py, .../tests/test_target_install.py, .../tests/test_utils.py, .../tests/test_smoke.py, .../tests/conftest.py
Replace subprocess/mocking with CmdMox stubs/spies; add CMD_MOX_UNSUPPORTED markers, update fixtures/signatures, add Windows-known-failure marker and adjust test scaffolding.
CmdMox test scaffolding & shared conftest
conftest.py, .github/actions/rust-build-release/tests/conftest.py
Add shared conftest wiring cmd-mox plugin, helpers to register stubbed commands, CMD_MOX_UNSUPPORTED, HAS_UV, REQUIRES_UV fixtures; removed patch_subprocess_run helper in rust-build-release tests.
Docs: CmdMox guides & design
docs/cmd-mox-users-guide.md, docs/python-native-command-mocking-design.md
Add user guide and architecture/design document for CmdMox command-doubling and testing patterns.
Tooling & configuration
pyproject.toml, Makefile, .gitignore, .github/workflows/ci.yml
Add dev dependency cmd-mox (git URL); remove [tool.pyright] block; extend fmt to run ruff check --fix; add .crush/ to .gitignore; add CI steps to install bun/action-validator and run make check-fmt and make lint on ubuntu-latest.
Small code tweaks
.github/actions/rust-build-release/src/main.py, .github/actions/generate-coverage/scripts/run_rust.py, .github/actions/generate-coverage/tests/test_scripts.py
Minor whitespace cleanup and clearer error message construction for missing cargo output streams; insignificant semantics changes.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Runner as GitHub Actions Runner
  participant Action as Release-to-PyPI (composite)
  participant Scripts as uv-run scripts
  participant GH as GitHub API
  participant PyPI as PyPI (uv Trusted Publishing)

  Runner->>Action: Trigger action (inputs incl. python-version)
  Action->>Scripts: determine_release (tag/env)
  Scripts-->>Action: resolved tag, version

  alt require-confirmation == true
    Action->>Scripts: confirm_release(expected="release <tag>", confirm=input)
    Scripts-->>Action: OK or exit(error)
  end

  Action->>Scripts: check_github_release(tag, token, repo)
  Scripts->>GH: GET /repos/{repo}/releases/tags/{tag} (with retries)
  GH-->>Scripts: JSON (ensure not draft/prerelease)
  Scripts-->>Action: OK

  Action->>Scripts: validate_toml_versions(version, pattern, fail-on-dynamic)
  Scripts-->>Action: OK or exit(error)

  Action->>Action: setup uv & cache
  Action->>Scripts: uv python install "${{ inputs.python-version }}"
  Action->>Scripts: build dists (sdist/wheel)
  Action->>Scripts: publish_release(index?)
  Scripts->>PyPI: Publish (Trusted Publishing OIDC)
  PyPI-->>Scripts: success

  Action->>Scripts: write_summary(tag, index, environment)
  Scripts-->>Action: appended summary
  Action-->>Runner: outputs tag, version
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • leynos/shared-actions#77 — Modifies rust-build-release action code and tests; likely related to the CmdMox test refactor and rust-build-release adjustments.

Suggested labels

codex

Poem

Tag resolved, scripts align and run,
Checks retry until the publish is done.
TOML sings true, confirmations pass,
uv builds, PyPI welcomes the glass.
Tests stand guard — release takes flight. 🚀

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 73.91% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed Confirm that the title "Add release-to-pypi-uv composite action" concisely and accurately summarises the primary change, namely the addition of a new composite GitHub Action and its supporting scripts, documentation and tests. Keep the title short and specific; it avoids noisy file lists, emojis, or vague wording and will be clear to teammates scanning history. Approve the title as-is.
Description Check ✅ Passed Confirm that the pull request description is directly related to the changeset by summarising the new composite action, helper scripts, documentation, changelog and tests while including a test invocation and brief CI context. Treat this description as adequate for the lenient description check because it is on-topic and informs reviewers of the intent and test steps even if it omits exhaustive implementation detail. Optionally add a short note about required workflow permissions or known CI caveats to help reviewers, but do not block the merge on that.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes and found some issues that need to be addressed.

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `.github/actions/release-to-pypi-uv/scripts/check_github_release.py:25-26` </location>
<code_context>
+
+def _fetch_release(repo: str, tag: str, token: str) -> dict[str, object]:
+    api = f"https://api.github.com/repos/{repo}/releases/tags/{tag}"
+    request = urllib.request.Request(
+        api,
+        headers={
+            "Authorization": f"Bearer {token}",
+            "Accept": "application/vnd.github+json",
+            "X-GitHub-Api-Version": "2022-11-28",
+            "User-Agent": "release-to-pypi-action",
+        },
+    )
+
</code_context>

<issue_to_address>
**suggestion:** No retry logic for transient network errors.

Adding retries with exponential backoff will help handle temporary network issues and make the API requests more reliable.

```suggestion
import time

def _fetch_release(repo: str, tag: str, token: str) -> dict[str, object]:
    api = f"https://api.github.com/repos/{repo}/releases/tags/{tag}"
    request = urllib.request.Request(
        api,
        headers={
            "Authorization": f"Bearer {token}",
            "Accept": "application/vnd.github+json",
            "X-GitHub-Api-Version": "2022-11-28",
            "User-Agent": "release-to-pypi-action",
        },
    )
    max_attempts = 5
    backoff_factor = 1.5
    for attempt in range(1, max_attempts + 1):
        try:
            with urllib.request.urlopen(request) as response:
                return json.load(response)
        except (urllib.error.URLError, urllib.error.HTTPError) as e:
            if attempt == max_attempts:
                raise
            sleep_time = backoff_factor ** attempt
            time.sleep(sleep_time)
```
</issue_to_address>

### Comment 2
<location> `.github/actions/release-to-pypi-uv/scripts/determine_release.py:43-48` </location>
<code_context>
+        )
+        raise typer.Exit(1)
+
+    if not re.fullmatch(r"v[0-9].*", resolved_tag):
+        typer.echo(
+            f"::error::Tag must start with 'v' (e.g. v1.2.3), got '{resolved_tag}'.",
</code_context>

<issue_to_address>
**suggestion:** Tag format regex may allow invalid semantic versions.

The current regex matches tags like 'v1abc', which aren't valid semantic versions. Use a stricter pattern such as 'v\d+\.\d+\.\d+' to ensure proper semantic versioning.

```suggestion
    if not re.fullmatch(r"v\d+\.\d+\.\d+", resolved_tag):
        typer.echo(
            f"::error::Tag must be a valid semantic version (e.g. v1.2.3), got '{resolved_tag}'.",
            err=True,
        )
        raise typer.Exit(1)
```
</issue_to_address>

### Comment 3
<location> `.github/actions/release-to-pypi-uv/tests/test_determine_release.py:46-57` </location>
<code_context>
+    return out
+
+
+def test_resolves_tag_from_ref(tmp_path: Path) -> None:
+    env = base_env(tmp_path)
+    env["GITHUB_REF_TYPE"] = "tag"
+    env["GITHUB_REF_NAME"] = "v1.2.3"
+
+    script = Path(__file__).resolve().parents[1] / "scripts" / "determine_release.py"
+    result = run_script(script, env=env)
+
+    assert result.returncode == 0, result.stderr
+    outputs = read_outputs(tmp_path)
+    assert outputs["tag"] == "v1.2.3"
+    assert outputs["version"] == "1.2.3"
+
+
</code_context>

<issue_to_address>
**suggestion (testing):** Consider adding tests for edge cases such as missing environment variables and invalid input formats.

Tests should also cover scenarios with missing or empty environment variables and tags with unexpected formats to verify error handling.

```suggestion
def test_resolves_tag_from_ref(tmp_path: Path) -> None:
    env = base_env(tmp_path)
    env["GITHUB_REF_TYPE"] = "tag"
    env["GITHUB_REF_NAME"] = "v1.2.3"

    script = Path(__file__).resolve().parents[1] / "scripts" / "determine_release.py"
    result = run_script(script, env=env)

    assert result.returncode == 0, result.stderr
    outputs = read_outputs(tmp_path)
    assert outputs["tag"] == "v1.2.3"
    assert outputs["version"] == "1.2.3"


def test_missing_github_ref_type(tmp_path: Path) -> None:
    env = base_env(tmp_path)
    # env["GITHUB_REF_TYPE"] is intentionally missing
    env["GITHUB_REF_NAME"] = "v1.2.3"

    script = Path(__file__).resolve().parents[1] / "scripts" / "determine_release.py"
    result = run_script(script, env=env)

    assert result.returncode != 0
    assert "GITHUB_REF_TYPE" in result.stderr or "missing" in result.stderr.lower()


def test_missing_github_ref_name(tmp_path: Path) -> None:
    env = base_env(tmp_path)
    env["GITHUB_REF_TYPE"] = "tag"
    # env["GITHUB_REF_NAME"] is intentionally missing

    script = Path(__file__).resolve().parents[1] / "scripts" / "determine_release.py"
    result = run_script(script, env=env)

    assert result.returncode != 0
    assert "GITHUB_REF_NAME" in result.stderr or "missing" in result.stderr.lower()


def test_empty_github_ref_name(tmp_path: Path) -> None:
    env = base_env(tmp_path)
    env["GITHUB_REF_TYPE"] = "tag"
    env["GITHUB_REF_NAME"] = ""

    script = Path(__file__).resolve().parents[1] / "scripts" / "determine_release.py"
    result = run_script(script, env=env)

    assert result.returncode != 0
    assert "GITHUB_REF_NAME" in result.stderr or "empty" in result.stderr.lower()


def test_invalid_tag_format(tmp_path: Path) -> None:
    env = base_env(tmp_path)
    env["GITHUB_REF_TYPE"] = "tag"
    env["GITHUB_REF_NAME"] = "release-1.2.3"  # Unexpected format

    script = Path(__file__).resolve().parents[1] / "scripts" / "determine_release.py"
    result = run_script(script, env=env)

    assert result.returncode != 0
    assert "tag format" in result.stderr.lower() or "invalid" in result.stderr.lower()


def test_malformed_version_tag(tmp_path: Path) -> None:
    env = base_env(tmp_path)
    env["GITHUB_REF_TYPE"] = "tag"
    env["GITHUB_REF_NAME"] = "v1.2"  # Malformed version string

    script = Path(__file__).resolve().parents[1] / "scripts" / "determine_release.py"
    result = run_script(script, env=env)

    assert result.returncode != 0
    assert "version" in result.stderr.lower() or "invalid" in result.stderr.lower()
```
</issue_to_address>

### Comment 4
<location> `.github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py:34-48` </location>
<code_context>
+    )
+
+
+def test_passes_when_versions_match(tmp_path: Path) -> None:
+    project = tmp_path / "pkg"
+    project.mkdir()
+    (project / "pyproject.toml").write_text(
+        """
+[project]
+name = "demo"
+version = "1.0.0"
+""".strip()
+    )
+
+    result = _run(tmp_path, version="1.0.0")
+
+    assert result.returncode == 0, result.stderr
+    assert "all versions match 1.0.0" in result.stdout
+
+
</code_context>

<issue_to_address>
**suggestion (testing):** Consider adding tests for TOML files with missing [project] section and for multiple TOML files.

Adding these tests will help verify the script's behavior when TOML files are missing the [project] section and when handling multiple files with mixed validity.

```suggestion
def test_passes_when_versions_match(tmp_path: Path) -> None:
    project = tmp_path / "pkg"
    project.mkdir()
    (project / "pyproject.toml").write_text(
        """
[project]
name = "demo"
version = "1.0.0"
""".strip()
    )

    result = _run(tmp_path, version="1.0.0")

    assert result.returncode == 0, result.stderr
    assert "all versions match 1.0.0" in result.stdout


def test_fails_when_project_section_missing(tmp_path: Path) -> None:
    project = tmp_path / "pkg"
    project.mkdir()
    (project / "pyproject.toml").write_text(
        """
[tool.poetry]
name = "demo"
version = "1.0.0"
""".strip()
    )

    result = _run(tmp_path, version="1.0.0")

    assert result.returncode != 0
    assert "missing [project] section" in result.stderr.lower() or "could not find [project]" in result.stderr.lower()


def test_multiple_toml_files_mixed_validity(tmp_path: Path) -> None:
    # Valid TOML
    valid_project = tmp_path / "pkg_valid"
    valid_project.mkdir()
    (valid_project / "pyproject.toml").write_text(
        """
[project]
name = "demo"
version = "1.0.0"
""".strip()
    )

    # Invalid TOML (missing [project])
    invalid_project = tmp_path / "pkg_invalid"
    invalid_project.mkdir()
    (invalid_project / "pyproject.toml").write_text(
        """
[tool.poetry]
name = "demo"
version = "1.0.0"
""".strip()
    )

    # Another valid TOML with matching version
    another_valid = tmp_path / "pkg_another_valid"
    another_valid.mkdir()
    (another_valid / "pyproject.toml").write_text(
        """
[project]
name = "demo2"
version = "1.0.0"
""".strip()
    )

    result = _run(tmp_path, version="1.0.0")

    # Should fail due to one invalid TOML
    assert result.returncode != 0
    assert "missing [project] section" in result.stderr.lower() or "could not find [project]" in result.stderr.lower()
```
</issue_to_address>

### Comment 5
<location> `.github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py:68-77` </location>
<code_context>
+    assert "version '1.0.1' != tag version '1.0.0'" in result.stderr
+
+
+def test_dynamic_version_failure(tmp_path: Path) -> None:
+    project = tmp_path / "pkg"
+    project.mkdir()
+    (project / "pyproject.toml").write_text(
+        """
+[project]
+name = "demo"
+dynamic = ["version"]
+""".strip()
+    )
+
+    result = _run(tmp_path, version="1.0.0", fail_dynamic="true")
+
+    assert result.returncode == 1
+    assert "dynamic 'version'" in result.stderr
</code_context>

<issue_to_address>
**suggestion (testing):** Consider adding a test for dynamic version with fail_dynamic set to false.

Adding this test will confirm that the script behaves as expected when fail_dynamic is false, ensuring it emits a notice and skips the version check.
</issue_to_address>

### Comment 6
<location> `.github/actions/release-to-pypi-uv/scripts/check_github_release.py:75` </location>
<code_context>
+        raise RuntimeError(f"{path}: failed to parse: {exc}") from exc
+
+
+def main(
+    version: str = VERSION_OPTION,
+    pattern: str = PATTERN_OPTION,
</code_context>

<issue_to_address>
**issue (review_instructions):** Add behavioural and unit tests for check_github_release.py to demonstrate correct operation and error handling.

This script introduces logic for verifying GitHub Release status, including error handling for draft and prerelease states. You must provide both behavioural and unit tests to cover success, draft, prerelease, and API failure scenarios.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*`

**Instructions:**
For any new feature or change to an existing feature, both behavioural *and* unit tests are required.

</details>
</issue_to_address>

### Comment 7
<location> `.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py:59` </location>
<code_context>
+        raise RuntimeError(f"{path}: failed to parse: {exc}") from exc
+
+
+def main(
+    version: str = VERSION_OPTION,
+    pattern: str = PATTERN_OPTION,
</code_context>

<issue_to_address>
**issue (review_instructions):** Add behavioural and unit tests for validate_toml_versions.py to verify version checks and error handling.

This script validates pyproject.toml versions and handles dynamic version errors. You must provide both behavioural and unit tests to cover matching, mismatching, missing, and dynamic version cases.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*`

**Instructions:**
For any new feature or change to an existing feature, both behavioural *and* unit tests are required.

</details>
</issue_to_address>

### Comment 8
<location> `.github/actions/release-to-pypi-uv/scripts/determine_release.py:26` </location>
<code_context>
+        fh.write(f"version={version}\n")
+
+
+def main(tag: str | None = TAG_OPTION, github_output: Path = GITHUB_OUTPUT_OPTION) -> None:
+    ref_type = os.getenv("GITHUB_REF_TYPE", "")
+    ref_name = os.getenv("GITHUB_REF_NAME", "")
</code_context>

<issue_to_address>
**issue (review_instructions):** Add behavioural and unit tests for determine_release.py to cover tag resolution and error conditions.

This script resolves release tags and versions, including error handling for missing or malformed tags. You must provide both behavioural and unit tests for all code paths, including valid and invalid inputs.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*`

**Instructions:**
For any new feature or change to an existing feature, both behavioural *and* unit tests are required.

</details>
</issue_to_address>

### Comment 9
<location> `.github/actions/release-to-pypi-uv/scripts/publish_release.py:24` </location>
<code_context>
+INDEX_OPTION = typer.Option("", envvar="INPUT_UV_INDEX")
+
+
+def main(index: str = INDEX_OPTION) -> None:
+    if index:
+        typer.echo(f"Publishing with uv to index '{index}'")
</code_context>

<issue_to_address>
**issue (review_instructions):** Add behavioural and unit tests for publish_release.py to verify publishing logic and error handling.

This script publishes distributions using uv, optionally to a custom index. You must provide both behavioural and unit tests to cover publishing to default and custom indexes, and error scenarios from run_cmd.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*`

**Instructions:**
For any new feature or change to an existing feature, both behavioural *and* unit tests are required.

</details>
</issue_to_address>

### Comment 10
<location> `.github/actions/release-to-pypi-uv/scripts/write_summary.py:20` </location>
<code_context>
+        raise RuntimeError(f"{path}: failed to parse: {exc}") from exc
+
+
+def main(
+    version: str = VERSION_OPTION,
+    pattern: str = PATTERN_OPTION,
</code_context>

<issue_to_address>
**issue (review_instructions):** Add behavioural and unit tests for write_summary.py to verify summary output and error handling.

This script writes a release summary to a file. You must provide both behavioural and unit tests to cover correct output and error conditions (e.g., file write failures).

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*`

**Instructions:**
For any new feature or change to an existing feature, both behavioural *and* unit tests are required.

</details>
</issue_to_address>

### Comment 11
<location> `.github/actions/release-to-pypi-uv/scripts/confirm_release.py:16` </location>
<code_context>
+CONFIRM_OPTION = typer.Option("", envvar="INPUT_CONFIRM")
+
+
+def main(expected: str = EXPECTED_OPTION, confirm: str = CONFIRM_OPTION) -> None:
+    if confirm != expected:
+        typer.echo(
</code_context>

<issue_to_address>
**issue (review_instructions):** Add behavioural and unit tests for confirm_release.py to verify confirmation logic and error handling.

This script validates a manual confirmation string. You must provide both behavioural and unit tests to cover correct and incorrect confirmation inputs and error handling.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*`

**Instructions:**
For any new feature or change to an existing feature, both behavioural *and* unit tests are required.

</details>
</issue_to_address>

### Comment 12
<location> `.github/actions/release-to-pypi-uv/README.md:3` </location>
<code_context>
+# Release to PyPI (uv)
+
+Build and publish Python distributions via [uv](https://github.com/astral-sh/uv) with GitHub's
+trusted publishing flow.
+
</code_context>

<issue_to_address>
**issue (review_instructions):** This paragraph exceeds the 80 column wrapping limit for Markdown text.

Please wrap this paragraph so that no line exceeds 80 columns, as per the documentation standards.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*.md`

**Instructions:**
Paragraphs and bullets must be wrapped to 80 columns

</details>
</issue_to_address>

### Comment 13
<location> `.github/actions/release-to-pypi-uv/tests/test_determine_release.py:19` </location>
<code_context>
        cwd=env.get("PWD", None),

</code_context>

<issue_to_address>
**suggestion (code-quality):** Replace `dict.get(x, None)` with `dict.get(x)` ([`remove-none-from-default-get`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/remove-none-from-default-get))

```suggestion
        cwd=env.get("PWD"),
```

<br/><details><summary>Explanation</summary>When using a dictionary's `get` method you can specify a default to return if
the key is not found. This defaults to `None`, so it is unnecessary to specify
`None` if this is the required behaviour. Removing the unnecessary argument
makes the code slightly shorter and clearer.
</details>
</issue_to_address>

### Comment 14
<location> `.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py:91` </location>
<code_context>
def main(
    version: str = VERSION_OPTION,
    pattern: str = PATTERN_OPTION,
    fail_on_dynamic: str = FAIL_ON_DYNAMIC_OPTION,
) -> None:
    files = list(_iter_files(pattern))
    if not files:
        typer.echo(f"::warning::No TOML files matched pattern {pattern}")
        return

    literal_version_errors: list[str] = []
    dynamic_errors: list[str] = []
    checked = 0
    fail_dynamic = _parse_bool(fail_on_dynamic)

    for path in files:
        try:
            data = _load_toml(path)
        except RuntimeError as exc:
            typer.echo(f"::error::{exc}", err=True)
            return

        project = data.get("project")
        if not isinstance(project, dict):
            continue
        checked += 1

        dynamic = project.get("dynamic")
        dynamic_set = {str(item) for item in dynamic} if isinstance(dynamic, (list, tuple)) else set()
        if "version" in dynamic_set:
            message = f"{path}: uses dynamic 'version' (PEP 621)."
            if fail_dynamic:
                dynamic_errors.append(message + " Set fail-on-dynamic-version=false to allow.")
            else:
                typer.echo(f"::notice::{message} Skipping version check.")
            continue

        toml_version = project.get("version")
        if toml_version is None:
            literal_version_errors.append(
                f"{path}: missing [project].version and not marked dynamic"
            )
            continue

        if str(toml_version) != version:
            literal_version_errors.append(
                f"{path}: [project].version '{toml_version}' != tag version '{version}'"
            )

    if dynamic_errors or literal_version_errors:
        for error in (*dynamic_errors, *literal_version_errors):
            typer.echo(f"::error::{error}", err=True)
        raise typer.Exit(1)

    typer.echo(f"Checked {checked} PEP 621 project file(s); all versions match {version}.")

</code_context>

<issue_to_address>
**suggestion (code-quality):** We've found these issues:

- Use f-string instead of string concatenation ([`use-fstring-for-concatenation`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-fstring-for-concatenation/))
- Split conditional into multiple branches ([`split-or-ifs`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/split-or-ifs/))
- Merge duplicate blocks in conditional ([`merge-duplicate-blocks`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/merge-duplicate-blocks/))
- Remove redundant conditional ([`remove-redundant-if`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/remove-redundant-if/))

```suggestion
                dynamic_errors.append(f"{message} Set fail-on-dynamic-version=false to allow.")
```
</issue_to_address>

### Comment 15
<location> `.github/actions/release-to-pypi-uv/tests/test_determine_release.py:12-20` </location>
<code_context>
def run_script(script: Path, *, env: dict[str, str]) -> subprocess.CompletedProcess[str]:
    cmd = ["uv", "run", "--script", str(script)]
    result = subprocess.run(  # noqa: S603
        cmd,
        capture_output=True,
        encoding="utf-8",
        errors="replace",
        env=env,
        check=False,
        cwd=env.get("PWD", None),
    )
    return result

</code_context>

<issue_to_address>
**issue (code-quality):** Inline variable that is immediately returned ([`inline-immediately-returned-variable`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/inline-immediately-returned-variable/))
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread .github/actions/release-to-pypi-uv/scripts/check_github_release.py
Comment thread .github/actions/release-to-pypi-uv/scripts/determine_release.py Outdated
Comment thread .github/actions/release-to-pypi-uv/tests/test_determine_release.py
Comment thread .github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py Outdated
Comment thread .github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py Outdated
Comment thread .github/actions/release-to-pypi-uv/tests/test_determine_release.py Outdated
Comment thread .github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py Outdated
Comment thread .github/actions/release-to-pypi-uv/tests/test_determine_release.py Outdated
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

Codex Review: Here are some suggestions.

Reply with @codex fix comments to fix any unresolved comments.

About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you open a pull request for review, mark a draft as ready, or comment "@codex review". If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex fix this CI failure" or "@codex address that feedback".

Comment thread .github/actions/release-to-pypi-uv/scripts/publish_release.py Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ee76025 and 8d8dff3.

📒 Files selected for processing (13)
  • .github/actions/release-to-pypi-uv/CHANGELOG.md (1 hunks)
  • .github/actions/release-to-pypi-uv/README.md (1 hunks)
  • .github/actions/release-to-pypi-uv/action.yml (1 hunks)
  • .github/actions/release-to-pypi-uv/scripts/check_github_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/scripts/confirm_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/scripts/determine_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/scripts/publish_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py (1 hunks)
  • .github/actions/release-to-pypi-uv/scripts/write_summary.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_determine_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py (1 hunks)
  • README.md (1 hunks)
  • docs/scripting-standards.md (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
{README.md,docs/**}

📄 CodeRabbit inference engine (.rules/python-00.md)

Colocate documentation near reusable packages: keep README.md or docs/ and include usage examples

Files:

  • README.md
  • docs/scripting-standards.md
**/*.md

⚙️ CodeRabbit configuration file

**/*.md: * Avoid 2nd person or 1st person pronouns ("I", "you", "we")

  • Use en-GB-oxendict (-ize / -our) spelling and grammar
  • Headings must not be wrapped.
  • Documents must start with a level 1 heading
  • Headings must correctly increase or decrease by no more than one level at a time
  • Use GitHub-flavoured Markdown style for footnotes and endnotes.
  • Numbered footnotes must be numbered by order of appearance in the document.

Files:

  • README.md
  • docs/scripting-standards.md
🧬 Code graph analysis (6)
.github/actions/release-to-pypi-uv/scripts/publish_release.py (1)
.github/actions/release-to-pypi-uv/scripts/write_summary.py (1)
  • main (20-30)
.github/actions/release-to-pypi-uv/scripts/write_summary.py (3)
.github/actions/release-to-pypi-uv/scripts/check_github_release.py (1)
  • main (75-87)
.github/actions/release-to-pypi-uv/scripts/determine_release.py (1)
  • main (26-53)
.github/actions/release-to-pypi-uv/scripts/publish_release.py (1)
  • main (24-30)
.github/actions/release-to-pypi-uv/tests/test_determine_release.py (1)
shellstub.py (1)
  • env (169-174)
.github/actions/release-to-pypi-uv/scripts/determine_release.py (3)
.github/actions/release-to-pypi-uv/scripts/check_github_release.py (1)
  • main (75-87)
.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py (1)
  • main (59-113)
.github/actions/release-to-pypi-uv/scripts/write_summary.py (1)
  • main (20-30)
.github/actions/release-to-pypi-uv/scripts/check_github_release.py (1)
.github/actions/release-to-pypi-uv/scripts/determine_release.py (1)
  • main (26-53)
.github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py (1)
shellstub.py (1)
  • env (169-174)
🔍 Remote MCP Ref

Additional review facts & quick checks (concise)

  • Composite action outputs: mapping action outputs to a step's outputs (e.g. outputs: tag: ${{ steps.resolve.outputs.tag }}) is supported for composite actions.
  • Setting step outputs from a script: writing lines name=value to the file at $GITHUB_OUTPUT is the documented way to set outputs; determine_release's append-to-GITHUB_OUTPUT matches that pattern.
  • Job summary: appending text to the path in $GITHUB_STEP_SUMMARY is the documented way to add a job summary; write_summary's append usage matches docs.
  • GitHub Release check: GET /repos/{owner}/{repo}/releases/tags/{tag} returns 404 if no release; handling 404/drafts/prerelease as check_github_release.py does is appropriate. Ensure the GH token used by the script has repo/read-release access and that the request sets the expected Accept/Authorization headers.
  • PEP 621 / dynamic versions: projects that declare dynamic = ["version"] will not have a static [project].version; validate_toml_versions' fail-on-dynamic option matches the expected handling pattern (fail vs skip). (Behaviour echoed by the added tests in this PR; confirm policy for dynamic-version projects in your org.)

Reviewer action items (concrete):

  • Verify the composite action consumers will run on runners that install Python 3.13 and uv (the action uses astral-sh/setup-uv and scripts call uv run --script). Ensure the setup action is available/pinned in workflows.
  • Confirm cmd_utils.run_cmd (imported by publish_release.py) exists in the repo and properly masks secrets / checks return codes.
  • Confirm the action's use of the GH token and any PyPI tokens (via uv publish) follows your org's secret/permissions policy and that logs will not expose credentials.

Tooling note: I fetched GitHub docs and related references while preparing these checks; see tool calls used below for source verification.

⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Sourcery review
  • GitHub Check: python-tests (windows-latest)
  • GitHub Check: python-tests (ubuntu-latest)
🔇 Additional comments (7)
README.md (1)

12-12: Add action entry: LGTM.

Register the new action in the table correctly.

.github/actions/release-to-pypi-uv/CHANGELOG.md (1)

1-8: Changelog entry reads cleanly.

Date and versioning look correct; scope matches the implementation.

docs/scripting-standards.md (1)

1-80: Standards read crisply and follow repository guidelines.

Headings, en‑GB spelling, and neutral voice comply with house style.

.github/actions/release-to-pypi-uv/scripts/confirm_release.py (1)

12-25: Confirmation gate: LGTM.

Enforce a strict match, emit a GitHub annotation, and exit non‑zero on failure.

.github/actions/release-to-pypi-uv/scripts/publish_release.py (1)

24-30: Publish flow: LGTM post‑import fix.

Command selection and messaging are appropriate; delegate exit‑code handling to run_cmd.

Run the checks below to ensure the import now resolves and that run_cmd enforces non‑zero exit handling and masks secrets.

#!/bin/bash
set -euo pipefail

echo "Confirm cmd_utils location relative to publish_release.py…"
PUBLISH_DIR=$(dirname ".github/actions/release-to-pypi-uv/scripts/publish_release.py")
test -f "$PUBLISH_DIR/cmd_utils.py" && echo "Found: $PUBLISH_DIR/cmd_utils.py" || { echo "Missing cmd_utils.py next to publish_release.py"; exit 1; }

echo "Inspect run_cmd signature and behaviour…"
rg -nC3 'def\s+run_cmd\s*\(' "$PUBLISH_DIR/cmd_utils.py" || true
.github/actions/release-to-pypi-uv/action.yml (1)

40-50: Surface resolve outputs robustly.

Mapping outputs to step outputs is correct. Keep step id stable; changing it is a breaking change for consumers relying on action outputs.

.github/actions/release-to-pypi-uv/tests/test_determine_release.py (1)

24-31: Clear GITHUB_REF_TYPE and GITHUB_REF_NAME in base_env
Reset these variables in base_env to avoid CI-provided refs flipping test_resolves_tag_from_input. Apply:

 merged["GITHUB_REF_TYPE"] = ""
 merged["GITHUB_REF_NAME"] = ""

Comment thread .github/actions/release-to-pypi-uv/action.yml
Comment thread .github/actions/release-to-pypi-uv/README.md
Comment thread .github/actions/release-to-pypi-uv/README.md
Comment thread .github/actions/release-to-pypi-uv/scripts/check_github_release.py Outdated
Comment thread .github/actions/release-to-pypi-uv/scripts/determine_release.py Outdated
Comment thread .github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py Outdated
Comment thread .github/actions/release-to-pypi-uv/scripts/write_summary.py
Comment thread .github/actions/release-to-pypi-uv/tests/test_determine_release.py
Comment thread .github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
.github/actions/release-to-pypi-uv/action.yml (1)

40-97: Add branding for Marketplace discoverability.

Surface icon and colour to improve UX when browsing actions.

Apply at the top-level:

 name: Release to PyPI (uv)
 description: Build and publish Python distributions to PyPI using uv.
+branding:
+  icon: package
+  color: purple
♻️ Duplicate comments (2)
.github/actions/release-to-pypi-uv/README.md (2)

47-52: Fix invalid local action reference in usage.

Path-based actions must not include a ref. Either remove the ref for local usage or switch to the repo path for external consumption.

Apply one of:

-      - name: Build and publish
-        uses: ./.github/actions/release-to-pypi-uv@v1
+      - name: Build and publish
+        uses: ./.github/actions/release-to-pypi-uv

or

-      - name: Build and publish
-        uses: ./.github/actions/release-to-pypi-uv@v1
+      - name: Build and publish
+        uses: leynos/shared-actions/.github/actions/release-to-pypi-uv@v1

25-26: Document token wiring for the GitHub Release check.

State explicitly that the action sets GH_TOKEN from github.token internally, so users do not need to pass GH_TOKEN in their workflow.

Apply this addition under “Required permissions”:

 > **Required permissions**: set the job to `permissions: contents: read` and `permissions: id-token: write` so uv Trusted Publishing can exchange an OIDC token with PyPI.
+
+Note: This action wires `GH_TOKEN` from `github.token` for the GitHub Release check; no additional env mapping is required in your workflow.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8d8dff3 and 3dd6aab.

📒 Files selected for processing (3)
  • .github/actions/release-to-pypi-uv/CHANGELOG.md (1 hunks)
  • .github/actions/release-to-pypi-uv/README.md (1 hunks)
  • .github/actions/release-to-pypi-uv/action.yml (1 hunks)
⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: python-tests (windows-latest)
  • GitHub Check: python-tests (ubuntu-latest)
  • GitHub Check: Sourcery review
🔇 Additional comments (4)
.github/actions/release-to-pypi-uv/CHANGELOG.md (1)

3-13: Changelog entries read cleanly and match the shipped surface.

Ship v1.0.1 note aligns with README/action.yml updates about required permissions; initial feature list is scoped and accurate.

.github/actions/release-to-pypi-uv/action.yml (3)

3-5: Good: call out required OIDC permissions in metadata.

Keep this comment block; it prevents a common Trusted Publishing failure mode.


65-71: Good: wire GH_TOKEN and repository for the GitHub Release check.

Leave the internal env wiring; this removes friction for consumers.


43-51: Mark resolved — setup-uv v6.4.3 pinned to commit e92bafb6253dcd438e0484186d7669ea7a8ca1cc (exact match)
v6.4.3 resolves to e92bafb6253dcd438e0484186d7669ea7a8ca1cc; no change required.

Comment thread .github/actions/release-to-pypi-uv/action.yml Outdated
Comment thread .github/actions/release-to-pypi-uv/README.md
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Sep 18, 2025

@coderabbitai pause

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Sep 18, 2025

✅ Actions performed

Reviews paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

♻️ Duplicate comments (3)
.github/actions/release-to-pypi-uv/scripts/determine_release.py (1)

43-49: Support SemVer prerelease/build metadata if policy allows.

Current regex blocks tags like v1.2.3-rc.1 or v1.2.3+build. Enable full SemVer or confirm strict MAJOR.MINOR.PATCH is intended.

Apply if prereleases/builds should pass:

-    if not re.fullmatch(r"v\d+\.\d+\.\d+", resolved_tag):
+    if not re.fullmatch(
+        r"v\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?",
+        resolved_tag,
+    ):
.github/actions/release-to-pypi-uv/action.yml (2)

3-5: Call out permissions in README usage too; keep OIDC requirements front-and-centre.

Mirror these exact permissions in the action README usage snippet so users don’t miss them at job level.

Quote the PyPI docs that state id-token: write is mandatory for Trusted Publishing, and whether contents: read is recommended/required for related steps.

(Refs: PyPI Trusted Publishing docs and Marketplace pages corroborate id-token: write; contents: read is commonly recommended when the workflow reads repo content.) (docs.pypi.org)


47-55: Propagate interpreter via setup-uv and key the cache by Python version.

Set python-version on setup-uv so UV_PYTHON is exported to subsequent steps; include the Python version in the cache-suffix to avoid cross-version cache collisions. (github.com)

File: .github/actions/release-to-pypi-uv/action.yml Lines: 47-55

     - name: Setup uv
       # v6.4.3
       uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc
       with:
+        python-version: ${{ inputs.python-version }}
         cache-dependency-glob: |
           **/pyproject.toml
           **/uv.lock
-        cache-suffix: action-${{ github.action_ref || github.sha }}
+        cache-suffix: action-${{ github.action_ref || github.sha }}-py${{ inputs.python-version }}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3dd6aab and 9ed7ee0.

📒 Files selected for processing (15)
  • .github/actions/release-to-pypi-uv/CHANGELOG.md (1 hunks)
  • .github/actions/release-to-pypi-uv/README.md (1 hunks)
  • .github/actions/release-to-pypi-uv/action.yml (1 hunks)
  • .github/actions/release-to-pypi-uv/scripts/check_github_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/scripts/determine_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/scripts/publish_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py (1 hunks)
  • .github/actions/release-to-pypi-uv/scripts/write_summary.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/_helpers.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_check_github_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_confirm_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_determine_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_publish_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_write_summary.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (11)
.github/actions/release-to-pypi-uv/tests/test_publish_release.py (1)
.github/actions/release-to-pypi-uv/tests/_helpers.py (1)
  • load_script_module (13-21)
.github/actions/release-to-pypi-uv/tests/test_confirm_release.py (1)
.github/actions/release-to-pypi-uv/tests/test_determine_release.py (1)
  • base_env (23-30)
.github/actions/release-to-pypi-uv/scripts/write_summary.py (2)
.github/actions/release-to-pypi-uv/scripts/determine_release.py (1)
  • main (26-53)
.github/actions/release-to-pypi-uv/scripts/publish_release.py (1)
  • main (53-59)
.github/actions/release-to-pypi-uv/tests/test_check_github_release.py (2)
.github/actions/release-to-pypi-uv/tests/_helpers.py (1)
  • load_script_module (13-21)
.github/actions/release-to-pypi-uv/scripts/check_github_release.py (1)
  • main (97-109)
.github/actions/release-to-pypi-uv/scripts/publish_release.py (1)
.github/actions/release-to-pypi-uv/scripts/write_summary.py (1)
  • main (20-38)
.github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py (1)
shellstub.py (1)
  • env (169-174)
.github/actions/release-to-pypi-uv/tests/test_write_summary.py (1)
.github/actions/release-to-pypi-uv/tests/_helpers.py (1)
  • load_script_module (13-21)
.github/actions/release-to-pypi-uv/scripts/check_github_release.py (1)
.github/actions/release-to-pypi-uv/tests/test_check_github_release.py (1)
  • read (25-26)
.github/actions/release-to-pypi-uv/scripts/determine_release.py (3)
.github/actions/release-to-pypi-uv/scripts/check_github_release.py (1)
  • main (97-109)
.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py (1)
  • main (68-122)
.github/actions/release-to-pypi-uv/scripts/write_summary.py (1)
  • main (20-38)
.github/actions/release-to-pypi-uv/tests/test_determine_release.py (1)
shellstub.py (1)
  • env (169-174)
.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py (1)
.github/actions/release-to-pypi-uv/scripts/determine_release.py (1)
  • main (26-53)
⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Sourcery review
🔇 Additional comments (29)
.github/actions/release-to-pypi-uv/CHANGELOG.md (1)

3-9: Ship these entries as‑is.

Content, dates, and scope read cleanly; no action needed.

.github/actions/release-to-pypi-uv/scripts/write_summary.py (1)

20-38: Keep the Markdown summary; it’s tidy and idempotent.

Heading, bullets, and prefix logic are sound.

.github/actions/release-to-pypi-uv/scripts/check_github_release.py (1)

37-71: Leave the retry/timeout and 403/404 handling as implemented.

Backoff, headers, and actionable errors are appropriate.

.github/actions/release-to-pypi-uv/tests/test_publish_release.py (1)

22-46: Retain these tests; behaviour coverage is crisp.

Default vs custom index calls are asserted correctly.

.github/actions/release-to-pypi-uv/tests/test_check_github_release.py (7)

15-27: LGTM! Clean context manager implementation.

The DummyResponse class properly implements the context manager protocol and correctly encodes the JSON payload.


34-44: LGTM! Well-structured success test.

The test correctly validates the published release scenario with proper output assertion.


46-57: LGTM! Draft release handling validated.

The test properly checks that draft releases trigger an error with the expected message.


59-70: LGTM! Prerelease validation covered.

The test correctly verifies that prereleases are rejected with appropriate error messaging.


72-89: LGTM! 404 error handling tested.

The test properly simulates and validates the missing release scenario with HTTPError.


91-111: LGTM! Permission error handling verified.

The test correctly validates 403 Forbidden responses and checks for the permission error message.


113-129: LGTM! Retry logic thoroughly tested.

The test effectively validates the retry mechanism with URLError, ensuring three attempts are made before success.

.github/actions/release-to-pypi-uv/tests/test_write_summary.py (3)

18-32: LGTM! Core functionality validated.

The test properly verifies that the summary is written with the correct format and content for the default index case.


34-48: LGTM! Append behaviour tested.

The test correctly validates that existing content is preserved and only one header is added when appending to an existing file.


50-59: LGTM! Error propagation verified.

The test appropriately checks that IO errors are raised when writing to non-existent paths.

.github/actions/release-to-pypi-uv/scripts/publish_release.py (2)

17-44: LGTM! Path resolution logic is comprehensive.

The _extend_sys_path function properly handles both GitHub Action and local execution contexts, with appropriate error handling for missing parent directories.


53-60: LGTM! Publishing logic correctly handles index parameter.

The main function appropriately branches based on whether an index is provided and uses clear messaging for each case.

.github/actions/release-to-pypi-uv/tests/test_confirm_release.py (3)

11-26: LGTM! Helper function properly configured.

The run_confirm function correctly sets up the environment and executes the script with appropriate subprocess parameters.


28-33: LGTM! Success case validated.

The test properly verifies that matching confirmation strings result in success with the expected output.


35-39: LGTM! Failure case tested.

The test correctly validates that mismatched confirmation strings result in failure with the appropriate error message.

.github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py (8)

13-32: LGTM! Test runner well-configured.

The _run helper function properly sets up the environment with PYTHONPATH and required variables for script execution.


34-49: LGTM! Version match validation covered.

The test correctly verifies that matching versions pass validation with the expected success message.


51-66: LGTM! Version mismatch detection tested.

The test properly validates that version mismatches are caught and reported with the correct error message.


68-83: LGTM! Dynamic version failure case covered.

The test correctly verifies that dynamic versions trigger failure when the fail flag is set to true.


85-94: LGTM! Parse error handling validated.

The test properly checks that invalid TOML syntax triggers an error with the appropriate message.


96-111: LGTM! Dynamic version allowance tested.

The test correctly validates that dynamic versions are permitted when the fail flag is false, with appropriate notice in stdout.


113-127: LGTM! Missing project section handling verified.

The test properly confirms that TOML files without a [project] section are ignored and don't cause failures.


129-153: LGTM! Multiple file validation tested.

The test effectively validates that mismatches are detected across multiple TOML files in different directories.

.github/actions/release-to-pypi-uv/action.yml (1)

48-55: Pin the setup-uv ref to the intended release tag or confirm the SHA.

Either pin to a tag (e.g. v6.4.3) or verify the SHA matches that release to keep supply‑chain hygiene tight.

Does tag v6.4.3 of astral-sh/setup-uv correspond to commit e92bafb6253dcd438e0484186d7669ea7a8ca1cc?
.github/actions/release-to-pypi-uv/tests/test_determine_release.py (1)

45-57: LGTM: test matrix robustly locks the resolve paths and error modes.

Keep this coverage; it nails happy paths and key failures (bad tag, missing env, malformed version).

Also applies to: 59-70, 72-82, 84-95, 97-107, 109-119, 121-131, 133-143

Comment thread .github/actions/release-to-pypi-uv/action.yml
Comment thread .github/actions/release-to-pypi-uv/action.yml
Comment thread .github/actions/release-to-pypi-uv/README.md
Comment thread .github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py Outdated
Comment thread .github/actions/release-to-pypi-uv/tests/_helpers.py Outdated
Comment thread .github/actions/release-to-pypi-uv/tests/test_determine_release.py
Comment thread .github/actions/release-to-pypi-uv/tests/test_determine_release.py
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

♻️ Duplicate comments (2)
.github/actions/release-to-pypi-uv/README.md (1)

52-54: Forward the python-version input to setup-uv.

The README shows setup-uv with a python-version input, but the composite action doesn't wire this through. Add the input mapping to ensure the specified Python version is actually installed.

Apply this diff to the action.yml:

     - uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc
       with:
+        python-version: ${{ inputs.python-version }}
         cache-dependency-glob: |
           **/pyproject.toml
           **/uv.lock
.github/actions/release-to-pypi-uv/tests/test_determine_release.py (1)

10-20: Add uv availability check to prevent test failures.

The test suite will crash with FileNotFoundError if uv isn't installed. Add a skip condition to handle environments where uv is unavailable.

Apply this diff:

+import shutil
 import os
 import subprocess
 from pathlib import Path
+import pytest


+# Skip tests if uv is not available
+pytestmark = pytest.mark.skipif(shutil.which("uv") is None, reason="uv not available")
+

 def run_script(script: Path, *, env: dict[str, str]) -> subprocess.CompletedProcess[str]:
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9ed7ee0 and 3837291.

📒 Files selected for processing (7)
  • .github/actions/release-to-pypi-uv/README.md (1 hunks)
  • .github/actions/release-to-pypi-uv/action.yml (1 hunks)
  • .github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/_helpers.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_determine_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py (1 hunks)
  • docs/cmd-mox-users-guide.md (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
{README.md,docs/**}

📄 CodeRabbit inference engine (.rules/python-00.md)

Colocate documentation near reusable packages: keep README.md or docs/ and include usage examples

Files:

  • docs/cmd-mox-users-guide.md
**/*.md

⚙️ CodeRabbit configuration file

**/*.md: * Avoid 2nd person or 1st person pronouns ("I", "you", "we")

  • Use en-GB-oxendict (-ize / -our) spelling and grammar
  • Headings must not be wrapped.
  • Documents must start with a level 1 heading
  • Headings must correctly increase or decrease by no more than one level at a time
  • Use GitHub-flavoured Markdown style for footnotes and endnotes.
  • Numbered footnotes must be numbered by order of appearance in the document.

Files:

  • docs/cmd-mox-users-guide.md
🧬 Code graph analysis (3)
.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py (1)
.github/actions/release-to-pypi-uv/scripts/determine_release.py (1)
  • main (26-53)
.github/actions/release-to-pypi-uv/tests/test_determine_release.py (1)
shellstub.py (1)
  • env (169-174)
.github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py (2)
.github/actions/release-to-pypi-uv/tests/_helpers.py (1)
  • load_script_module (21-29)
.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py (2)
  • main (75-129)
  • _parse_bool (49-55)
🔍 Remote MCP

Additional context for PR #90 review

  • Astral-sh/setup-uv pin
    The composite action’s action.yml pins astral-sh/setup-uv to version v6.4.3, which was published on July 23, 2025 and includes a bug-fix for handling relative paths beginning with dots. This confirms the referenced version exists and is up-to-date for the action’s setup step. (github.com)

  • Composite action permissions behavior
    According to GitHub’s documentation, composite actions cannot declare or override job-level permissions in their metadata; any required permissions (e.g. id-token: write for trusted publishing) must be set in the calling workflow. Reviewers should verify that workflows invoking this action include the necessary permissions block for GitHub token forwarding and environment protection. (docs.github.com)

⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Sourcery review
  • GitHub Check: python-tests (windows-latest)
  • GitHub Check: python-tests (ubuntu-latest)
  • GitHub Check: build-release (windows-latest, x86_64-pc-windows-gnu)
  • GitHub Check: build-release (windows-latest, aarch64-pc-windows-gnu)
🔇 Additional comments (16)
docs/cmd-mox-users-guide.md (1)

1-6: Open with a level‑1 heading; heading progression is correct.

Keep this structure; it complies with the docs rules (H1 start, no wrapped headings, single‑level increments).

.github/actions/release-to-pypi-uv/tests/_helpers.py (2)

10-18: LGTM!

The path resolution logic correctly handles both GitHub Actions runtime (GITHUB_ACTION_PATH) and local development scenarios. The fallback calculation properly navigates the directory structure to locate the repository root and scripts directory.


21-29: LGTM!

The module loading implementation is robust, with proper error handling for missing specs or loaders. The consistent module naming convention (release_to_pypi_uv_{name}) aids debugging.

.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py (6)

1-6: LGTM!

The uv script shebang and metadata are correctly configured for Python 3.13+ with the typer dependency. The script description clearly explains its purpose.


17-22: LGTM!

The Typer options are properly configured with environment variable bindings that match the composite action's environment setup. The defaults align with the action inputs.


24-33: LGTM!

The skip list comprehensively covers common directories that should be excluded from TOML discovery, including VCS, virtualenv, build artifacts, and development tools.


49-55: LGTM!

The boolean parsing function handles None values and empty strings correctly, using a comprehensive set of truthy values for robust environment variable interpretation.


102-110: LGTM!

The dynamic version handling logic correctly distinguishes between failure and notice modes based on the fail_on_dynamic flag, providing clear feedback in both cases.


124-127: LGTM!

The error aggregation and reporting correctly exits with code 1 when any validation errors are found, ensuring proper CI failure propagation.

.github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py (4)

12-14: LGTM!

The module fixture correctly uses the helper to load the target script module for testing.


84-105: LGTM!

The parameterised test comprehensively covers various truthy string representations, ensuring robust boolean parsing across different input formats.


139-159: LGTM!

The parameterised test for falsey variants ensures the boolean parser correctly handles all expected false-like values, including edge cases like empty strings.


201-227: LGTM!

The mixed validity test properly exercises the error aggregation logic when multiple TOML files have different validation outcomes, ensuring all errors are collected and reported.

.github/actions/release-to-pypi-uv/action.yml (1)

1-5: LGTM!

The action metadata clearly documents the required permissions for OIDC trusted publishing in the comment, helping users understand the necessary workflow configuration.

.github/actions/release-to-pypi-uv/tests/test_determine_release.py (2)

46-57: LGTM!

The test correctly validates tag resolution from GitHub ref environment variables and verifies both the exit code and output parsing.


85-95: LGTM!

The negative test case properly exercises the error path when no tag is provided and the workflow isn't running on a tag ref, validating both exit code and error message.

Comment thread .github/actions/release-to-pypi-uv/action.yml
Comment thread .github/actions/release-to-pypi-uv/action.yml Outdated
Comment thread docs/cmd-mox-users-guide.md Outdated
Comment thread docs/cmd-mox-users-guide.md Outdated
Comment thread docs/cmd-mox-users-guide.md Outdated
Comment thread docs/cmd-mox-users-guide.md Outdated
Comment thread docs/cmd-mox-users-guide.md
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Sep 20, 2025

@coderabbitai please create a new GitHub issue for the following:

Prepare all required actions
Getting action download info
Download action repository 'astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6' (SHA:557e51de59eb14aaaba2ed9621916900a91d50c6)
Download action repository 'leynos/shared-actions@f9f1c863c8a5bef64aa6779caa746e1a4a6c1ad4' (SHA:f9f1c863c8a5bef64aa6779caa746e1a4a6c1ad4)
Download action repository 'Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0' (SHA:98c8021b550208e191a6a3145459bfc9fb29c4c0)
Getting action download info
Download action repository 'actions-rust-lang/setup-rust-toolchain@9d7e65c320fdb52dcd45ffaa68deb6c02c8754d9' (SHA:9d7e65c320fdb52dcd45ffaa68deb6c02c8754d9)
Download action repository 'astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc' (SHA:e92bafb6253dcd438e0484186d7669ea7a8ca1cc)
Download action repository 'actions/cache@v4' (SHA:0400d5f644dc74513175e3cd8d07132dd4860809)
Download action repository 'mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad' (SHA:7d986dd989559c6ecdb630a3fd2557667be217ad)
Download action repository 'msys2/setup-msys2@v2' (SHA:fb197b72ce45fb24f17bf3f807a388985654d1f2)
Download action repository 'mbround18/setup-osxcross@b26146d499c54979ed3d023266865dc188881911' (SHA:b26146d499c54979ed3d023266865dc188881911)
Getting action download info
Download action repository 'Swatinem/rust-cache@v2' (SHA:98c8021b550208e191a6a3145459bfc9fb29c4c0)
Getting action download info
Getting action download info
Getting action download info
Download action repository 'actions/cache@v3' (SHA:2f8e54208210a422b2efd51efaa6bd6d7ca8920f)
Run ./.github/actions/rust-build-release
Run astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6
Trying to find version for uv in: /home/runner/work/shared-actions/shared-actions/uv.toml
Could not find file: /home/runner/work/shared-actions/shared-actions/uv.toml
Trying to find version for uv in: /home/runner/work/shared-actions/shared-actions/pyproject.toml
Could not determine uv version from uv.toml or pyproject.toml. Falling back to latest.
Getting latest version from GitHub API...
manifest-file not provided, reading from local file.
manifest-file does not contain version 0.8.19, arch x86_64, platform unknown-linux-gnu. Falling back to GitHub releases.
Downloading uv from "https://github.com/astral-sh/uv/releases/download/0.8.19/uv-x86_64-unknown-linux-gnu.tar.gz" ...
/usr/bin/tar xz --warning=no-unknown-keyword --overwrite -C /home/runner/work/_temp/1dca5f78-f4be-418b-b2b1-52f509abed29 -f /home/runner/work/_temp/5134777d-0300-431b-b77d-0149b4f45353
Added /home/runner/.local/bin to the path
Added /opt/hostedtoolcache/uv/0.8.19/x86_64 to the path
Set UV_CACHE_DIR to /home/runner/work/_temp/setup-uv-cache
Successfully installed uv version 0.8.19
Searching files using cache dependency glob: /home/runner/work/shared-actions/shared-actions/**/*requirements*.txt,/home/runner/work/shared-actions/shared-actions/**/*requirements*.in,/home/runner/work/shared-actions/shared-actions/**/*constraints*.txt,/home/runner/work/shared-actions/shared-actions/**/*constraints*.in,/home/runner/work/shared-actions/shared-actions/**/pyproject.toml,/home/runner/work/shared-actions/shared-actions/**/uv.lock
/home/runner/work/shared-actions/shared-actions/pyproject.toml
/home/runner/work/shared-actions/shared-actions/uv.lock
Found 2 files to hash.
Trying to restore uv cache from GitHub Actions cache with key: setup-uv-1-x86_64-unknown-linux-gnu-3.12.3-pruned-16faa7bf93c13f42f512c13a8ded98b919819d261fffd7df23016a935ce80587
No GitHub Actions cache found for key: setup-uv-1-x86_64-unknown-linux-gnu-3.12.3-pruned-16faa7bf93c13f42f512c13a8ded98b919819d261fffd7df23016a935ce80587
Run set -euo pipefail
Downloading pygments (1.2MiB)
 Downloading pygments
Installed 8 packages in 11ms
Run set -euo pipefail
Run leynos/shared-actions/.github/actions/setup-rust@f9f1c863c8a5bef64aa6779caa746e1a4a6c1ad4
Run actions-rust-lang/setup-rust-toolchain@9d7e65c320fdb52dcd45ffaa68deb6c02c8754d9
Run : construct rustup command line
Run if [[ ! -v CARGO_INCREMENTAL ]]; then
Run echo "::add-matcher::/home/runner/work/_actions/actions-rust-lang/setup-rust-toolchain/9d7e65c320fdb52dcd45ffaa68deb6c02c8754d9/rust.json"
Run if ! command -v rustup &> /dev/null ; then
Run if [[ -z "$toolchain" && ( -f "rust-toolchain" || -f "rust-toolchain.toml" ) ]]
info: syncing channel updates for '1.89.0-x86_64-unknown-linux-gnu'
info: latest update on 2025-08-07, rust version 1.89.0 (29483883e 2025-08-04)
info: downloading component 'cargo'
info: downloading component 'clippy'
info: downloading component 'llvm-tools'
info: downloading component 'rust-std'
info: downloading component 'rustc'
info: downloading component 'rustfmt'
info: installing component 'cargo'
info: installing component 'clippy'
info: installing component 'llvm-tools'
info: installing component 'rust-std'
info: installing component 'rustc'
info: installing component 'rustfmt'

  1.89.0-x86_64-unknown-linux-gnu installed - rustc 1.89.0 (29483883e 2025-08-04)

info: override toolchain for '/home/runner/work/shared-actions/shared-actions' set to '1.89.0-x86_64-unknown-linux-gnu'
Run echo "rustc-version=$(rustc --version)" >> $GITHUB_OUTPUT
rustc 1.89.0 (29483883e 2025-08-04)
binary: rustc
commit-hash: 29483883eed69d5fb4db01964cdf2af4d86e9cb2
commit-date: 2025-08-04
host: x86_64-unknown-linux-gnu
release: 1.89.0
LLVM version: 20.1.7
cargo 1.89.0 (c24e10642 2025-06-23)
release: 1.89.0
commit-hash: c24e1064277fe51ab72011e2612e556ac56addf7
commit-date: 2025-06-23
host: x86_64-unknown-linux-gnu
libgit2: 1.9.0 (sys:0.20.2 vendored)
libcurl: 8.12.1-DEV (sys:0.4.80+curl-8.12.1 vendored ssl:OpenSSL/3.5.0)
ssl: OpenSSL 3.5.0 8 Apr 2025
os: Ubuntu 24.4.0 (noble) [64-bit]
info: This is the version for the rustup toolchain manager, not the rustc compiler.
info: The currently active `rustc` version is `rustc 1.89.0 (29483883e 2025-08-04)`
info: This is the version for the rustup toolchain manager, not the rustc compiler.
rustup 1.28.2 (e4f3ad6f8 2025-04-28)
info: The currently active `rustc` version is `rustc 1.89.0 (29483883e 2025-08-04)`
Run # Not all versions support setting CARGO_REGISTRIES_CRATES_IO_PROTOCOL
Run Swatinem/rust-cache@v2
Error: The process '/home/runner/.cargo/bin/cargo' failed with exit code 101
    at ExecState._setResult (/home/runner/work/_actions/Swatinem/rust-cache/v2/dist/restore/index.js:9728:25)
    at ExecState.CheckComplete (/home/runner/work/_actions/Swatinem/rust-cache/v2/dist/restore/index.js:9711:18)
    at ChildProcess.<anonymous> (/home/runner/work/_actions/Swatinem/rust-cache/v2/dist/restore/index.js:9605:27)
    at ChildProcess.emit (node:events:524:28)
    at maybeClose (node:internal/child_process:1104:16)
    at ChildProcess._handle.onexit (node:internal/child_process:304:5) {
  commandFailed: {
    command: 'cargo metadata --all-features --format-version 1 --no-deps',
    stderr: '\x1B[1m\x1B[31merror\x1B[0m\x1B[1m:\x1B[0m could not find `Cargo.toml` in `/home/runner/work/shared-actions/shared-actions` or any parent directory\n'
  }
}
Cache Configuration

... Restoring cache ...
Cache hit for: v0-rust-build-release-Linux-x64-3dfd62ae-da39a3ee
Received 2483827 of 2483827 (100.0%), 39.5 MBs/sec
Cache Size: ~2 MB (2483827 B)
/usr/bin/tar -xf /home/runner/work/_temp/b604decf-9e55-4eb7-90d1-c3f9038a4f83/cache.tzst -P -C /home/runner/work/shared-actions/shared-actions --use-compress-program unzstd
Cache restored successfully
Restored from cache key "v0-rust-build-release-Linux-x64-3dfd62ae-da39a3ee" full match: true.
Run astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc
Trying to find version for uv in: /home/runner/work/shared-actions/shared-actions/uv.toml
Could not find file: /home/runner/work/shared-actions/shared-actions/uv.toml
Trying to find version for uv in: /home/runner/work/shared-actions/shared-actions/pyproject.toml
Could not determine uv version from uv.toml or pyproject.toml. Falling back to latest.
Found uv in tool-cache for 0.8.19
Added /home/runner/.local/bin to the path
Added /opt/hostedtoolcache/uv/0.8.19/x86_64 to the path
Set UV_CACHE_DIR to /home/runner/work/_temp/setup-uv-cache
Successfully installed uv version 0.8.19
Searching files using cache dependency glob: /home/runner/work/shared-actions/shared-actions/**/pyproject.toml,/home/runner/work/shared-actions/shared-actions/**/uv.lock,/home/runner/work/shared-actions/shared-actions/**/scripts/*.py
/home/runner/work/shared-actions/shared-actions/.github/actions/generate-coverage/scripts/cmd_utils_loader.py
/home/runner/work/shared-actions/shared-actions/.github/actions/generate-coverage/scripts/coverage_parsers.py
/home/runner/work/shared-actions/shared-actions/.github/actions/generate-coverage/scripts/detect.py
/home/runner/work/shared-actions/shared-actions/.github/actions/generate-coverage/scripts/install_cargo_llvm_cov.py
/home/runner/work/shared-actions/shared-actions/.github/actions/generate-coverage/scripts/merge_cobertura.py
/home/runner/work/shared-actions/shared-actions/.github/actions/generate-coverage/scripts/ratchet_coverage.py
/home/runner/work/shared-actions/shared-actions/.github/actions/generate-coverage/scripts/run_python.py
/home/runner/work/shared-actions/shared-actions/.github/actions/generate-coverage/scripts/run_rust.py
/home/runner/work/shared-actions/shared-actions/.github/actions/generate-coverage/scripts/set_outputs.py
/home/runner/work/shared-actions/shared-actions/.github/actions/generate-coverage/scripts/shared_utils.py
/home/runner/work/shared-actions/shared-actions/.github/actions/ratchet-coverage/scripts/install_cargo_llvm_cov.py
/home/runner/work/shared-actions/shared-actions/.github/actions/ratchet-coverage/scripts/ratchet_coverage.py
/home/runner/work/shared-actions/shared-actions/.github/actions/ratchet-coverage/scripts/run_coverage.py
/home/runner/work/shared-actions/shared-actions/.github/actions/release-to-pypi-uv/scripts/check_github_release.py
/home/runner/work/shared-actions/shared-actions/.github/actions/release-to-pypi-uv/scripts/confirm_release.py
/home/runner/work/shared-actions/shared-actions/.github/actions/release-to-pypi-uv/scripts/determine_release.py
/home/runner/work/shared-actions/shared-actions/.github/actions/release-to-pypi-uv/scripts/publish_release.py
/home/runner/work/shared-actions/shared-actions/.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py
/home/runner/work/shared-actions/shared-actions/.github/actions/release-to-pypi-uv/scripts/write_summary.py
/home/runner/work/shared-actions/shared-actions/.github/actions/rust-build-release/scripts/__init__.py
/home/runner/work/shared-actions/shared-actions/.github/actions/rust-build-release/scripts/cmd_utils.py
/home/runner/work/shared-actions/shared-actions/.github/actions/rust-build-release/scripts/package.py
/home/runner/work/shared-actions/shared-actions/.github/actions/rust-build-release/scripts/polythene.py
/home/runner/work/shared-actions/shared-actions/.github/actions/rust-build-release/scripts/script_utils.py
/home/runner/work/shared-actions/shared-actions/.github/actions/setup-rust/scripts/copy_openbsd_stdlib.py
/home/runner/work/shared-actions/shared-actions/.github/actions/setup-rust/scripts/validate_workspaces.py
/home/runner/work/shared-actions/shared-actions/pyproject.toml
/home/runner/work/shared-actions/shared-actions/uv.lock
Found 28 files to hash.
Trying to restore uv cache from GitHub Actions cache with key: setup-uv-1-x86_64-unknown-linux-gnu-3.12.3-pruned-cc07394c1f0a79ba68fb8cb80740c9eb801c02f75895cec7525aeac53726511d
No GitHub Actions cache found for key: setup-uv-1-x86_64-unknown-linux-gnu-3.12.3-pruned-cc07394c1f0a79ba68fb8cb80740c9eb801c02f75895cec7525aeac53726511d
Run actions/cache@v4
Cache hit for: Linux-cargo--1955aa672c6e1c8c429e888eb3954deb5024388412f955318c1cb570874e7685
Received 46605097 of 46605097 (100.0%), 109.5 MBs/sec
Cache Size: ~44 MB (46605097 B)
/usr/bin/tar -xf /home/runner/work/_temp/a0c4a939-9d1d-440d-a714-51bde61ee02e/cache.tzst -P -C /home/runner/work/shared-actions/shared-actions --use-compress-program unzstd
Cache restored successfully
Cache restored from key: Linux-cargo--1955aa672c6e1c8c429e888eb3954deb5024388412f955318c1cb570874e7685
Run mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad
try to setup sccache version: v0.10.0
sccache download from url: https://github.com/mozilla/sccache/releases/download/v0.10.0/sccache-v0.10.0-x86_64-unknown-linux-musl.tar.gz
Correct checksum: 1fbb35e135660d04a2d5e42b59c7874d39b3deb17de56330b25b713ec59f849b
/usr/bin/tar xz --warning=no-unknown-keyword --overwrite -C /home/runner/work/_temp/4067f292-6e08-4728-97bb-8a0590d66140 -f /home/runner/work/_temp/4f878a8d-724b-4a75-88c0-16d3a5b62cb5
sccache extracted to: /home/runner/work/_temp/4067f292-6e08-4728-97bb-8a0590d66140
sccache cached to: /opt/hostedtoolcache/sccache/0.10.0/x64
Warning: Unexpected input(s) 'toolchain', valid inputs are ['prefix-key', 'shared-key', 'key', 'env-vars', 'workspaces', 'cache-directories', 'cache-targets', 'cache-on-failure', 'cache-all-crates', 'cache-workspace-crates', 'save-if', 'cache-provider', 'cache-bin', 'lookup-only']
Run Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0
Cache Configuration

... Restoring cache ...
Cache hit for: v0-rust-build-release-Linux-x64-3dfd62ae-12bd91a9
Received 31244472 of 31244472 (100.0%), 85.4 MBs/sec
Cache Size: ~30 MB (31244472 B)
/usr/bin/tar -xf /home/runner/work/_temp/57a95fbb-b111-473f-a410-a2cef82df9dd/cache.tzst -P -C /home/runner/work/shared-actions/shared-actions --use-compress-program unzstd
Cache restored successfully
Restored from cache key "v0-rust-build-release-Linux-x64-3dfd62ae-12bd91a9" full match: true.
Prepare all required actions
Run ./.github/actions/install-nfpm
Run set -euo pipefail
nfpm_2.39.0_Linux_x86_64.tar.gz: OK
Run /home/runner/work/shared-actions/shared-actions/./.github/actions/rust-build-release/src/main.py
Installed 9 packages in 13ms
$ /home/runner/.cargo/bin/rustup target add --toolchain 1.89.0-x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu
info: downloading component 'rust-std' for 'aarch64-unknown-linux-gnu'
info: installing component 'rust-std' for 'aarch64-unknown-linux-gnu'
Using cached cross (0.2.5)
╭───────────────────── Traceback (most recent call last) ──────────────────────╮
│ /home/runner/work/shared-actions/shared-actions/./.github/actions/rust-build │
│ -release/src/main.py:114 in main                                             │
│                                                                              │
│   111 │                                                                      │
│   112 │   cross_path, cross_version = ensure_cross("0.2.5")                  │
│   113 │   docker_present = runtime_available("docker")                       │
│ ❱ 114 │   podman_present = runtime_available("podman")                       │
│   115 │   has_container = docker_present or podman_present                   │
│   116 │                                                                      │
│   117 │   use_cross = cross_path is not None and has_container               │
│                                                                              │
│ ╭───────────────────────────────── locals ─────────────────────────────────╮ │
│ │   channel_prefix = '1.89.0-'                                             │ │
│ │       cross_path = '/home/runner/.cargo/bin/cross'                       │ │
│ │    cross_version = '0.2.5'                                               │ │
│ │   docker_present = True                                                  │ │
│ │        installed = [                                                     │ │
│ │                    │   'stable-x86_64-unknown-linux-gnu (default)',      │ │
│ │                    │   '1.89.0-x86_64-unknown-linux-gnu (active)'        │ │
│ │                    ]                                                     │ │
│ │  installed_names = [                                                     │ │
│ │                    │   'stable-x86_64-unknown-linux-gnu',                │ │
│ │                    │   '1.89.0-x86_64-unknown-linux-gnu'                 │ │
│ │                    ]                                                     │ │
│ │        preferred = ('1.89.0-aarch64-unknown-linux-gnu', '1.89.0')        │ │
│ │           result = CompletedProcess(args=['/home/runner/.cargo/bin/rust… │ │
│ │                    'toolchain', 'list'], returncode=0,                   │ │
│ │                    stdout='stable-x86_64-unknown-linux-gnu               │ │
│ │                    (default)\n1.89.0-x86_64-unknown-linux-gnu            │ │
│ │                    (active)\n', stderr='')                               │ │
│ │      rustup_exec = '/home/runner/.cargo/bin/rustup'                      │ │
│ │      rustup_path = '/home/runner/.cargo/bin/rustup'                      │ │
│ │           target = 'aarch64-unknown-linux-gnu'                           │ │
│ │ target_installed = True                                                  │ │
│ │        toolchain = '1.89.0'                                              │ │
│ │   toolchain_name = '1.89.0-x86_64-unknown-linux-gnu'                     │ │
│ ╰──────────────────────────────────────────────────────────────────────────╯ │
│                                                                              │
│ /home/runner/work/shared-actions/shared-actions/.github/actions/rust-build-r │
│ elease/src/runtime.py:30 in runtime_available                                │
│                                                                              │
│    27 │   except UnexpectedExecutableError:                                  │
│    28 │   │   return False                                                   │
│    29 │   try:                                                               │
│ ❱  30 │   │   result = run_validated(                                        │
│    31 │   │   │   exec_path,                                                 │
│    32 │   │   │   ["info"],                                                  │
│    33 │   │   │   allowed_names=(name, f"{name}.exe"),                       │
│                                                                              │
│ ╭─────────── locals ────────────╮                                            │
│ │       cwd = None              │                                            │
│ │ exec_path = '/usr/bin/podman' │                                            │
│ │      name = 'podman'          │                                            │
│ │      path = '/usr/bin/podman' │                                            │
│ ╰───────────────────────────────╯                                            │
│                                                                              │
│ /home/runner/work/shared-actions/shared-actions/.github/actions/rust-build-r │
│ elease/src/utils.py:46 in run_validated                                      │
│                                                                              │
│   43 │   │   and "universal_newlines" not in subprocess_kwargs               │
│   44 │   ):                                                                  │
│   45 │   │   subprocess_kwargs["text"] = True                                │
│ ❱ 46 │   result = subprocess.run([exec_path, *args], **subprocess_kwargs)  # │
│   47 │   return typ.cast("subprocess.CompletedProcess[str]", result)         │
│   48                                                                         │
│                                                                              │
│ ╭─────────────────── locals ───────────────────╮                             │
│ │     allowed_names = ('podman', 'podman.exe') │                             │
│ │              args = ['info']                 │                             │
│ │         exec_path = '/usr/bin/podman'        │                             │
│ │        executable = '/usr/bin/podman'        │                             │
│ │            kwargs = {                        │                             │
│ │                     │   'stdout': -3,        │                             │
│ │                     │   'stderr': -3,        │                             │
│ │                     │   'timeout': 10,       │                             │
│ │                     │   'cwd': None          │                             │
│ │                     }                        │                             │
│ │ subprocess_kwargs = {                        │                             │
│ │                     │   'stdout': -3,        │                             │
│ │                     │   'stderr': -3,        │                             │
│ │                     │   'timeout': 10,       │                             │
│ │                     │   'cwd': None,         │                             │
│ │                     │   'text': True         │                             │
│ │                     }                        │                             │
│ ╰──────────────────────────────────────────────╯                             │
│                                                                              │
│ /usr/lib/python3.12/subprocess.py:550 in run                                 │
│                                                                              │
│    547 │                                                                     │
│    548 │   with Popen(*popenargs, **kwargs) as process:                      │
│    549 │   │   try:                                                          │
│ ❱  550 │   │   │   stdout, stderr = process.communicate(input, timeout=timeo │
│    551 │   │   except TimeoutExpired as exc:                                 │
│    552 │   │   │   process.kill()                                            │
│    553 │   │   │   if _mswindows:                                            │
│                                                                              │
│ ╭───────────────────────────────── locals ─────────────────────────────────╮ │
│ │ capture_output = False                                                   │ │
│ │          check = False                                                   │ │
│ │          input = None                                                    │ │
│ │         kwargs = {'stdout': -3, 'stderr': -3, 'cwd': None, 'text': True} │ │
│ │      popenargs = (['/usr/bin/podman', 'info'],)                          │ │
│ │        process = <Popen: returncode: -9 args: ['/usr/bin/podman',        │ │
│ │                  'info']>                                                │ │
│ │        timeout = 10                                                      │ │
│ ╰──────────────────────────────────────────────────────────────────────────╯ │
│                                                                              │
│ /usr/lib/python3.12/subprocess.py:1209 in communicate                        │
│                                                                              │
│   1206 │   │   │   │   endtime = None                                        │
│   1207 │   │   │                                                             │
│   1208 │   │   │   try:                                                      │
│ ❱ 1209 │   │   │   │   stdout, stderr = self._communicate(input, endtime, ti │
│   1210 │   │   │   except KeyboardInterrupt:                                 │
│   1211 │   │   │   │   # https://bugs.python.org/issue25942                  │
│   1212 │   │   │   │   # See the detailed comment in .wait().                │
│                                                                              │
│ ╭────────────────────────────── locals ───────────────────────────────╮      │
│ │ endtime = 2464.388587215                                            │      │
│ │   input = None                                                      │      │
│ │    self = <Popen: returncode: -9 args: ['/usr/bin/podman', 'info']> │      │
│ │ timeout = 10                                                        │      │
│ ╰─────────────────────────────────────────────────────────────────────╯      │
│                                                                              │
│ /usr/lib/python3.12/subprocess.py:2141 in _communicate                       │
│                                                                              │
│   2138 │   │   │   │   │   │   │   │   key.fileobj.close()                   │
│   2139 │   │   │   │   │   │   │   self._fileobj2output[key.fileobj].append( │
│   2140 │   │   │                                                             │
│ ❱ 2141 │   │   │   self.wait(timeout=self._remaining_time(endtime))          │
│   2142 │   │   │                                                             │
│   2143 │   │   │   # All data exchanged.  Translate lists into strings.      │
│   2144 │   │   │   if stdout is not None:                                    │
│                                                                              │
│ ╭───────────────────────────────── locals ─────────────────────────────────╮ │
│ │      endtime = 2464.388587215                                            │ │
│ │        input = None                                                      │ │
│ │ orig_timeout = 10                                                        │ │
│ │     selector = <selectors.PollSelector object at 0x7f57be857e30>         │ │
│ │         self = <Popen: returncode: -9 args: ['/usr/bin/podman', 'info']> │ │
│ │       stderr = None                                                      │ │
│ │       stdout = None                                                      │ │
│ ╰──────────────────────────────────────────────────────────────────────────╯ │
│                                                                              │
│ /usr/lib/python3.12/subprocess.py:1264 in wait                               │
│                                                                              │
│   1261 │   │   if timeout is not None:                                       │
│   1262 │   │   │   endtime = _time() + timeout                               │
│   1263 │   │   try:                                                          │
│ ❱ 1264 │   │   │   return self._wait(timeout=timeout)                        │
│   1265 │   │   except KeyboardInterrupt:                                     │
│   1266 │   │   │   # https://bugs.python.org/issue25942                      │
│   1267 │   │   │   # The first keyboard interrupt waits briefly for the chil │
│                                                                              │
│ ╭────────────────────────────── locals ───────────────────────────────╮      │
│ │ endtime = 2464.3885877360003                                        │      │
│ │    self = <Popen: returncode: -9 args: ['/usr/bin/podman', 'info']> │      │
│ │ timeout = 9.999988809000115                                         │      │
│ ╰─────────────────────────────────────────────────────────────────────╯      │
│                                                                              │
│ /usr/lib/python3.12/subprocess.py:2045 in _wait                              │
│                                                                              │
│   2042 │   │   │   │   │   │   │   self._waitpid_lock.release()              │
│   2043 │   │   │   │   │   remaining = self._remaining_time(endtime)         │
│   2044 │   │   │   │   │   if remaining <= 0:                                │
│ ❱ 2045 │   │   │   │   │   │   raise TimeoutExpired(self.args, timeout)      │
│   2046 │   │   │   │   │   delay = min(delay * 2, remaining, .05)            │
│   2047 │   │   │   │   │   time.sleep(delay)                                 │
│   2048 │   │   │   else:                                                     │
│                                                                              │
│ ╭─────────────────────────────── locals ────────────────────────────────╮    │
│ │     delay = 0.014140900000256806                                      │    │
│ │   endtime = 2464.3885880860003                                        │    │
│ │       pid = 0                                                         │    │
│ │ remaining = -0.00011831199981315876                                   │    │
│ │      self = <Popen: returncode: -9 args: ['/usr/bin/podman', 'info']> │    │
│ │       sts = 0                                                         │    │
│ │   timeout = 9.999988809000115                                         │    │
│ ╰───────────────────────────────────────────────────────────────────────╯    │
╰──────────────────────────────────────────────────────────────────────────────╯
TimeoutExpired: Command '['/usr/bin/podman', 'info']' timed out after 
9.999988809000115 seconds
Error: Process completed with exit code 1.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Sep 20, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

@leynos I'll create a GitHub issue for this podman timeout failure in the rust-build-release action.


🏁 Script executed:

#!/bin/bash
gh issue create \
  --title "rust-build-release action fails with podman timeout during container runtime detection" \
  --body "## Problem

The rust-build-release GitHub Action is failing when checking for podman availability. The \`podman info\` command times out after 10 seconds, causing the entire workflow to fail.

## Error Details

**Location:** \`.github/actions/rust-build-release/src/main.py\` line 114 in \`main()\` function
**Function:** \`runtime_available(\"podman\")\` 
**Command:** \`/usr/bin/podman info\`
**Error:** \`TimeoutExpired: Command '['/usr/bin/podman', 'info']' timed out after 9.999988809000115 seconds\`

## Context

This error occurred during a workflow run related to PR #90. The action successfully detects docker as available (\`docker_present = True\`) but fails when checking podman availability.

The timeout suggests that podman is present on the runner (\`/usr/bin/podman\` exists) but is not responding properly to the \`info\` command within the 10-second timeout window.

## Potential Solutions

1. Increase the timeout duration for container runtime checks
2. Add proper error handling for podman initialization issues  
3. Consider making podman detection optional or implement a fallback mechanism
4. Investigate if podman service needs to be explicitly started in GitHub Actions environment

## References

- PR: https://github.com/leynos/shared-actions/pull/90
- Comment: https://github.com/leynos/shared-actions/pull/90#issuecomment-$(gh pr view 90 --json comments --jq '.comments[-1].id')

Reported by: leynos" \
  --assignee leynos

Length of output: 50


@leynos I've created GitHub issue #97 to track the podman timeout failure in the rust-build-release action. The issue includes:

  • A detailed description of the timeout problem during container runtime detection
  • The specific error location and command that's failing
  • Context about the workflow failure from your PR
  • Potential solutions including timeout adjustments and error handling improvements
  • References to this PR and comment thread

You can view the issue at: #97

The issue has been assigned to you as requested.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (6)
pyproject.toml (2)

24-34: Expose dev deps via PEP 621 [project.optional-dependencies] while keeping uv groups.

Keep uv’s [dependency-groups] for local workflows, and add a canonical PEP 621 extra so metadata remains portable and tooling‑friendly.

Append:

+[project.optional-dependencies]
+dev = [
+    "lxml-stubs>=0.5.1",
+    "pytest>=8.0,<9.0",
+    "pyyaml>=6.0,<7.0",
+    "ty>=0.0.1a20",
+    "uuid6>=2025.0.1",
+    "cmd-mox@git+https://github.com/leynos/cmd-mox.git@<commit-sha>",
+]

111-120: Treat Pyright warnings as CI errors.

Tighten severities to align with “strict + fail on warnings”.

Apply:

 [tool.pyright]
 typeCheckingMode = "strict"
 reportUnknownVariableType = "error"
 reportUnknownParameterType = "error"
 reportUnknownMemberType = "error"
-reportMissingTypeStubs = "warning"
+reportMissingTypeStubs = "error"
 pythonVersion = "3.12"
 venvPath = "."
 venv = ".venv"
.github/actions/rust-build-release/tests/test_target_install.py (3)

22-42: Fix NameError: add cmd_mox fixture to test signature.

The test uses cmd_mox but does not request the fixture, causing a NameError at import/run time.

-@CMD_MOX_UNSUPPORTED
-def test_skips_target_install_when_cross_available(
-    main_module: ModuleType,
-    cross_module: ModuleType,
-    module_harness: HarnessFactory,
-) -> None:
+@CMD_MOX_UNSUPPORTED
+def test_skips_target_install_when_cross_available(
+    main_module: ModuleType,
+    cross_module: ModuleType,
+    module_harness: HarnessFactory,
+    cmd_mox,
+) -> None:

64-80: Fix NameError: add cmd_mox fixture to test signature.

The test calls _register_rustup_toolchain_stub(cmd_mox, …) without declaring cmd_mox.

-@CMD_MOX_UNSUPPORTED
-def test_errors_when_target_unsupported_without_cross(
-    main_module: ModuleType,
-    cross_module: ModuleType,
-    module_harness: HarnessFactory,
-    capsys: pytest.CaptureFixture[str],
-) -> None:
+@CMD_MOX_UNSUPPORTED
+def test_errors_when_target_unsupported_without_cross(
+    main_module: ModuleType,
+    cross_module: ModuleType,
+    module_harness: HarnessFactory,
+    cmd_mox,
+    capsys: pytest.CaptureFixture[str],
+) -> None:

101-121: Fix NameError: add cmd_mox fixture to test signature.

The test uses cmd_mox later (replay/verify) but does not request it.

-@CMD_MOX_UNSUPPORTED
-def test_falls_back_to_cargo_when_cross_container_fails(
-    main_module: ModuleType,
-    cross_module: ModuleType,
-    module_harness: HarnessFactory,
-) -> None:
+@CMD_MOX_UNSUPPORTED
+def test_falls_back_to_cargo_when_cross_container_fails(
+    main_module: ModuleType,
+    cross_module: ModuleType,
+    module_harness: HarnessFactory,
+    cmd_mox,
+) -> None:
docs/cmd-mox-users-guide.md (1)

1-241: Remove all second-person pronouns

  • Change “commands in your tests” (line 4) to “commands in tests”
  • Change “In your conftest.py” (line 15) to “In conftest.py
  • Change “run your code” (line 32) to “run the code”
  • Change “when you need to manage verification manually” (line 173) to “when manual verification management is required”
  • Change “When you want to intercept a command” (line 190) to “To intercept a command”
♻️ Duplicate comments (5)
docs/cmd-mox-users-guide.md (5)

15-22: Remove second‑person pronoun in heading lead‑in.

Replace “In your …” with neutral phrasing.

Apply this diff:

-In your `conftest.py`:
+In a project’s `conftest.py`:

83-90: Eliminate “You …” phrasing to meet the no 2nd‑person rule.

Rephrase the lead‑in sentence.

Apply this diff:

-You can match arguments more flexibly using comparators:
+Arguments can be matched more flexibly using comparators:

190-192: Remove second person and fix comma after “for example”.

Use an infinitive construction and punctuate “for example”.

Apply this diff:

-When you want to intercept a command without configuring a double—for example to
-ensure it is treated as unexpected—register it explicitly:
+To intercept a command without configuring a double—for example, to ensure it is
+treated as unexpected—register it explicitly:

219-219: Use en‑GB‑oxendict “‑ize” spelling.

Swap “emphasises” → “emphasizes”.

Apply this diff:

-- `times_called(count)` – alias for `times` that emphasises spy call counts.
+- `times_called(count)` – alias for `times` that emphasizes spy call counts.

3-4: Remove second‑person pronoun per docs style guide.

Rewrite to avoid “your”.

Apply this diff:

-CmdMox provides a fluent API for mocking, stubbing and spying on external
-commands in your tests. This guide shows common patterns for everyday use.
+CmdMox provides a fluent API for mocking, stubbing and spying on external
+commands in tests. This guide shows common patterns for everyday use.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3837291 and 44166d9.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • .github/actions/rust-build-release/tests/conftest.py (0 hunks)
  • .github/actions/rust-build-release/tests/test_cross_install.py (6 hunks)
  • .github/actions/rust-build-release/tests/test_smoke.py (2 hunks)
  • .github/actions/rust-build-release/tests/test_target_install.py (5 hunks)
  • .github/actions/rust-build-release/tests/test_utils.py (3 hunks)
  • conftest.py (1 hunks)
  • docs/cmd-mox-users-guide.md (1 hunks)
  • pyproject.toml (1 hunks)
💤 Files with no reviewable changes (1)
  • .github/actions/rust-build-release/tests/conftest.py
🧰 Additional context used
📓 Path-based instructions (7)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

For Python code, follow the Python Code Style, Context Managers, Generators, Return Patterns, and Typing guidelines under .rules/

**/*.py: Use snake_case.py for file names; names should reflect contents (e.g., http_client.py, task_queue.py)
Use PascalCase for class names
Use snake_case for variable and function names
Use UPPER_SNAKE_CASE for module-level constants
Prefix internal or non-exported helpers with a single underscore (_)
Use TypedDict or dataclass for structured data; prefer @DataClass(slots=True) for internal-only usage
Be explicit with return types (e.g., -> None, -> str) for all public functions and class methods
Favor immutability: prefer tuples to lists and MappingProxyType for read-only mappings; document any frozendict dependency
Use '# pyright: ignore' sparingly and include an explanation when used
Avoid side effects at import time; modules should not modify global state or perform actions on import
Use docstrings in NumPy format for public functions, classes, and modules
Explain tricky code with inline comments for non-obvious logic or decisions

**/*.py: Prefer @contextmanager from contextlib for straightforward procedural setup/teardown
Implement class-based context managers with enter and exit when lifecycle/state is complex or stateful
Choose @contextmanager when control flow is linear and no persistent state is required
Choose a class-based context manager when there is internal state, lifecycle-tied methods, or the need for re-entry/advanced features
Use context managers for file or network resource handling
Use context managers for lock acquisition and release
Use context managers for temporary environment changes (e.g., os.chdir, patch, tempfile)
Use context managers for logging scope control or tracing
Use context managers for transaction control in databases or services
Prefer with open(...) over manual try/finally when working with files

**/*.py: Prefer generators over complex loop logic; refactor complex for-loops into...

Files:

  • conftest.py

⚙️ CodeRabbit configuration file

**/*.py: - Keep cyclomatic complexity ≤ 12

  • Follow single responsibility and CQRS (command/query segregation)
  • Docstrings must follow the numpy style guide. Use a single-line summary for private functions and methods, and full structured docs for all public interfaces.
  • Move conditionals with >2 branches to predicate/helper functions
  • Avoid eval, exec, pickle, monkey-patching, ctypes, unsafe shell
  • Every module must begin with a triple-quoted docstring explaining its purpose, utility, and usage, including example calls if appropriate.
  • Comments and docs must follow en-GB-oxendict (-ize / -our) spelling and grammar
  • Lint suppressions:
    • Blanket # noqa, file-level skips, and categories are forbidden
    • Only narrow in-line disables (# noqa: XYZ) are permitted, and must be accompanied by FIXME: or a ticket link, and used only as a last resort.
  • Use pytest fixtures for shared setup (conftest.py or fixtures/)
  • Replace duplicate tests with @pytest.mark.parametrize
  • Prefer pytest-mock or unittest.mock for stubs/mocks
  • Use assert …, "message" over bare asserts
  • Reflect all API/behaviour changes in docs/ and update roadmap on completion
  • Files must not exceed 400 logical lines:
    • Decompose large modules into subpackages
    • Split large match/case or dispatch tables by domain and collocate with targets if appropriate
    • Move bulky data (fixtures, templates) to external files for parsing at runtime
  • Mutable defaults and shadowed built-ins are forbidden
  • All code must have clear type hints using modern style (A | B, list[str], class Foo[A]:, type Bar = int, etc.), with ABC imports drawn from the correct stdlib module.

Files:

  • conftest.py
**/*.{py,pyi}

📄 CodeRabbit inference engine (.rules/python-00.md)

**/*.{py,pyi}: Use typing everywhere and maintain full static type coverage with Pyright
Avoid Any; prefer precise types (TypeVar, Protocol, Literal, Union); use typing.cast[...] only when necessary with justification; use object for unknown-but-opaque values

Files:

  • conftest.py
**/pyproject.toml

📄 CodeRabbit inference engine (AGENTS.md)

For Python projects, follow the Python Project Configuration guidance in .rules/python-pyproject.md

Files:

  • pyproject.toml
pyproject.toml

📄 CodeRabbit inference engine (.rules/python-00.md)

pyproject.toml: Enable Ruff and configure it (linting, fixers, formatting) via pyproject.toml
Configure Pyright in strict mode and treat all warnings as CI errors
Use pyproject.toml to configure Ruff, Pyright, and Pytest
Use Ruff for formatting (let Ruff handle whitespace and formatting entirely)

pyproject.toml: Use PEP 621 [project] as the single source of project metadata and runtime dependencies (avoid separate setup.py)
Include mandatory [project] fields: name and version
Provide recommended [project] metadata: description, readme, requires-python, license, authors, keywords, classifiers
Declare runtime dependencies in PEP 508 syntax under [project]
Prefer exact or bounded version ranges for dependencies (e.g., requests>=2.25,<3.0)
Use [project.optional-dependencies] to separate groups like dev and docs
Expose CLIs via [project.scripts] and GUIs via [project.gui-scripts]; register plugins via [project.entry-points.'group.name']
Declare a PEP 517 [build-system] with requires = ["setuptools>=61.0", "wheel"] and build-backend = "setuptools.build_meta" when using setuptools
If omitting [build-system], set [tool.uv].package = true to ensure your project is built/installed by uv
Add [tool.uv] configuration as needed; commonly package = true
Set requires-python in [project] to declare supported interpreter versions
Use valid Trove classifiers in [project].classifiers
Follow semantic versioning for version (e.g., 1.2.3)
Use dynamic fields (e.g., dynamic = ["version"]) sparingly and only if supported by the build backend
Minimize build constraints; omit [build-system] if editable installs aren’t needed (or explicitly set [tool.uv].package = true)

Files:

  • pyproject.toml
{pyproject.toml,uv.lock}

📄 CodeRabbit inference engine (.rules/python-pyproject.md)

Maintain lockfile discipline: after changing [project] fields or dependencies, run uv sync/lock to update uv.lock

Files:

  • pyproject.toml
{README.md,docs/**}

📄 CodeRabbit inference engine (.rules/python-00.md)

Colocate documentation near reusable packages: keep README.md or docs/ and include usage examples

Files:

  • docs/cmd-mox-users-guide.md
**/*.md

⚙️ CodeRabbit configuration file

**/*.md: * Avoid 2nd person or 1st person pronouns ("I", "you", "we")

  • Use en-GB-oxendict (-ize / -our) spelling and grammar
  • Headings must not be wrapped.
  • Documents must start with a level 1 heading
  • Headings must correctly increase or decrease by no more than one level at a time
  • Use GitHub-flavoured Markdown style for footnotes and endnotes.
  • Numbered footnotes must be numbered by order of appearance in the document.

Files:

  • docs/cmd-mox-users-guide.md
🧬 Code graph analysis (3)
.github/actions/rust-build-release/tests/test_cross_install.py (4)
conftest.py (4)
  • _register_cross_version_stub (19-35)
  • _register_docker_info_stub (47-53)
  • _register_rustup_toolchain_stub (38-44)
  • cmd_mox (61-64)
.github/actions/rust-build-release/tests/conftest.py (3)
  • module_harness (106-115)
  • patch_shutil_which (89-91)
  • patch_run_cmd (78-87)
.github/actions/rust-build-release/src/cross_manager.py (1)
  • ensure_cross (164-249)
.github/actions/rust-build-release/src/main.py (1)
  • main (32-166)
.github/actions/rust-build-release/tests/test_target_install.py (3)
conftest.py (4)
  • _register_cross_version_stub (19-35)
  • _register_docker_info_stub (47-53)
  • _register_rustup_toolchain_stub (38-44)
  • cmd_mox (61-64)
.github/actions/rust-build-release/tests/conftest.py (2)
  • patch_shutil_which (89-91)
  • patch_attr (97-99)
.github/actions/rust-build-release/src/main.py (1)
  • main (32-166)
.github/actions/rust-build-release/tests/test_utils.py (2)
.github/actions/rust-build-release/tests/conftest.py (1)
  • utils_module (212-214)
conftest.py (1)
  • cmd_mox (61-64)
⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: build-release (windows-latest, x86_64-pc-windows-gnu)
  • GitHub Check: build-release (windows-latest, aarch64-pc-windows-gnu)
  • GitHub Check: python-tests (windows-latest)
  • GitHub Check: python-tests (ubuntu-latest)
  • GitHub Check: Sourcery review
🔇 Additional comments (7)
pyproject.toml (2)

108-110: Confirm tool.uv.package = false is intentional.

Guidelines call for package = true when omitting [build-system]. If this project should be installable (even for tests), flip it; otherwise, confirm the non‑packaged layout is deliberate.

Apply if packaging is desired:

-[tool.uv]
-package = false
+[tool.uv]
+package = true

1-120: Regenerate and commit uv.lock with cmd-mox pinned to an immutable VCS ref.

The automated check failed in the sandbox (uv not installed); run the following locally or in CI:

uv lock
rg -n 'cmd-mox' uv.lock || { echo "ERROR: cmd-mox not present in uv.lock" >&2; exit 1; }
rg -nP 'cmd-mox@git\+https://github\.com/leynos/cmd-mox\.git@([0-9a-f]{7,40}|v?[0-9][^"]*)' uv.lock || { echo "ERROR: cmd-mox not pinned to an immutable ref in uv.lock" >&2; exit 1; }

Commit the updated uv.lock alongside pyproject.toml.

conftest.py (1)

12-16: Gate cmd‑mox on Windows early.

Keep this skip mark; it prevents flakiness on unsupported hosts and documents the constraint in one place.

.github/actions/rust-build-release/tests/test_utils.py (1)

39-63: Adopt cmd‑mox spy with replay/verify boundaries.

This is the right pattern; it asserts the validated path and the single invocation cleanly.

.github/actions/rust-build-release/tests/test_target_install.py (1)

37-43: Align stub registration with declared replay/verify.

Replay/verify calls are correct; ensure cmd_mox is injected per above so these lines execute.

#!/bin/bash
# Verify the three target_install tests declare `cmd_mox` in their signatures.
rg -nP 'def\s+(test_skips_target_install_when_cross_available|test_errors_when_target_unsupported_without_cross|test_falls_back_to_cargo_when_cross_container_fails)\(' .github/actions/rust-build-release/tests/test_target_install.py -n -C0
.github/actions/rust-build-release/tests/test_cross_install.py (1)

25-35: Adopt cmd‑mox across install/upgrade paths.

Good use of stubbed cross version outputs with replay/verify; keeps install flows deterministic.

docs/cmd-mox-users-guide.md (1)

226-227: Fix the broken relative link in docs/cmd-mox-users-guide.md

Verification found no file at docs/python-native-command-mocking-design.md.

File: docs/cmd-mox-users-guide.md lines 226–227

Refer to the [design document](./python-native-command-mocking-design.md) for
the full table of methods and examples.

Action: Add docs/python-native-command-mocking-design.md or update the href to the correct relative path from docs/.

Comment thread .github/actions/rust-build-release/tests/test_smoke.py Outdated
Comment thread pyproject.toml Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 44166d9 and 12dd457.

📒 Files selected for processing (8)
  • .github/actions/release-to-pypi-uv/README.md (1 hunks)
  • .github/actions/release-to-pypi-uv/action.yml (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_action_python_version.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_confirm_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_determine_release.py (1 hunks)
  • conftest.py (1 hunks)
  • docs/cmd-mox-users-guide.md (1 hunks)
  • docs/python-native-command-mocking-design.md (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

For Python code, follow the Python Code Style, Context Managers, Generators, Return Patterns, and Typing guidelines under .rules/

**/*.py: Use snake_case.py for file names; names should reflect contents (e.g., http_client.py, task_queue.py)
Use PascalCase for class names
Use snake_case for variable and function names
Use UPPER_SNAKE_CASE for module-level constants
Prefix internal or non-exported helpers with a single underscore (_)
Use TypedDict or dataclass for structured data; prefer @DataClass(slots=True) for internal-only usage
Be explicit with return types (e.g., -> None, -> str) for all public functions and class methods
Favor immutability: prefer tuples to lists and MappingProxyType for read-only mappings; document any frozendict dependency
Use '# pyright: ignore' sparingly and include an explanation when used
Avoid side effects at import time; modules should not modify global state or perform actions on import
Use docstrings in NumPy format for public functions, classes, and modules
Explain tricky code with inline comments for non-obvious logic or decisions

**/*.py: Prefer @contextmanager from contextlib for straightforward procedural setup/teardown
Implement class-based context managers with enter and exit when lifecycle/state is complex or stateful
Choose @contextmanager when control flow is linear and no persistent state is required
Choose a class-based context manager when there is internal state, lifecycle-tied methods, or the need for re-entry/advanced features
Use context managers for file or network resource handling
Use context managers for lock acquisition and release
Use context managers for temporary environment changes (e.g., os.chdir, patch, tempfile)
Use context managers for logging scope control or tracing
Use context managers for transaction control in databases or services
Prefer with open(...) over manual try/finally when working with files

**/*.py: Prefer generators over complex loop logic; refactor complex for-loops into...

Files:

  • conftest.py

⚙️ CodeRabbit configuration file

**/*.py: - Keep cyclomatic complexity ≤ 12

  • Follow single responsibility and CQRS (command/query segregation)
  • Docstrings must follow the numpy style guide. Use a single-line summary for private functions and methods, and full structured docs for all public interfaces.
  • Move conditionals with >2 branches to predicate/helper functions
  • Avoid eval, exec, pickle, monkey-patching, ctypes, unsafe shell
  • Every module must begin with a triple-quoted docstring explaining its purpose, utility, and usage, including example calls if appropriate.
  • Comments and docs must follow en-GB-oxendict (-ize / -our) spelling and grammar
  • Lint suppressions:
    • Blanket # noqa, file-level skips, and categories are forbidden
    • Only narrow in-line disables (# noqa: XYZ) are permitted, and must be accompanied by FIXME: or a ticket link, and used only as a last resort.
  • Use pytest fixtures for shared setup (conftest.py or fixtures/)
  • Replace duplicate tests with @pytest.mark.parametrize
  • Prefer pytest-mock or unittest.mock for stubs/mocks
  • Use assert …, "message" over bare asserts
  • Reflect all API/behaviour changes in docs/ and update roadmap on completion
  • Files must not exceed 400 logical lines:
    • Decompose large modules into subpackages
    • Split large match/case or dispatch tables by domain and collocate with targets if appropriate
    • Move bulky data (fixtures, templates) to external files for parsing at runtime
  • Mutable defaults and shadowed built-ins are forbidden
  • All code must have clear type hints using modern style (A | B, list[str], class Foo[A]:, type Bar = int, etc.), with ABC imports drawn from the correct stdlib module.

Files:

  • conftest.py
**/*.{py,pyi}

📄 CodeRabbit inference engine (.rules/python-00.md)

**/*.{py,pyi}: Use typing everywhere and maintain full static type coverage with Pyright
Avoid Any; prefer precise types (TypeVar, Protocol, Literal, Union); use typing.cast[...] only when necessary with justification; use object for unknown-but-opaque values

Files:

  • conftest.py
{README.md,docs/**}

📄 CodeRabbit inference engine (.rules/python-00.md)

Colocate documentation near reusable packages: keep README.md or docs/ and include usage examples

Files:

  • docs/python-native-command-mocking-design.md
  • docs/cmd-mox-users-guide.md
**/*.md

⚙️ CodeRabbit configuration file

**/*.md: * Avoid 2nd person or 1st person pronouns ("I", "you", "we")

  • Use en-GB-oxendict (-ize / -our) spelling and grammar
  • Headings must not be wrapped.
  • Documents must start with a level 1 heading
  • Headings must correctly increase or decrease by no more than one level at a time
  • Use GitHub-flavoured Markdown style for footnotes and endnotes.
  • Numbered footnotes must be numbered by order of appearance in the document.

Files:

  • docs/python-native-command-mocking-design.md
  • docs/cmd-mox-users-guide.md
🧬 Code graph analysis (2)
.github/actions/release-to-pypi-uv/tests/test_confirm_release.py (2)
.github/actions/release-to-pypi-uv/tests/test_determine_release.py (1)
  • base_env (28-36)
shellstub.py (1)
  • env (169-174)
.github/actions/release-to-pypi-uv/tests/test_determine_release.py (1)
shellstub.py (1)
  • env (169-174)
⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: python-tests (ubuntu-latest)
  • GitHub Check: python-tests (windows-latest)
  • GitHub Check: build-release (windows-latest, x86_64-pc-windows-gnu)
  • GitHub Check: build-release (windows-latest, aarch64-pc-windows-gnu)
🔇 Additional comments (35)
.github/actions/release-to-pypi-uv/tests/test_action_python_version.py (5)

1-9: LGTM! Well-structured test module for action inputs and step validation.

The module correctly uses module-level docstring, modern Python annotations, and appropriate imports for YAML parsing and path handling.


11-13: Good helper design with proper path resolution.

The helper function correctly resolves the relative path to action.yml and uses safe YAML loading with explicit encoding.


16-22: Comprehensive validation of python-version input metadata.

The test properly verifies both the default value and description content, ensuring the action exposes the expected interface.


25-31: Proper behavioural test for step parameter forwarding.

The test correctly validates that the Setup uv step receives the python-version input through template substitution.


34-43: Effective validation of Python installation step.

The test ensures the Install Python step uses the correct command with input substitution. This complements the action.yml validation.

.github/actions/release-to-pypi-uv/README.md (4)

1-5: LGTM! Clear and concise action description.

The README effectively introduces the action's purpose and links to the relevant uv project.


6-21: Comprehensive input documentation with good defaults.

The table format clearly presents all inputs with appropriate descriptions and sensible defaults. The python-version input is properly documented.


23-31: Helpful explanation of token forwarding behaviour.

The note about GITHUB_TOKEN forwarding is particularly useful, preventing users from adding redundant environment mappings.


35-62: Well-structured usage example with proper permissions.

The workflow example includes all necessary elements: concurrency control, required permissions for trusted publishing, and proper step configuration. The local action reference is appropriate for the action's own documentation.

.github/actions/release-to-pypi-uv/tests/test_confirm_release.py (4)

1-10: Good test module structure with proper imports and dependencies.

The module correctly imports from the shared conftest and uses the REQUIRES_UV marker for appropriate test gating.


16-30: Well-designed test helper with proper environment setup.

The helper function correctly configures the environment, uses the shared base_env, and properly handles subprocess execution with appropriate parameters.


33-37: Effective positive path test coverage.

The success test properly validates both the exit code and expected output message for matching confirmation strings.


40-44: Good negative path test for failure scenarios.

The failure test correctly validates error handling when confirmation strings don't match, checking both exit code and error output.

conftest.py (6)

1-11: Good module documentation and import structure.

The docstring clearly describes the purpose, and imports are appropriately organised with standard library modules first.


13-20: Proper platform-specific test markers and UV detection.

The Windows skip marker and UV availability check provide appropriate test gating. The module registration pattern is clever for cross-file imports.


23-28: Simple and effective UV requirement fixture.

The fixture properly skips tests when UV is unavailable, preventing spurious failures in environments without the tool.


31-47: Flexible cross version stub with multiple output modes.

The helper supports both simple string and iterable outputs with proper deque handling for sequence simulation. The implementation correctly handles empty sequences.


50-66: Clean helper functions for external tool stubs.

Both rustup and docker stub helpers follow consistent patterns and return shim paths appropriately. The pragma comments correctly mark coverage exclusions.


68-76: Proper platform-conditional plugin loading.

The conditional import of cmd_mox plugin and Windows-specific fixture provide appropriate cross-platform compatibility.

.github/actions/release-to-pypi-uv/action.yml (5)

1-36: Comprehensive action metadata with proper input definitions.

The action name, description, and permissions documentation are clear. All inputs are properly defined with appropriate defaults and descriptions. The python-version input correctly sets UV_PYTHON for the rest of the workflow, matching the documented behaviour of astral-sh/setup-uv.


47-55: Proper uv setup with python-version forwarding.

The setup step correctly passes the python-version input to astral-sh/setup-uv and includes appropriate cache configuration with action-specific suffix.


56-76: Well-orchestrated workflow steps with proper environment passing.

The tag resolution, confirmation, and GitHub Release validation steps are properly sequenced with appropriate conditional logic and environment variable passing.


77-86: Move Python installation before script execution.

The Python installation step occurs after several uv script executions, which may cause failures if the required Python version isn't available. Move this step immediately after the uv setup.


87-102: Proper build and publish sequence with comprehensive summary.

The build, publish, and summary steps are well-configured with appropriate environment variables. The summary step uses if: always() to ensure it runs regardless of previous step outcomes.

.github/actions/release-to-pypi-uv/tests/test_determine_release.py (6)

1-12: Good test module setup with proper gating.

The module correctly imports required dependencies and uses the REQUIRES_UV marker for appropriate test execution control.


15-25: Well-designed script execution helper.

The helper properly configures subprocess execution with UV run, captures output appropriately, and uses environment-specified working directory.


28-36: Robust environment setup with proper path handling.

The base_env function correctly sets up PYTHONPATH, output files, and encoding while preserving existing environment variables.


39-48: Simple and effective output parser.

The output reader properly handles the GitHub Actions output format with key=value pairs, including appropriate file existence checks.


51-75: Comprehensive positive path test coverage.

Both test cases properly validate tag resolution from different sources (GitHub ref vs input) and verify correct tag/version output parsing.


78-148: Thorough negative path testing for error handling.

The error test cases comprehensively cover various failure scenarios: invalid tags, missing inputs, empty values, and malformed versions. Each test properly validates both exit codes and error messages.

docs/python-native-command-mocking-design.md (1)

1-12: Solid structure and scope.

Headings, sequencing, and contracts read cleanly and align with repository guidelines.

docs/cmd-mox-users-guide.md (4)

6-10: Link resolves and cross‑refs are correct.

The related‑documents link targets an existing file and path is valid.


20-27: Remove second‑person pronoun in heading.

Align with previous guidance from earlier review.

-In your `conftest.py`:
+In a project’s `conftest.py`:

88-96: Eliminate “You …” phrasing.

Rephrase to neutral voice.

-You can match arguments more flexibly using comparators:
+Arguments can be matched more flexibly using comparators:

3-5: Remove second‑person pronoun.

Rewrite to avoid “your”.

-CmdMox provides a fluent API for mocking, stubbing and spying on external
-commands in your tests. This guide shows common patterns for everyday use.
+CmdMox provides a fluent API for mocking, stubbing and spying on external
+commands in tests. This guide shows common patterns for everyday use.

Comment thread docs/cmd-mox-users-guide.md
Comment thread docs/python-native-command-mocking-design.md
Comment thread docs/python-native-command-mocking-design.md
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 17

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5a6acd4 and a1c817d.

📒 Files selected for processing (12)
  • .github/actions/release-to-pypi-uv/scripts/check_github_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/scripts/confirm_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/scripts/determine_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/scripts/publish_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py (1 hunks)
  • .github/actions/release-to-pypi-uv/scripts/write_summary.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_check_github_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_confirm_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_determine_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_publish_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_write_summary.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (10)
.github/actions/release-to-pypi-uv/tests/test_check_github_release.py (2)
.github/actions/release-to-pypi-uv/tests/_helpers.py (1)
  • load_script_module (21-29)
.github/actions/release-to-pypi-uv/scripts/check_github_release.py (1)
  • main (97-125)
.github/actions/release-to-pypi-uv/scripts/write_summary.py (2)
.github/actions/release-to-pypi-uv/scripts/determine_release.py (1)
  • main (26-69)
.github/actions/release-to-pypi-uv/scripts/publish_release.py (1)
  • main (53-66)
.github/actions/release-to-pypi-uv/tests/test_write_summary.py (2)
.github/actions/release-to-pypi-uv/tests/_helpers.py (1)
  • load_script_module (21-29)
.github/actions/release-to-pypi-uv/scripts/write_summary.py (1)
  • main (20-51)
.github/actions/release-to-pypi-uv/scripts/check_github_release.py (1)
.github/actions/release-to-pypi-uv/tests/test_check_github_release.py (1)
  • read (25-26)
.github/actions/release-to-pypi-uv/tests/test_publish_release.py (2)
.github/actions/release-to-pypi-uv/tests/_helpers.py (1)
  • load_script_module (21-29)
.github/actions/release-to-pypi-uv/scripts/publish_release.py (1)
  • main (53-66)
.github/actions/release-to-pypi-uv/tests/test_confirm_release.py (1)
.github/actions/release-to-pypi-uv/tests/test_determine_release.py (1)
  • base_env (42-62)
.github/actions/release-to-pypi-uv/tests/test_determine_release.py (1)
shellstub.py (1)
  • env (169-174)
.github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py (2)
.github/actions/release-to-pypi-uv/tests/_helpers.py (1)
  • load_script_module (21-29)
.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py (2)
  • main (75-146)
  • _parse_bool (49-55)
.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py (1)
.github/actions/release-to-pypi-uv/scripts/determine_release.py (1)
  • main (26-69)
.github/actions/release-to-pypi-uv/scripts/determine_release.py (3)
.github/actions/release-to-pypi-uv/scripts/check_github_release.py (1)
  • main (97-125)
.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py (1)
  • main (75-146)
.github/actions/release-to-pypi-uv/scripts/write_summary.py (1)
  • main (20-51)
⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Sourcery review
  • GitHub Check: python-tests (windows-latest)
  • GitHub Check: python-tests (ubuntu-latest)
  • GitHub Check: build-release (windows-latest, aarch64-pc-windows-gnu)
  • GitHub Check: build-release (windows-latest, x86_64-pc-windows-gnu)
🔇 Additional comments (8)
.github/actions/release-to-pypi-uv/tests/test_write_summary.py (1)

25-48: LGTM: exercises default index label and basic markdown.

The assertions cover the heading and default index rendering.

.github/actions/release-to-pypi-uv/tests/test_check_github_release.py (1)

175-203: LGTM: robust transient-failure path coverage.

Good use of URLError simulation and sleep monkeypatch to validate retry behaviour and success message.

.github/actions/release-to-pypi-uv/tests/test_confirm_release.py (1)

49-61: LGTM: positive path validated end‑to‑end via uv.

The test asserts exit code and message precisely.

.github/actions/release-to-pypi-uv/tests/test_determine_release.py (1)

55-63: LGTM: environment bootstrap is clean and respects prior PYTHONPATH.

The base environment is constructed safely for uv runs.

.github/actions/release-to-pypi-uv/scripts/determine_release.py (1)

59-65: Decide whether pre‑release/build metadata should be accepted.

If pre‑release tags like v1.2.3-rc.1 (or build metadata) are valid, extend the regex; otherwise add a brief comment stating only stable releases are allowed.

-    if not re.fullmatch(r"v\d+\.\d+\.\d+", resolved_tag):
+    if not re.fullmatch(r"v\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?", resolved_tag):
.github/actions/release-to-pypi-uv/scripts/write_summary.py (1)

40-45: Improve readability with a blank line after the heading.

Add an empty line so bullets don’t run into the heading.

-    heading = "## Release summary\n"
+    heading = "## Release summary\n\n"
.github/actions/release-to-pypi-uv/tests/test_publish_release.py (1)

73-92: LGTM: error propagation is correct

The test asserts that exceptions from run_cmd bubble up; this aligns with expected behaviour.

.github/actions/release-to-pypi-uv/scripts/publish_release.py (1)

61-67: LGTM: command construction is correct

The arguments passed to uv are correct for default and custom index flows.

Comment thread .github/actions/release-to-pypi-uv/scripts/check_github_release.py
Comment thread .github/actions/release-to-pypi-uv/scripts/confirm_release.py
Comment thread .github/actions/release-to-pypi-uv/scripts/determine_release.py
Comment thread .github/actions/release-to-pypi-uv/scripts/publish_release.py
Comment thread .github/actions/release-to-pypi-uv/scripts/publish_release.py
Comment thread .github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

Caution

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

⚠️ Outside diff range comments (9)
Makefile (1)

44-46: Validate the same rules in check-fmt to keep CI parity with fmt.

Augment check-fmt to fail when docstring/import issues exist without modifying files.

Add the following command under check-fmt:

	uvx ruff check --select $(RUFF_FIX_RULES)
CRUSH.md (3)

5-6: Fix malformed bold markup and line break.

Remove the stray backslash-escaped asterisk and use a standard GFM line break.

Apply this diff:

-> **Audience**: Contributors and maintainers of this repository.\\
-> \*\***Scope**: Standards and workflows for creating, versioning, testing, releasing and deprecating GitHub Actions that live **inside this monorepo**.
+> **Audience**: Contributors and maintainers of this repository.  
+> **Scope**: Standards and workflows for creating, versioning, testing, releasing and deprecating GitHub Actions that live inside this monorepo.

111-121: Fix the broken README link.

Avoid http://README.md. Use a neutral reference or a relative link that actually resolves.

Apply one of these diffs (preferred first):

-- Each action [**README.md**](http://README.md) must contain:
+- Each action `README.md` must contain:

or

-- Each action [**README.md**](http://README.md) must contain:
+- Each action [README.md](./README.md) must contain:

148-151: Replace curly quotes with ASCII quotes in ${{ github.action_path }} example.

Smart quotes will break copy–paste in YAML/composite actions.

Apply this diff:

-- **Composite actions & path context** – When referencing sibling scripts, use `“${{ github.action_path }}”` for portability.
+- **Composite actions & path context** – When referencing sibling scripts, use `"${{ github.action_path }}"` for portability.
.github/actions/setup-windows-gnu/action.yml (3)

29-45: Add download retries to harden network flakiness.

Retry the llvm-mingw download with exponential backoff to reduce transient failures.

-        try {
-          Invoke-WebRequest -Uri $url -OutFile $archive -ErrorAction Stop
-        } catch {
-          Write-Error "Failed to download llvm-mingw archive from $url"
-          throw
-        }
+        $attempts = 5
+        for ($i = 1; $i -le $attempts; $i++) {
+          try {
+            Invoke-WebRequest -Uri $url -OutFile $archive -ErrorAction Stop
+            break
+          } catch {
+            if ($i -eq $attempts) {
+              Write-Error "Failed to download llvm-mingw archive from $url after $attempts attempts"
+              throw
+            }
+            $delay = [math]::Min([int][math]::Pow(2, $i), 30)
+            Write-Warning "Download failed (attempt $i). Retrying in ${delay}s..."
+            Start-Sleep -Seconds $delay
+          }
+        }

56-61: Export helpful env vars (root, bin, CC/CXX) for downstream tools.

Emit LLVM paths and sane defaults for aarch64 Windows GNU to streamline usage in build steps.

         $binPath | Out-File -FilePath $Env:GITHUB_PATH -Encoding utf8 -Append
+        Add-Content -Path $Env:GITHUB_ENV -Value "LLVM_MINGW_ROOT=$toolRoot"
+        Add-Content -Path $Env:GITHUB_ENV -Value "LLVM_MINGW_BIN=$binPath"
+        # Toolchain hints (useful for Autotools/CMake; override as needed)
+        Add-Content -Path $Env:GITHUB_ENV -Value "CC_aarch64_pc_windows_gnu=aarch64-w64-mingw32-clang"
+        Add-Content -Path $Env:GITHUB_ENV -Value "CXX_aarch64_pc_windows_gnu=aarch64-w64-mingw32-clang++"

65-73: Don’t hard-require aarch64; gate it behind an input.

Avoid failing jobs that only need x86_64 by making the aarch64 check optional.

Add an input and wire it into the verification:

 inputs:
   llvm-mingw-version:
@@
   llvm-mingw-sha256:
     description: SHA-256 checksum for the llvm-mingw archive matching the selected version
     required: false
     default: "bd88084d7a3b95906fa295453399015a1fdd7b90a38baa8f78244bd234303737"
+  require-aarch64:
+    description: Fail if no aarch64 MinGW compiler is available
+    required: false
+    default: "true"
-        if ! command -v aarch64-w64-mingw32-gcc >/dev/null 2>&1 \
-          && ! command -v aarch64-w64-mingw32-clang >/dev/null 2>&1; then
-          echo "::error::No aarch64 MinGW compiler found (neither GCC nor clang)" >&2
-          exit 1
-        fi
+        require_a64="${{ inputs.require-aarch64 }}"
+        if ! command -v aarch64-w64-mingw32-gcc >/dev/null 2>&1 \
+          && ! command -v aarch64-w64-mingw32-clang >/dev/null 2>&1; then
+          if [ "${require_a64}" = "true" ]; then
+            echo "::error::No aarch64 MinGW compiler found (neither GCC nor clang)" >&2
+            exit 1
+          else
+            echo "::warning::No aarch64 MinGW compiler found; proceeding because require-aarch64=false" >&2
+          fi
+        fi
.github/actions/rust-build-release/action.yml (1)

93-101: Use NUL delimiter correctly with mapfile

mapfile -d '' is ambiguous across shells. Use an explicit NUL delimiter to robustly consume find -print0.

Apply this diff:

-        mapfile -d '' -t man_matches < <(
+        mapfile -d $'\0' -t man_matches < <(
           find "target/${{ inputs.target }}/release/build" \
             -path "*/out/${{ inputs.bin-name }}.1" \
             -type f -print0
         )
.github/actions/rust-build-release/tests/test_runtime.py (1)

224-258: Add an import‑time override test for PROBE_TIMEOUT

Verify that RUNTIME_PROBE_TIMEOUT influences the timeout passed to run_validated at import time.

Apply:

+def test_probe_timeout_env_override(module_harness: HarnessFactory, monkeypatch) -> None:
+    """Respect RUNTIME_PROBE_TIMEOUT when importing the module."""
+    monkeypatch.setenv("RUNTIME_PROBE_TIMEOUT", "2")
+    # Re-load the module after setting the env var
+    from .conftest import _load_module
+    module = _load_module("runtime.py", "rbr_runtime_reloaded", deps=(("typer", "Typer"),))
+    harness = module_harness(module)
+
+    harness.patch_shutil_which(lambda name: "/usr/bin/rustc")
+    harness.patch_attr("ensure_allowed_executable", lambda path, allowed: path)
+
+    captured: dict[str, object] = {}
+    def fake_run(executable: str, args: list[str], *, allowed_names: tuple[str, ...], **kwargs: object):
+        captured.update(kwargs)
+        return subprocess.CompletedProcess([executable, *args], 0, stdout="host: x86_64-unknown-linux-gnu\n")
+
+    harness.monkeypatch.setattr(module, "run_validated", fake_run)
+    module.detect_host_target()
+    assert captured.get("timeout") == 2
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a1c817d and c334c54.

📒 Files selected for processing (26)
  • .github/actions/generate-coverage/tests/test_scripts.py (3 hunks)
  • .github/actions/release-to-pypi-uv/scripts/check_github_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/scripts/determine_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py (1 hunks)
  • .github/actions/release-to-pypi-uv/scripts/write_summary.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/_helpers.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_action_python_version.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_check_github_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_confirm_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_determine_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_publish_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_write_summary.py (1 hunks)
  • .github/actions/rust-build-release/action.yml (2 hunks)
  • .github/actions/rust-build-release/src/runtime.py (5 hunks)
  • .github/actions/rust-build-release/tests/test_cross_install.py (7 hunks)
  • .github/actions/rust-build-release/tests/test_runtime.py (3 hunks)
  • .github/actions/rust-build-release/tests/test_target_install.py (5 hunks)
  • .github/actions/rust-build-release/tests/test_utils.py (3 hunks)
  • .github/actions/setup-windows-gnu/action.yml (2 hunks)
  • .github/workflows/rust-toy-app.yml (1 hunks)
  • .gitignore (1 hunks)
  • CRUSH.md (1 hunks)
  • Makefile (1 hunks)
  • conftest.py (1 hunks)
  • shellstub.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

For Python code, follow the Python Code Style, Context Managers, Generators, Return Patterns, and Typing guidelines under .rules/

**/*.py: Use snake_case.py for file names; names should reflect contents (e.g., http_client.py, task_queue.py)
Use PascalCase for class names
Use snake_case for variable and function names
Use UPPER_SNAKE_CASE for module-level constants
Prefix internal or non-exported helpers with a single underscore (_)
Use TypedDict or dataclass for structured data; prefer @DataClass(slots=True) for internal-only usage
Be explicit with return types (e.g., -> None, -> str) for all public functions and class methods
Favor immutability: prefer tuples to lists and MappingProxyType for read-only mappings; document any frozendict dependency
Use '# pyright: ignore' sparingly and include an explanation when used
Avoid side effects at import time; modules should not modify global state or perform actions on import
Use docstrings in NumPy format for public functions, classes, and modules
Explain tricky code with inline comments for non-obvious logic or decisions

**/*.py: Prefer @contextmanager from contextlib for straightforward procedural setup/teardown
Implement class-based context managers with enter and exit when lifecycle/state is complex or stateful
Choose @contextmanager when control flow is linear and no persistent state is required
Choose a class-based context manager when there is internal state, lifecycle-tied methods, or the need for re-entry/advanced features
Use context managers for file or network resource handling
Use context managers for lock acquisition and release
Use context managers for temporary environment changes (e.g., os.chdir, patch, tempfile)
Use context managers for logging scope control or tracing
Use context managers for transaction control in databases or services
Prefer with open(...) over manual try/finally when working with files

**/*.py: Prefer generators over complex loop logic; refactor complex for-loops into...

Files:

  • shellstub.py
  • conftest.py

⚙️ CodeRabbit configuration file

**/*.py: - Keep cyclomatic complexity ≤ 12

  • Follow single responsibility and CQRS (command/query segregation)
  • Docstrings must follow the numpy style guide. Use a single-line summary for private functions and methods, and full structured docs for all public interfaces.
  • Move conditionals with >2 branches to predicate/helper functions
  • Avoid eval, exec, pickle, monkey-patching, ctypes, unsafe shell
  • Every module must begin with a triple-quoted docstring explaining its purpose, utility, and usage, including example calls if appropriate.
  • Comments and docs must follow en-GB-oxendict (-ize / -our) spelling and grammar
  • Lint suppressions:
    • Blanket # noqa, file-level skips, and categories are forbidden
    • Only narrow in-line disables (# noqa: XYZ) are permitted, and must be accompanied by FIXME: or a ticket link, and used only as a last resort.
  • Use pytest fixtures for shared setup (conftest.py or fixtures/)
  • Replace duplicate tests with @pytest.mark.parametrize
  • Prefer pytest-mock or unittest.mock for stubs/mocks
  • Use assert …, "message" over bare asserts
  • Reflect all API/behaviour changes in docs/ and update roadmap on completion
  • Files must not exceed 400 logical lines:
    • Decompose large modules into subpackages
    • Split large match/case or dispatch tables by domain and collocate with targets if appropriate
    • Move bulky data (fixtures, templates) to external files for parsing at runtime
  • Mutable defaults and shadowed built-ins are forbidden
  • All code must have clear type hints using modern style (A | B, list[str], class Foo[A]:, type Bar = int, etc.), with ABC imports drawn from the correct stdlib module.

Files:

  • shellstub.py
  • conftest.py
**/*.{py,pyi}

📄 CodeRabbit inference engine (.rules/python-00.md)

**/*.{py,pyi}: Use typing everywhere and maintain full static type coverage with Pyright
Avoid Any; prefer precise types (TypeVar, Protocol, Literal, Union); use typing.cast[...] only when necessary with justification; use object for unknown-but-opaque values

Files:

  • shellstub.py
  • conftest.py
**/*.md

⚙️ CodeRabbit configuration file

**/*.md: * Avoid 2nd person or 1st person pronouns ("I", "you", "we")

  • Use en-GB-oxendict (-ize / -our) spelling and grammar
  • Headings must not be wrapped.
  • Documents must start with a level 1 heading
  • Headings must correctly increase or decrease by no more than one level at a time
  • Use GitHub-flavoured Markdown style for footnotes and endnotes.
  • Numbered footnotes must be numbered by order of appearance in the document.

Files:

  • CRUSH.md
🧬 Code graph analysis (15)
.github/actions/release-to-pypi-uv/tests/test_write_summary.py (2)
.github/actions/release-to-pypi-uv/tests/_helpers.py (1)
  • load_script_module (21-31)
.github/actions/release-to-pypi-uv/scripts/write_summary.py (1)
  • main (20-51)
.github/actions/release-to-pypi-uv/tests/test_confirm_release.py (1)
.github/actions/release-to-pypi-uv/tests/test_determine_release.py (1)
  • base_env (29-49)
.github/actions/release-to-pypi-uv/tests/test_publish_release.py (2)
.github/actions/release-to-pypi-uv/tests/_helpers.py (1)
  • load_script_module (21-31)
.github/actions/release-to-pypi-uv/scripts/publish_release.py (1)
  • main (53-66)
.github/actions/rust-build-release/tests/test_utils.py (2)
.github/actions/rust-build-release/tests/conftest.py (1)
  • utils_module (212-214)
conftest.py (1)
  • cmd_mox (68-70)
.github/actions/release-to-pypi-uv/scripts/write_summary.py (2)
.github/actions/release-to-pypi-uv/scripts/determine_release.py (1)
  • main (26-55)
.github/actions/release-to-pypi-uv/scripts/publish_release.py (1)
  • main (53-66)
.github/actions/rust-build-release/tests/test_runtime.py (2)
.github/actions/rust-build-release/tests/conftest.py (4)
  • runtime_module (186-192)
  • module_harness (106-115)
  • patch_shutil_which (89-91)
  • patch_attr (97-99)
.github/actions/rust-build-release/src/runtime.py (2)
  • runtime_available (22-83)
  • detect_host_target (86-124)
.github/actions/rust-build-release/tests/test_target_install.py (2)
conftest.py (4)
  • _register_cross_version_stub (29-44)
  • _register_docker_info_stub (55-60)
  • _register_rustup_toolchain_stub (47-52)
  • cmd_mox (68-70)
.github/actions/rust-build-release/src/main.py (1)
  • main (32-166)
.github/actions/rust-build-release/tests/test_cross_install.py (3)
conftest.py (4)
  • _register_cross_version_stub (29-44)
  • _register_docker_info_stub (55-60)
  • _register_rustup_toolchain_stub (47-52)
  • cmd_mox (68-70)
.github/actions/rust-build-release/tests/conftest.py (3)
  • module_harness (106-115)
  • patch_shutil_which (89-91)
  • patch_run_cmd (78-87)
.github/actions/rust-build-release/src/main.py (1)
  • main (32-166)
.github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py (2)
.github/actions/release-to-pypi-uv/tests/_helpers.py (1)
  • load_script_module (21-31)
.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py (2)
  • main (75-154)
  • _parse_bool (49-55)
.github/actions/release-to-pypi-uv/scripts/determine_release.py (3)
.github/actions/release-to-pypi-uv/scripts/check_github_release.py (1)
  • main (103-131)
.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py (1)
  • main (75-154)
.github/actions/release-to-pypi-uv/scripts/write_summary.py (1)
  • main (20-51)
.github/actions/release-to-pypi-uv/scripts/check_github_release.py (1)
.github/actions/release-to-pypi-uv/tests/test_check_github_release.py (1)
  • read (29-30)
.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py (1)
.github/actions/release-to-pypi-uv/scripts/determine_release.py (1)
  • main (26-55)
.github/actions/release-to-pypi-uv/tests/test_determine_release.py (1)
.github/actions/generate-coverage/tests/test_scripts.py (1)
  • run_script (30-48)
.github/actions/release-to-pypi-uv/tests/test_check_github_release.py (2)
.github/actions/release-to-pypi-uv/tests/_helpers.py (1)
  • load_script_module (21-31)
.github/actions/release-to-pypi-uv/scripts/check_github_release.py (1)
  • main (103-131)
.github/actions/generate-coverage/tests/test_scripts.py (2)
.github/actions/generate-coverage/scripts/coverage_parsers.py (1)
  • get_line_coverage_percent_from_cobertura (32-89)
.github/actions/generate-coverage/scripts/run_rust.py (1)
  • get_line_coverage_percent_from_cobertura (120-149)
⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Sourcery review
  • GitHub Check: python-tests (windows-latest)
  • GitHub Check: python-tests (ubuntu-latest)
  • GitHub Check: build-release (windows-latest, x86_64-pc-windows-gnu)
  • GitHub Check: build-release (windows-latest, aarch64-pc-windows-gnu)
🔇 Additional comments (47)
.github/actions/release-to-pypi-uv/tests/test_publish_release.py (3)

51-61: LGTM: error propagation path is exercised correctly

Keep this as-is.


10-10: Insert SCRIPTS_DIR into sys.path, not REPO_ROOT, to avoid cmd_utils shadowing

Import SCRIPTS_DIR and prepend that path instead of the repo root.

-from ._helpers import REPO_ROOT, load_script_module
+from ._helpers import SCRIPTS_DIR, load_script_module
@@
-    if str(REPO_ROOT) not in module.sys.path:  # type: ignore[attr-defined]
-        module.sys.path.insert(0, str(REPO_ROOT))  # type: ignore[attr-defined]
+    if str(SCRIPTS_DIR) not in module.sys.path:  # type: ignore[attr-defined]
+        module.sys.path.insert(0, str(SCRIPTS_DIR))  # type: ignore[attr-defined]

Also applies to: 16-17


21-48: Parametrise default/custom index cases to remove duplication

Replace the two near-identical tests with one parametrised test.

-def test_publish_default_index(
-    monkeypatch: pytest.MonkeyPatch, publish_module: typ.Any
-) -> None:
-    calls: list[list[str]] = []
-
-    def fake_run_cmd(args: list[str], **_: object) -> None:
-        calls.append(args)
-
-    monkeypatch.setattr(publish_module, "run_cmd", fake_run_cmd)
-
-    publish_module.main(index="")
-
-    assert calls == [["uv", "publish"]]
-
-
-def test_publish_custom_index(
-    monkeypatch: pytest.MonkeyPatch, publish_module: typ.Any
-) -> None:
-    calls: list[list[str]] = []
-
-    def fake_run_cmd(args: list[str], **_: object) -> None:
-        calls.append(args)
-
-    monkeypatch.setattr(publish_module, "run_cmd", fake_run_cmd)
-
-    publish_module.main(index="testpypi")
-
-    assert calls == [["uv", "publish", "--index", "testpypi"]]
+@pytest.mark.parametrize(
+    "index,expected",
+    [
+        ("", [["uv", "publish"]]),
+        ("testpypi", [["uv", "publish", "--index", "testpypi"]]),
+    ],
+)
+def test_publish_index_variants(
+    monkeypatch: pytest.MonkeyPatch,
+    publish_module: typ.Any,
+    index: str,
+    expected: list[list[str]],
+) -> None:
+    calls: list[list[str]] = []
+
+    def fake_run_cmd(args: list[str], **_: object) -> None:
+        calls.append(args)
+
+    monkeypatch.setattr(publish_module, "run_cmd", fake_run_cmd)
+    publish_module.main(index=index)
+    assert calls == expected
CRUSH.md (3)

57-76: Versioning and tagging guidance reads well.

SemVer rules, tag format, and mutable major tags are clearly specified and align with common GitHub Actions practice.


1-1: Start the document with a level-1 heading; fix filename/title mismatch.

Comply with the Markdown guideline: documents must start with an H1. The bare AGENTS.md line is not a heading and does not match the file’s name.

Apply this diff:

-AGENTS.md
+# Agents

Likely an incorrect or invalid review comment.


162-170: Resolve: referenced .rules files exist.
Record verification output: OK for every target.

shellstub.py (1)

9-12: Defer type-only imports — keep cabc/Path under TYPE_CHECKING; verify no runtime evaluation of annotations.

File: shellstub.py (lines 9–12)
if typ.TYPE_CHECKING:
import collections.abc as cabc
from pathlib import Path

Ensure nothing in the repo calls typing.get_type_hints (or similar) at runtime; such calls require those names in module globals. Run the targeted search below and paste the output.

#!/bin/bash
set -euo pipefail
PAT='(get_type_hints|typing\.get_type_hints|from\s+typing\s+import\s+get_type_hints|inspect\.signature|__annotations__\s*=)'
if command -v rg >/dev/null 2>&1; then
  rg -n -C2 --hidden --glob '!**/.git/**' --glob '!**/venv/**' "$PAT" || true
elif command -v grep >/dev/null 2>&1; then
  grep -nR -E -C2 --exclude-dir=.git --exclude-dir=venv "$PAT" . || true
else
  python - <<'PY'
import os,re
p=re.compile(r'(get_type_hints|typing\.get_type_hints|from\s+typing\s+import\s+get_type_hints|inspect\.signature|__annotations__\s*=)')
for root,dirs,files in os.walk('.'):
    dirs[:] = [d for d in dirs if d not in ('.git','venv','env','.venv')]
    for f in files:
        if not f.endswith('.py'): continue
        path=os.path.join(root,f)
        try:
            with open(path,'r',encoding='utf-8') as fh:
                for i,line in enumerate(fh,1):
                    if p.search(line):
                        print(f"{path}:{i}:{line.rstrip()}")
        except:
            pass
PY
fi
.github/actions/setup-windows-gnu/action.yml (2)

17-18: Good clarification on aarch64 cross-linkers.

Keep the intent explicit near the MSYS2 step.


10-12: Validate the llvm-mingw defaults and keep them fresh.

Ensure the default SHA-256 matches the default version asset and plan for periodic bumps or drop the defaults to force explicit inputs.

Fetch the GitHub release “20250910” for mstorsjo/llvm-mingw and verify the SHA-256 of asset “llvm-mingw-20250910-ucrt-x86_64.zip” equals bd88084d7a3b95906fa295453399015a1fdd7b90a38baa8f78244bd234303737.
.github/actions/generate-coverage/tests/test_scripts.py (5)

467-472: LGTM

Zero‑lines LCOV case is correctly asserted to 0.00.


551-556: LGTM

Root‑totals Cobertura fallback is validated correctly.


559-565: LGTM

Zero‑totals Cobertura case handled as 0.00.


567-574: LGTM

Malformed XML raises typer.Exit as expected.


661-668: LGTM

Missing Cobertura file error path is covered and asserts exit code.

.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py (4)

1-5: Pin Typer dependency in script metadata

Avoid unbounded deps in script headers to reduce upstream breakage.

Apply this diff:

 # /// script
 # requires-python = ">=3.13"
-# dependencies = ["typer"]
+# dependencies = ["typer>=0.12,<0.13"]
 # ///

24-33: Broaden SKIP_PARTS to cut IO on large repos

Skip common caches/coverage artefacts.

Apply this diff:

 SKIP_PARTS = {
@@
     ".mypy_cache",
+    ".pytest_cache",
+    ".cache",
+    "htmlcov",
 }

38-46: Stream and sort discovery with iglob for determinism and lower memory

Iterate with glob.iglob and sort for stable order.

Apply this diff:

-def _iter_files(pattern: str) -> typ.Iterable[Path]:
-    candidates = [Path(p) for p in glob.glob(pattern, recursive=True)]
-    for path in candidates:
+def _iter_files(pattern: str) -> typ.Iterable[Path]:
+    for candidate in sorted(glob.iglob(pattern, recursive=True)):
+        path = Path(candidate)
         if not path.is_file():
             continue
         parts = set(path.parts)
         if parts & SKIP_PARTS:
             continue
         yield path

17-23: Gate “no matches” behaviour behind INPUT_FAIL_ON_EMPTY

Fail explicitly on zero matches when requested; keep warn‑and‑continue default.

Apply this diff:

 PATTERN_OPTION = typer.Option("**/pyproject.toml", envvar="INPUT_TOML_GLOB")
 FAIL_ON_DYNAMIC_OPTION = typer.Option(
     "false",
     envvar="INPUT_FAIL_ON_DYNAMIC_VERSION",
 )
+FAIL_ON_EMPTY_OPTION = typer.Option(
+    "false",
+    envvar="INPUT_FAIL_ON_EMPTY",
+)
@@
-def main(
+def main(
     version: str = VERSION_OPTION,
     pattern: str = PATTERN_OPTION,
-    fail_on_dynamic: str = FAIL_ON_DYNAMIC_OPTION,
+    fail_on_dynamic: str = FAIL_ON_DYNAMIC_OPTION,
+    fail_on_empty: str = FAIL_ON_EMPTY_OPTION,
 ) -> None:
@@
-    files = list(_iter_files(pattern))
-    if not files:
-        typer.echo(f"::warning::No TOML files matched pattern {pattern}")
-        return
+    files = list(_iter_files(pattern))
+    if not files:
+        if _parse_bool(fail_on_empty):
+            typer.echo(f"::error::No TOML files matched pattern {pattern}", err=True)
+            raise typer.Exit(1)
+        typer.echo(f"::warning::No TOML files matched pattern {pattern}")
+        return

Update the composite action inputs and README to document INPUT_FAIL_ON_EMPTY.

~~~web
Add an action input named "fail-on-empty" (envvar INPUT_FAIL_ON_EMPTY) defaulting to "false" in .github/actions/release-to-pypi-uv/action.yml and document it in the README’s inputs table.


Also applies to: 97-101

</blockquote></details>
<details>
<summary>.github/actions/release-to-pypi-uv/tests/test_write_summary.py (1)</summary><blockquote>

`18-26`: **Exercise of defaults and append behaviour is sound.**

Invoke the CLI with an empty index to hit the “pypi (default)” branch and write to a new file; this is the right coverage.

</blockquote></details>
<details>
<summary>.github/actions/release-to-pypi-uv/tests/test_check_github_release.py (1)</summary><blockquote>

`38-52`: **Happy path coverage is correct.**

Mock urlopen, run main, and assert the success message; this validates the green path cleanly.

</blockquote></details>
<details>
<summary>.github/actions/release-to-pypi-uv/tests/test_confirm_release.py (1)</summary><blockquote>

`15-31`: **Drive the script via uv with a realistic env.**

Build the environment, call uv run --script, and plumb stdout/stderr with encoding; this is the right shape for an end‑to‑end CLI test.

</blockquote></details>
<details>
<summary>.github/actions/release-to-pypi-uv/scripts/write_summary.py (1)</summary><blockquote>

`39-51`: **Emit a clean, Markdown‑friendly summary.**

Compute the index label, prepend a newline only when appending to non‑empty files, and write a single heading with bullets; this meets the readability goal and matches tests.

</blockquote></details>
<details>
<summary>.github/actions/rust-build-release/tests/test_utils.py (1)</summary><blockquote>

`38-62`: **Adopt cmd‑mox stubs and verify invocation fidelity.**

Use the shimmed docker.exe, replay/verify around run_validated, and assert args[0] equals the validated path; this improves determinism and removes brittle monkeypatching.

</blockquote></details>
<details>
<summary>conftest.py (1)</summary><blockquote>

`12-19`: **Provide portable markers and UV gating.**

Expose CMD_MOX_UNSUPPORTED and REQUIRES_UV, alias the module as shared_actions_conftest, and add require_uv; this cleanly centralises test controls.

</blockquote></details>
<details>
<summary>.github/actions/release-to-pypi-uv/scripts/determine_release.py (1)</summary><blockquote>

`20-24`: **Future‑proof GitHub output writes (multiline syntax).**

Write outputs using the multiline form to avoid edge cases if values ever contain special characters. Update tests that parse GITHUB_OUTPUT accordingly.

Apply this diff:

```diff
 def _emit_outputs(dest: Path, tag: str, version: str) -> None:
-    with dest.open("a", encoding="utf-8") as fh:
-        fh.write(f"tag={tag}\n")
-        fh.write(f"version={version}\n")
+    with dest.open("a", encoding="utf-8") as fh:
+        for key, val in (("tag", tag), ("version", version)):
+            fh.write(f"{key}<<__EOF__\n{val}\n__EOF__\n")
```

</blockquote></details>
<details>
<summary>.github/actions/release-to-pypi-uv/tests/_helpers.py (1)</summary><blockquote>

`12-19`: **Ignore the off‑by‑one claim — keep REPO_ROOT as-is.**

Keep REPO_ROOT computed from _action_root.parents[2]. Verification shows SCRIPTS_DIR = /home/jailuser/git/.github/actions/release-to-pypi-uv/scripts and REPO_ROOT = /home/jailuser/git; _action_root.parents[2] == SCRIPTS_DIR.parents[3] == repository root (parents[1] is '.github'). Do not apply the proposed diff.  


> Likely an incorrect or invalid review comment.

</blockquote></details>
<details>
<summary>.github/actions/rust-build-release/tests/test_runtime.py (3)</summary><blockquote>

`38-61`: **Treat runtime discovery timeouts as unavailable — LGTM**

The timeout path is exercised cleanly and asserts the correct False outcome.

---

`128-156`: **Gate podman on security probe timeout — LGTM**

Correctly models a timeout during the security check as “unavailable”.

---

`194-222`: **Fallback to default triple on rustc timeout — LGTM**

The test validates graceful degradation on slow/hung rustc.

</blockquote></details>
<details>
<summary>.github/actions/rust-build-release/tests/test_target_install.py (3)</summary><blockquote>

`22-60`: **Adopt cmd‑mox for tool discovery — LGTM**

Stub registration and which‑patching are tidy; replay/verify brackets execution correctly, and the assertions prove cross usage.

---

`65-99`: **Error when target unsupported without cross — LGTM**

The test isolates the failure path and asserts the emitted error reliably.

---

`104-141`: **Fallback to cargo when cross container fails — LGTM**

The side‑effect with return code 125 is appropriate and the final assertions lock the cargo fallback.

</blockquote></details>
<details>
<summary>.github/actions/rust-build-release/tests/test_cross_install.py (7)</summary><blockquote>

`26-47`: **Install cross when missing — LGTM**

The staged which responses model first‑run install accurately and assert the correct cargo install args.

---

`81-111`: **Upgrade outdated cross — LGTM**

The rolling version stub and assertions cover the upgrade path cleanly.

---

`113-135`: **Use cached cross — LGTM**

The absence of calls validates the no‑op behaviour when version is sufficient.

---

`137-172`: **Install prebuilt cross on Windows — LGTM**

The test enforces the release path and avoids cargo install on Windows hosts.

---

`351-391`: **Install cross even without container runtimes — LGTM**

The flow ensures installation proceeds and that the build falls back to cargo with the correct toolchain spec.

---

`393-427`: **Fallback to git when crates.io fails — LGTM**

The two‑attempt flow and assertions on --git/--tag are robust.

---

`429-462`: **Fallback to cargo when runtime unusable — LGTM**

The docker info non‑zero exit models an unusable runtime and asserts cargo usage definitively.

</blockquote></details>
<details>
<summary>.github/actions/release-to-pypi-uv/tests/test_determine_release.py (2)</summary><blockquote>

`14-26`: **Use explicit working directory — LGTM**

Running via uv with cwd from env stabilises relative paths; keep check=False to assert return codes manually.

---

`29-49`: **Harden base env for uv runs — LGTM**

PYTHONPATH composition and PWD/IO encoding setup are appropriate for hermetic script execution.

</blockquote></details>
<details>
<summary>.github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py (2)</summary><blockquote>

`51-67`: **Tighten the success assertion**

Assert the deterministic success line to lock the checked count.

Apply:

```diff
-    captured = capsys.readouterr()
-    assert "all versions match 1.0.0" in captured.out
+    captured = capsys.readouterr()
+    assert "Checked 1 PEP 621 project file(s); all versions match 1.0.0." in captured.out
```

---

`254-283`: **Add a failure test for missing [project].version**

Exercise the non‑dynamic missing version error path.

Apply:

```diff
@@
 def test_missing_project_section_is_ignored(
@@
     assert captured.err == ""
+
+
+def test_fails_when_project_version_missing(
+    project_root: Path,
+    module: typ.Any,
+    capsys: pytest.CaptureFixture[str],
+) -> None:
+    _write_pyproject(
+        project_root / "pkg",
+        """
+[project]
+name = "demo"
+""",
+    )
+    with pytest.raises(module.typer.Exit):
+        _invoke_main(module, version="1.0.0")
+    captured = capsys.readouterr()
+    assert "missing [project].version and not marked dynamic" in captured.err
```

</blockquote></details>
<details>
<summary>.github/actions/release-to-pypi-uv/scripts/check_github_release.py (1)</summary><blockquote>

`47-70`: **Handle HTTP 401 unauthorised explicitly**

Surface invalid/expired tokens with an actionable message instead of falling into the generic branch.

Apply:

```diff
         except urllib.error.HTTPError as exc:  # pragma: no cover - network failure path
             detail = (
                 exc.read().decode("utf-8", errors="ignore")
                 if hasattr(exc, "read")
                 else ""
             )
             if exc.code == 404:
                 raise GithubReleaseError(
                     f"No GitHub release found for tag {tag}. Create and publish the release first."
                 ) from exc
+            if exc.code == 401:
+                msg = "GitHub API returned 401 Unauthorized. Ensure GH_TOKEN is valid and not expired."
+                context = detail or exc.reason
+                raise GithubReleaseError(f"{msg} ({context})") from exc
             if exc.code == 403:
                 msg = (
                     "GitHub token lacks permission to read releases or has expired. "
                     "Ensure the workflow is using GITHUB_TOKEN with contents:read scope."
                 )
                 context = detail or exc.reason
                 raise GithubReleaseError(f"{msg} ({context})") from exc
```

</blockquote></details>
<details>
<summary>.github/workflows/rust-toy-app.yml (3)</summary><blockquote>

`40-43`: **Replace Out-File with Add-Content to avoid writing a UTF‑8 BOM to $GITHUB_ENV.**

Out-File can emit a UTF‑8 BOM on file creation in some PowerShell versions; use Add-Content -Encoding utf8 to append without a BOM and keep the step idempotent.

File: .github/workflows/rust-toy-app.yml (lines 40–43)

~~~diff
-        shell: pwsh
-        run: |
-          "LLVM_MINGW_VERSION=20250910" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
-          "LLVM_MINGW_SHA256=bd88084d7a3b95906fa295453399015a1fdd7b90a38baa8f78244bd234303737" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
+        shell: pwsh
+        run: |
+          Add-Content -Path $env:GITHUB_ENV -Value 'LLVM_MINGW_VERSION=20250910' -Encoding utf8
+          Add-Content -Path $env:GITHUB_ENV -Value 'LLVM_MINGW_SHA256=bd88084d7a3b95906fa295453399015a1fdd7b90a38baa8f78244bd234303737' -Encoding utf8

Run this on a Windows runner after the step to verify no BOM (previous attempt failed because GITHUB_ENV was not set):

python - <<'PY'
import os, sys
p = os.environ.get('GITHUB_ENV')
if not p:
    print('GITHUB_ENV not set')
    sys.exit(2)
with open(p, 'rb') as f:
    b = f.read(3)
print('BOM present' if b == b'\xef\xbb\xbf' else 'No BOM')
PY

51-55: Harden rust-build-release against Podman detection timeouts — verify action inputs or add a knob

Run the diagnostic below from the repository root and paste the full output.

#!/bin/bash
set -euo pipefail

echo "PWD: $(pwd)"
echo
echo "=== List .github and .github/actions ==="
ls -la .github || true
ls -la .github/actions || true
echo

if [ -d ".github/actions/rust-build-release" ]; then
  echo "FOUND: .github/actions/rust-build-release"
  find .github/actions/rust-build-release -type f -maxdepth 5 -print
  echo
  for f in .github/actions/rust-build-release/action.yml .github/actions/rust-build-release/action.yaml; do
    if [ -f "$f" ]; then
      echo "---- $f ----"
      sed -n '1,240p' "$f" || true
    fi
  done

  echo
  echo "---- Search for timeout/podman/docker/runtime inputs or strings ----"
  rg -n --hidden -uu -S -C3 -e 'inputs:' -e 'timeout' -e 'container' -e 'runtime' -e 'podman' -e 'docker' -e 'TimeoutExpired' -e 'subprocess\.run' -e 'timeout=' .github/actions/rust-build-release || true

  echo
  if [ -d ".github/actions/rust-build-release/src" ]; then
    echo "---- Head of source files (up to 10) ----"
    find .github/actions/rust-build-release/src -type f | head -n 10 | while read -r file; do
      echo "==== $file ===="
      sed -n '1,120p' "$file" || true
    done
  fi
else
  echo "NOT FOUND: .github/actions/rust-build-release"
  echo "Searching whole repo for rust-build-release, podman, timeout occurrences..."
  rg -n --hidden -uu -S -C3 -e 'rust-build-release' -e 'podman' -e 'docker' -e 'TimeoutExpired' -e 'subprocess\.run' -e 'timeout=' || true
fi

If the action exposes an input to disable runtime detection or set a timeout, set that input on the rust-build-release step in .github/workflows/rust-toy-app.yml for ubuntu-latest. If no input exists, add inputs to action.yml (e.g., runtime_detection: 'skip'|'auto' and runtime_detection_timeout: ), default them to 'skip' in CI, and wire them into the workflow to avoid spurious Podman timeouts.


42-43: Validate llvm-mingw version/sha256 pair before toolchain setup.

Assert LLVM_MINGW_SHA256 matches the downloaded llvm-mingw release asset and fail the job on mismatch to guard against supply‑chain drift.

Check .github/workflows/rust-toy-app.yml (lines 42–43) and verify the custom action at .github/actions/setup-windows-gnu enforces the checksum:

# Confirm action directory exists and search for checksum enforcement
if [ -d ".github/actions/setup-windows-gnu" ]; then
  rg -uu -n -C2 -g ".github/actions/setup-windows-gnu/**" -e 'sha256' -e 'Get-FileHash' -e 'sha256sum' -e 'shasum' -e 'Verify' -e 'checksum'
  rg -uu -n -g ".github/actions/setup-windows-gnu/**" -e 'inputs:' -e 'llvm-mingw-sha256'
else
  echo "MISSING: .github/actions/setup-windows-gnu directory"
fi

If checksum enforcement is absent, add a preflight step immediately before “Configure Windows GNU toolchains” that downloads the 20250910 asset and verifies the SHA256 with Get-FileHash; fail fast on mismatch. Provide the asset filename pattern and I will prepare a patch.

Comment thread .github/actions/release-to-pypi-uv/tests/test_action_python_version.py Outdated
Comment thread .github/actions/release-to-pypi-uv/tests/test_publish_release.py Outdated
Comment thread .github/actions/release-to-pypi-uv/tests/test_write_summary.py
Comment thread .github/actions/rust-build-release/src/runtime.py Outdated
Comment thread .github/actions/setup-windows-gnu/action.yml
Comment thread .gitignore
Comment thread .gitignore
Comment thread Makefile Outdated
Comment thread Makefile Outdated
@leynos leynos force-pushed the python-lib-release-action branch from 2401e23 to def8d17 Compare September 21, 2025 14:28
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

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

⚠️ Outside diff range comments (4)
CRUSH.md (3)

111-121: Fix the README link and simplify formatting.

Use a relative link and drop bold inside the link text to keep Markdown clean and portable.

Apply:

-- Each action [**README.md**](http://README.md) must contain:
+- Each action [README.md](README.md) must contain:

148-149: Replace smart quotes with ASCII quotes for copy‑paste reliability.

Smart quotes around expressions break usage when copied from docs.

Apply:

-- **Composite actions & path context** – When referencing sibling scripts, use “${{ github.action_path }}” for portability.
+- **Composite actions & path context** – When referencing sibling scripts, use "${{ github.action_path }}" for portability.

89-91: Clarify act guidance for macOS.

nektos/act runs Linux containers and does not execute macOS runners. Recommend using it for ubuntu-latest locally and gate macOS smoke tests to GitHub‑hosted jobs.

Apply:

-- **Integration smoke test**: Run via `nektos/act` in CI for the matrix `ubuntu-latest`, `macos-latest` (where feasible).
+- **Integration smoke test**: Run `nektos/act` locally for `ubuntu-latest`. Execute `macos-latest` smoke tests on GitHub‑hosted runners.
.github/actions/rust-build-release/tests/test_cross_install.py (1)

464-487: Fix stream assertion: warning is written to stderr, not stdout.

Read from err and assert the exact message to lock the contract.

-    out = capsys.readouterr().out.lower()
-    assert "warning" in out
+    io = capsys.readouterr()
+    msg = io.err.lower()
+    assert "warning" in msg
+    assert "cross install failed; continuing without cross" in msg
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c334c54 and def8d17.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (32)
  • .github/actions/generate-coverage/tests/test_scripts.py (3 hunks)
  • .github/actions/release-to-pypi-uv/CHANGELOG.md (1 hunks)
  • .github/actions/release-to-pypi-uv/README.md (1 hunks)
  • .github/actions/release-to-pypi-uv/action.yml (1 hunks)
  • .github/actions/release-to-pypi-uv/scripts/check_github_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/scripts/confirm_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/scripts/determine_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/scripts/publish_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py (1 hunks)
  • .github/actions/release-to-pypi-uv/scripts/write_summary.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/_helpers.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_action_python_version.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_check_github_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_confirm_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_determine_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_publish_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_write_summary.py (1 hunks)
  • .github/actions/rust-build-release/tests/conftest.py (0 hunks)
  • .github/actions/rust-build-release/tests/test_cross_install.py (7 hunks)
  • .github/actions/rust-build-release/tests/test_smoke.py (2 hunks)
  • .github/actions/rust-build-release/tests/test_target_install.py (5 hunks)
  • .github/actions/rust-build-release/tests/test_utils.py (3 hunks)
  • .gitignore (1 hunks)
  • CRUSH.md (1 hunks)
  • Makefile (1 hunks)
  • README.md (1 hunks)
  • conftest.py (1 hunks)
  • docs/cmd-mox-users-guide.md (1 hunks)
  • docs/python-native-command-mocking-design.md (1 hunks)
  • docs/scripting-standards.md (1 hunks)
  • pyproject.toml (1 hunks)
💤 Files with no reviewable changes (1)
  • .github/actions/rust-build-release/tests/conftest.py
🧰 Additional context used
📓 Path-based instructions (7)
{README.md,docs/**}

📄 CodeRabbit inference engine (.rules/python-00.md)

Colocate documentation near reusable packages: keep README.md or docs/ and include usage examples

Files:

  • docs/python-native-command-mocking-design.md
  • docs/scripting-standards.md
  • docs/cmd-mox-users-guide.md
  • README.md
**/*.md

⚙️ CodeRabbit configuration file

**/*.md: * Avoid 2nd person or 1st person pronouns ("I", "you", "we")

  • Use en-GB-oxendict (-ize / -our) spelling and grammar
  • Headings must not be wrapped.
  • Documents must start with a level 1 heading
  • Headings must correctly increase or decrease by no more than one level at a time
  • Use GitHub-flavoured Markdown style for footnotes and endnotes.
  • Numbered footnotes must be numbered by order of appearance in the document.

Files:

  • docs/python-native-command-mocking-design.md
  • docs/scripting-standards.md
  • CRUSH.md
  • docs/cmd-mox-users-guide.md
  • README.md
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

For Python code, follow the Python Code Style, Context Managers, Generators, Return Patterns, and Typing guidelines under .rules/

**/*.py: Use snake_case.py for file names; names should reflect contents (e.g., http_client.py, task_queue.py)
Use PascalCase for class names
Use snake_case for variable and function names
Use UPPER_SNAKE_CASE for module-level constants
Prefix internal or non-exported helpers with a single underscore (_)
Use TypedDict or dataclass for structured data; prefer @DataClass(slots=True) for internal-only usage
Be explicit with return types (e.g., -> None, -> str) for all public functions and class methods
Favor immutability: prefer tuples to lists and MappingProxyType for read-only mappings; document any frozendict dependency
Use '# pyright: ignore' sparingly and include an explanation when used
Avoid side effects at import time; modules should not modify global state or perform actions on import
Use docstrings in NumPy format for public functions, classes, and modules
Explain tricky code with inline comments for non-obvious logic or decisions

**/*.py: Prefer @contextmanager from contextlib for straightforward procedural setup/teardown
Implement class-based context managers with enter and exit when lifecycle/state is complex or stateful
Choose @contextmanager when control flow is linear and no persistent state is required
Choose a class-based context manager when there is internal state, lifecycle-tied methods, or the need for re-entry/advanced features
Use context managers for file or network resource handling
Use context managers for lock acquisition and release
Use context managers for temporary environment changes (e.g., os.chdir, patch, tempfile)
Use context managers for logging scope control or tracing
Use context managers for transaction control in databases or services
Prefer with open(...) over manual try/finally when working with files

**/*.py: Prefer generators over complex loop logic; refactor complex for-loops into...

Files:

  • conftest.py

⚙️ CodeRabbit configuration file

**/*.py: - Keep cyclomatic complexity ≤ 12

  • Follow single responsibility and CQRS (command/query segregation)
  • Docstrings must follow the numpy style guide. Use a single-line summary for private functions and methods, and full structured docs for all public interfaces.
  • Move conditionals with >2 branches to predicate/helper functions
  • Avoid eval, exec, pickle, monkey-patching, ctypes, unsafe shell
  • Every module must begin with a triple-quoted docstring explaining its purpose, utility, and usage, including example calls if appropriate.
  • Comments and docs must follow en-GB-oxendict (-ize / -our) spelling and grammar
  • Lint suppressions:
    • Blanket # noqa, file-level skips, and categories are forbidden
    • Only narrow in-line disables (# noqa: XYZ) are permitted, and must be accompanied by FIXME: or a ticket link, and used only as a last resort.
  • Use pytest fixtures for shared setup (conftest.py or fixtures/)
  • Replace duplicate tests with @pytest.mark.parametrize
  • Prefer pytest-mock or unittest.mock for stubs/mocks
  • Use assert …, "message" over bare asserts
  • Reflect all API/behaviour changes in docs/ and update roadmap on completion
  • Files must not exceed 400 logical lines:
    • Decompose large modules into subpackages
    • Split large match/case or dispatch tables by domain and collocate with targets if appropriate
    • Move bulky data (fixtures, templates) to external files for parsing at runtime
  • Mutable defaults and shadowed built-ins are forbidden
  • All code must have clear type hints using modern style (A | B, list[str], class Foo[A]:, type Bar = int, etc.), with ABC imports drawn from the correct stdlib module.

Files:

  • conftest.py
**/*.{py,pyi}

📄 CodeRabbit inference engine (.rules/python-00.md)

**/*.{py,pyi}: Use typing everywhere and maintain full static type coverage with Pyright
Avoid Any; prefer precise types (TypeVar, Protocol, Literal, Union); use typing.cast[...] only when necessary with justification; use object for unknown-but-opaque values

Files:

  • conftest.py
**/pyproject.toml

📄 CodeRabbit inference engine (AGENTS.md)

For Python projects, follow the Python Project Configuration guidance in .rules/python-pyproject.md

Files:

  • pyproject.toml
pyproject.toml

📄 CodeRabbit inference engine (.rules/python-00.md)

pyproject.toml: Enable Ruff and configure it (linting, fixers, formatting) via pyproject.toml
Configure Pyright in strict mode and treat all warnings as CI errors
Use pyproject.toml to configure Ruff, Pyright, and Pytest
Use Ruff for formatting (let Ruff handle whitespace and formatting entirely)

pyproject.toml: Use PEP 621 [project] as the single source of project metadata and runtime dependencies (avoid separate setup.py)
Include mandatory [project] fields: name and version
Provide recommended [project] metadata: description, readme, requires-python, license, authors, keywords, classifiers
Declare runtime dependencies in PEP 508 syntax under [project]
Prefer exact or bounded version ranges for dependencies (e.g., requests>=2.25,<3.0)
Use [project.optional-dependencies] to separate groups like dev and docs
Expose CLIs via [project.scripts] and GUIs via [project.gui-scripts]; register plugins via [project.entry-points.'group.name']
Declare a PEP 517 [build-system] with requires = ["setuptools>=61.0", "wheel"] and build-backend = "setuptools.build_meta" when using setuptools
If omitting [build-system], set [tool.uv].package = true to ensure your project is built/installed by uv
Add [tool.uv] configuration as needed; commonly package = true
Set requires-python in [project] to declare supported interpreter versions
Use valid Trove classifiers in [project].classifiers
Follow semantic versioning for version (e.g., 1.2.3)
Use dynamic fields (e.g., dynamic = ["version"]) sparingly and only if supported by the build backend
Minimize build constraints; omit [build-system] if editable installs aren’t needed (or explicitly set [tool.uv].package = true)

Files:

  • pyproject.toml
{pyproject.toml,uv.lock}

📄 CodeRabbit inference engine (.rules/python-pyproject.md)

Maintain lockfile discipline: after changing [project] fields or dependencies, run uv sync/lock to update uv.lock

Files:

  • pyproject.toml
🧬 Code graph analysis (15)
.github/actions/release-to-pypi-uv/tests/test_check_github_release.py (2)
.github/actions/release-to-pypi-uv/tests/_helpers.py (1)
  • load_script_module (22-32)
.github/actions/release-to-pypi-uv/scripts/check_github_release.py (1)
  • main (103-131)
.github/actions/release-to-pypi-uv/scripts/publish_release.py (1)
.github/actions/release-to-pypi-uv/scripts/write_summary.py (1)
  • main (20-51)
.github/actions/release-to-pypi-uv/tests/test_confirm_release.py (1)
.github/actions/release-to-pypi-uv/tests/test_determine_release.py (1)
  • base_env (29-49)
.github/actions/release-to-pypi-uv/scripts/check_github_release.py (1)
.github/actions/release-to-pypi-uv/tests/test_check_github_release.py (1)
  • read (30-31)
.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py (1)
.github/actions/release-to-pypi-uv/scripts/determine_release.py (1)
  • main (26-55)
.github/actions/rust-build-release/tests/test_utils.py (2)
.github/actions/rust-build-release/tests/conftest.py (1)
  • utils_module (212-214)
conftest.py (1)
  • cmd_mox (68-70)
.github/actions/release-to-pypi-uv/scripts/write_summary.py (2)
.github/actions/release-to-pypi-uv/scripts/determine_release.py (1)
  • main (26-55)
.github/actions/release-to-pypi-uv/scripts/publish_release.py (1)
  • main (53-66)
.github/actions/rust-build-release/tests/test_target_install.py (3)
conftest.py (4)
  • _register_cross_version_stub (29-44)
  • _register_docker_info_stub (55-60)
  • _register_rustup_toolchain_stub (47-52)
  • cmd_mox (68-70)
.github/actions/rust-build-release/tests/conftest.py (3)
  • module_harness (106-115)
  • patch_shutil_which (89-91)
  • patch_attr (97-99)
.github/actions/rust-build-release/src/main.py (1)
  • main (56-193)
.github/actions/generate-coverage/tests/test_scripts.py (2)
.github/actions/generate-coverage/scripts/coverage_parsers.py (1)
  • get_line_coverage_percent_from_cobertura (32-89)
.github/actions/generate-coverage/scripts/run_rust.py (1)
  • get_line_coverage_percent_from_cobertura (120-149)
.github/actions/release-to-pypi-uv/scripts/determine_release.py (3)
.github/actions/release-to-pypi-uv/scripts/check_github_release.py (1)
  • main (103-131)
.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py (1)
  • main (75-154)
.github/actions/release-to-pypi-uv/scripts/write_summary.py (1)
  • main (20-51)
.github/actions/release-to-pypi-uv/tests/test_write_summary.py (2)
.github/actions/release-to-pypi-uv/tests/_helpers.py (1)
  • load_script_module (22-32)
.github/actions/release-to-pypi-uv/scripts/write_summary.py (1)
  • main (20-51)
.github/actions/release-to-pypi-uv/tests/test_publish_release.py (2)
.github/actions/release-to-pypi-uv/tests/_helpers.py (1)
  • load_script_module (22-32)
.github/actions/release-to-pypi-uv/scripts/publish_release.py (1)
  • main (53-66)
.github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py (2)
.github/actions/release-to-pypi-uv/tests/_helpers.py (1)
  • load_script_module (22-32)
.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py (2)
  • main (75-154)
  • _parse_bool (49-55)
.github/actions/release-to-pypi-uv/tests/test_determine_release.py (1)
.github/actions/generate-coverage/tests/test_scripts.py (1)
  • run_script (30-48)
.github/actions/rust-build-release/tests/test_cross_install.py (3)
conftest.py (4)
  • _register_cross_version_stub (29-44)
  • _register_docker_info_stub (55-60)
  • _register_rustup_toolchain_stub (47-52)
  • cmd_mox (68-70)
.github/actions/rust-build-release/tests/conftest.py (2)
  • patch_shutil_which (89-91)
  • patch_run_cmd (78-87)
.github/actions/rust-build-release/src/cross_manager.py (1)
  • ensure_cross (164-249)
🪛 GitHub Actions: CI
.github/actions/rust-build-release/tests/test_smoke.py

[error] 1-1: Test failure: test_action_builds_release_binary_and_manpage[x86_64-pc-windows-msvc] XPASS(strict) Known failure on Windows; see #93. Process completed with exit code 1.


[error] 1-1: Test failure: test_action_builds_release_binary_and_manpage[x86_64-pc-windows-gnu] XPASS(strict) Known failure on Windows; see #93. Process completed with exit code 1.

⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Sourcery review
🔇 Additional comments (56)
CRUSH.md (1)

162-170: Verify referenced .rules/ documents exist.

Automated check failed in this environment; run the following locally and report missing files.

#!/bin/sh
missing=0
for p in \
  .rules/python-00.md \
  .rules/python-context-managers.md \
  .rules/python-generators.md \
  .rules/python-pyproject.md \
  .rules/python-return.md \
  .rules/python-typing.md; do
  if [ -f "$p" ]; then
    echo "OK: $p"
  else
    echo "MISSING: $p"
    missing=1
  fi
done
exit $missing
README.md (1)

12-12: Document the new action entry.

Entry reads well and matches the path/version.

docs/scripting-standards.md (1)

1-80: Adopt scripting standards doc.

Content meets the docs style (headings, en‑GB, no second person) and provides actionable guidance.

.gitignore (1)

159-160: Anchor Crush artefact ignore to the repository root.

Prevent overmatching nested directories by anchoring the pattern.

-.crush/
+/.crush/

Also anchor the adjacent uv entries as previously flagged (apply near Lines 156–157):

/uv.lock
/.uv/
Makefile (1)

42-42: Parameterise Ruff auto-fix selection.

Drive rule selection via a variable to avoid churn in recipes.

-	uvx ruff check --select D202,I001 --fix
+	uvx ruff check --select $(RUFF_FIX_RULES) --fix

Add near the top:

RUFF_FIX_RULES ?= D,I

Update the target description (Line 40) to: “Format Python files and auto-fix selected lint rules”.

pyproject.toml (1)

31-31: Pin the VCS dependency to an immutable revision.

Float-freeze cmd-mox for reproducible CI; use a tag or commit SHA. If private, ensure credentials are wired in CI.

-    "cmd-mox@git+https://github.com/leynos/cmd-mox.git",
+    "cmd-mox@git+https://github.com/leynos/cmd-mox.git@<commit-sha>",

Enable strict Pyright per repository standards (absent at present):

[tool.pyright]
typeCheckingMode = "strict"
reportUnknownParameterType = true
reportUnknownMemberType = true
reportMissingTypeStubs = "error"
pythonVersion = "3.12"
venvPath = "."
venv = ".venv"
docs/cmd-mox-users-guide.md (1)

42-43: Use en‑GB “behaviour”.

Align with repository docs style.

-1. **Record** – describe each expected command call, including its arguments
-   and behavior.
+1. **Record** – describe each expected command call, including its arguments
+   and behaviour.
docs/python-native-command-mocking-design.md (3)

29-31: Use en‑GB “behaviour”.

Match the mandated spelling in design docs.

-   deterministic behavior.
+   deterministic behaviour.

90-92: Switch to Oxford “‑ize” form.

Use initializes per en‑GB‑oxendict.

-manager initialises the controller.
+manager initializes the controller.

107-107: Use en‑GB “behaviour”.

Align the final occurrence.

-  identical behavior given the same expectations and inputs.
+  identical behaviour given the same expectations and inputs.
.github/actions/release-to-pypi-uv/scripts/confirm_release.py (1)

31-36: Avoid leaking the confirmation phrase; normalise input before compare.

Do not echo the expected phrase into logs and tolerate trivial whitespace issues.

-    if confirm != expected:
-        typer.echo(
-            f"::error::Confirmation failed. Set the 'confirm' input to: {expected}",
-            err=True,
-        )
+    # Normalise whitespace to reduce accidental mismatches
+    expected = " ".join(expected.split())
+    confirm = " ".join(confirm.split())
+    if confirm != expected:
+        typer.echo("::error::Confirmation failed. Set the 'confirm' input to the expected phrase.", err=True)
         raise typer.Exit(1)
.github/actions/release-to-pypi-uv/scripts/write_summary.py (1)

20-52: Emit summary looks good; interface is clear.

Retain as-is. The newline prefix prevents header collisions in step summaries, and the env-mapped Typer options make the CLI predictable.

.github/actions/generate-coverage/tests/test_scripts.py (5)

467-472: LCOV zero‑lines test reads clean.


551-556: Cobertura root totals test is correct and matches parser behaviour.


559-564: Cobertura zero totals case exercised properly.


567-574: Malformed XML failure path asserted appropriately.


661-668: Missing file exit path asserted with exit code check.

.github/actions/release-to-pypi-uv/tests/test_publish_release.py (3)

52-65: Tighten failure assertion by matching the error message.

Assert the raised message to lock the contract.

-    with pytest.raises(DummyError):
-        publish_module.main(index="")
+    with pytest.raises(DummyError, match="uv publish failed"):
+        publish_module.main(index="")

11-19: Prevent unintended module shadowing; insert scripts dir not repo root.

Import SCRIPTS_DIR and prepend it to sys.path to avoid picking unrelated cmd_utils modules.

-from ._helpers import REPO_ROOT, load_script_module
+from ._helpers import SCRIPTS_DIR, load_script_module
@@
 def fixture_publish_module() -> ModuleType:
     module = load_script_module("publish_release")
-    if str(REPO_ROOT) not in module.sys.path:  # type: ignore[attr-defined]
-        module.sys.path.insert(0, str(REPO_ROOT))  # type: ignore[attr-defined]
+    if str(SCRIPTS_DIR) not in module.sys.path:  # type: ignore[attr-defined]
+        module.sys.path.insert(0, str(SCRIPTS_DIR))  # type: ignore[attr-defined]
     return module

22-49: Parametrise default/custom index tests to remove duplication.

Replace the two near‑identical tests with one parametrised test.

-def test_publish_default_index(
-    monkeypatch: pytest.MonkeyPatch, publish_module: ModuleType
-) -> None:
-    calls: list[list[str]] = []
-
-    def fake_run_cmd(args: list[str], **_: object) -> None:
-        calls.append(args)
-
-    monkeypatch.setattr(publish_module, "run_cmd", fake_run_cmd)
-
-    publish_module.main(index="")
-
-    assert calls == [["uv", "publish"]]
-
-
-def test_publish_custom_index(
-    monkeypatch: pytest.MonkeyPatch, publish_module: ModuleType
-) -> None:
-    calls: list[list[str]] = []
-
-    def fake_run_cmd(args: list[str], **_: object) -> None:
-        calls.append(args)
-
-    monkeypatch.setattr(publish_module, "run_cmd", fake_run_cmd)
-
-    publish_module.main(index="testpypi")
-
-    assert calls == [["uv", "publish", "--index", "testpypi"]]
+@pytest.mark.parametrize(
+    "index,expected",
+    [
+        ("", [["uv", "publish"]]),
+        ("testpypi", [["uv", "publish", "--index", "testpypi"]]),
+    ],
+)
+def test_publish_index_variants(
+    monkeypatch: pytest.MonkeyPatch,
+    publish_module: ModuleType,
+    index: str,
+    expected: list[list[str]],
+) -> None:
+    calls: list[list[str]] = []
+
+    def fake_run_cmd(args: list[str], **_: object) -> None:
+        calls.append(args)
+
+    monkeypatch.setattr(publish_module, "run_cmd", fake_run_cmd)
+    publish_module.main(index=index)
+    assert calls == expected
.github/actions/release-to-pypi-uv/scripts/determine_release.py (1)

20-24: Future‑proof GITHUB_OUTPUT writes using multiline syntax.

Guard against future values containing special characters by switching to the Actions multiline output format.

 def _emit_outputs(dest: Path, tag: str, version: str) -> None:
-    with dest.open("a", encoding="utf-8") as fh:
-        fh.write(f"tag={tag}\n")
-        fh.write(f"version={version}\n")
+    with dest.open("a", encoding="utf-8") as fh:
+        for key, val in (("tag", tag), ("version", version)):
+            fh.write(f"{key}<<__EOF__\n{val}\n__EOF__\n")
.github/actions/rust-build-release/tests/test_smoke.py (1)

65-69: Deduplicate Windows target checks.

Collapse the repeated endswith logic.

-    if target.endswith("-pc-windows-gnu") or target.endswith("-pc-windows-msvc"):
-        marks.append(WINDOWS_ONLY)
-    if target.endswith("-pc-windows-gnu") or target.endswith("-pc-windows-msvc"):
-        marks.append(WINDOWS_KNOWN_FAILURE)
+    if target.endswith(("-pc-windows-gnu", "-pc-windows-msvc")):
+        marks.extend((WINDOWS_ONLY, WINDOWS_KNOWN_FAILURE))
.github/actions/release-to-pypi-uv/tests/test_check_github_release.py (1)

138-159: Add a failing‑after‑retries test.

Exercise the terminal retry path and assert non‑zero exit with an error message.

Apply this diff (append to the end of the file):

+def test_retries_then_fail(monkeypatch, module, capsys) -> None:
+    def failing_urlopen(request, timeout: float = 30):  # noqa: ANN001
+        raise module.urllib.error.URLError("temporary")
+
+    monkeypatch.setattr(module.urllib.request, "urlopen", failing_urlopen)
+    monkeypatch.setattr(module.time, "sleep", lambda _: None)
+
+    with pytest.raises(module.typer.Exit):
+        module.main(tag="v1.0.0", token="token", repo="owner/repo")
+
+    captured = capsys.readouterr()
+    assert "temporary" in captured.err or "fetch" in captured.err
.github/actions/release-to-pypi-uv/CHANGELOG.md (1)

3-23: LGTM — entries read clearly and match the shipped surface.

.github/actions/release-to-pypi-uv/README.md (1)

30-33: LGTM — usage, permissions, and token wiring are correct.

.github/actions/release-to-pypi-uv/tests/test_confirm_release.py (1)

34-46: LGTM — happy‑path acceptance is clear and stable.

.github/actions/release-to-pypi-uv/tests/test_write_summary.py (1)

29-33: Assert the environment bullet as well.

Tighten the first test to verify the environment line is written.

Apply this diff:

     content = summary_path.read_text(encoding="utf-8")
     assert "## Release summary" in content
     assert "- Released tag: v1.2.3" in content
     assert "- Publish index: pypi (default)" in content
+    assert "- Environment: pypi" in content
.github/actions/release-to-pypi-uv/scripts/publish_release.py (2)

36-41: Remove dead truthiness check in sys.path extension.

Path objects are always truthy; keep only the existence test.

Apply this diff:

-    for candidate in candidates:
-        if not candidate:
-            continue
-        if not candidate.exists():
+    for candidate in candidates:
+        if not candidate.exists():
             continue

50-70: Decouple CLI options from the programmatic API and normalise index.

Avoid Typer options in function defaults and strip whitespace before use.

Apply this diff:

-INDEX_OPTION = typer.Option("", envvar="INPUT_UV_INDEX")
+INDEX_OPTION = typer.Option(
+    "",
+    envvar="INPUT_UV_INDEX",
+    help="Optional index name or URL for uv publish.",
+)
@@
-def main(index: str = INDEX_OPTION) -> None:
+def main(index: str = "") -> None:
@@
-    if index:
+    index = index.strip()
+    if index:
         typer.echo(f"Publishing with uv to index '{index}'")
         run_cmd(["uv", "publish", "--index", index])
     else:
         typer.echo("Publishing with uv to default index (PyPI)")
         run_cmd(["uv", "publish"])
 
-
-if __name__ == "__main__":
-    typer.run(main)
+def cli(index: str = INDEX_OPTION) -> None:
+    """CLI entrypoint."""
+    main(index=index)
+
+if __name__ == "__main__":
+    typer.run(cli)
.github/actions/release-to-pypi-uv/tests/test_action_python_version.py (1)

32-37: Relax brittle equality on the install step.

Assert substring presence to tolerate harmless quoting/layout changes.

Apply this diff:

-    assert install_step["run"] == 'uv python install "${{ inputs.python-version }}"'
+    assert 'uv python install "${{ inputs.python-version }}"' in install_step["run"]
.github/actions/release-to-pypi-uv/tests/_helpers.py (1)

13-19: Fix REPO_ROOT when GITHUB_ACTION_PATH is set.

parents[2] resolves to “.github”, not the repository root. Use parents[3] for parity with the else branch.

Apply this diff:

 if _ACTION_PATH:
     _action_root = Path(_ACTION_PATH).resolve()
     SCRIPTS_DIR = _action_root / "scripts"
-    REPO_ROOT = _action_root.parents[2]
+    REPO_ROOT = _action_root.parents[3]
 else:
     SCRIPTS_DIR = Path(__file__).resolve().parents[1] / "scripts"
     REPO_ROOT = SCRIPTS_DIR.parents[3]
.github/actions/rust-build-release/tests/test_utils.py (1)

38-47: Adopt cmd‑mox spy; LGTM.

Use of CMD_MOX_UNSUPPORTED, environment.shim_dir, spy/replay/verify is correct and consistent with repo fixtures.

conftest.py (2)

12-20: Expose shared test utilities; LGTM.

Windows guard, uv gating, and module export via sys.modules are tidy and pragmatic.


29-61: Stub helpers look solid; LGTM.

Deque‑based multi‑response handler and shim path returns align with cmd‑mox patterns.

.github/actions/rust-build-release/tests/test_target_install.py (7)

22-63: Cross path with container probe; LGTM.

Harness usage, which‑patching, and cmd‑mox replay/verify produce deterministic outcomes.


65-101: Error path without cross; LGTM.

Assertions verify user‑visible error and proper Exit on unsupported target.


104-141: Fallback to cargo on container error; LGTM.

Simulated cross failure via return code 125 and correct fallback are covered.


151-201: Windows host probe decisions; LGTM.

Probe suppression for Windows targets and positive probe for non‑Windows targets are well isolated.


256-275: Unit coverage for should_probe_container; LGTM.

Parameterised expectations are clear and minimal.


278-323: Linker configuration prefers toolchain GCC; LGTM.

Environment export behaviour is correctly asserted.


332-389: Cross linkers on PATH; LGTM.

Selective which patching and env var checks read cleanly.

.github/actions/release-to-pypi-uv/scripts/check_github_release.py (1)

47-70: Handle HTTP 401 and 429 explicitly; improve operator feedback and resilience.

Map 401 unauthorised to an actionable message and respect 429 Retry‑After for backoff. This was requested previously and appears missing again.

         except urllib.error.HTTPError as exc:  # pragma: no cover - network failure path
             detail = (
                 exc.read().decode("utf-8", errors="ignore")
                 if hasattr(exc, "read")
                 else ""
             )
             if exc.code == 404:
                 raise GithubReleaseError(
                     f"No GitHub release found for tag {tag}. Create and publish the release first."
                 ) from exc
+            if exc.code == 401:
+                msg = "GitHub API returned 401 Unauthorized. Ensure GH_TOKEN is valid and not expired."
+                context = detail or exc.reason
+                raise GithubReleaseError(f"{msg} ({context})") from exc
             if exc.code == 403:
                 msg = (
                     "GitHub token lacks permission to read releases or has expired. "
                     "Ensure the workflow is using GITHUB_TOKEN with contents:read scope."
                 )
                 context = detail or exc.reason
                 raise GithubReleaseError(f"{msg} ({context})") from exc
+            if exc.code == 429:
+                # Respect rate-limit signalling and increase backoff.
+                retry_after = 0.0
+                try:
+                    header = exc.headers.get("Retry-After") if hasattr(exc, "headers") else None  # type: ignore[truthy-bool]
+                    if header:
+                        retry_after = float(header)
+                except Exception:
+                    retry_after = 0.0
+                if attempt < max_attempts:
+                    sleep_for = max(delay, retry_after)
+                    time.sleep(sleep_for)
+                    delay *= backoff_factor
+                    continue
             if attempt == max_attempts:
                 raise GithubReleaseError(
                     f"GitHub API request failed with status {exc.code}: {detail or exc.reason}"
                 ) from exc
             time.sleep(delay)
             delay *= backoff_factor
.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py (5)

4-5: Pin Typer dependency in script metadata.

Align dependency policy with other scripts.

-# dependencies = ["typer"]
+# dependencies = ["typer>=0.12,<0.13"]

24-33: Expand skip set to cut IO on large repos.

Skip common caches/artefacts during discovery.

 SKIP_PARTS = {
     ".git",
     ".venv",
     "venv",
     "node_modules",
     "dist",
     "build",
     ".direnv",
     ".mypy_cache",
+    ".pytest_cache",
+    ".cache",
+    "__pycache__",
+    ".tox",
+    "htmlcov",
 }

38-46: Stream and stabilise file discovery.

Use iglob and sort for determinism and lower memory.

-def _iter_files(pattern: str) -> typ.Iterable[Path]:
-    candidates = [Path(p) for p in glob.glob(pattern, recursive=True)]
-    for path in candidates:
+def _iter_files(pattern: str) -> typ.Iterable[Path]:
+    for candidate in sorted(glob.iglob(pattern, recursive=True)):
+        path = Path(candidate)
         if not path.is_file():
             continue
         parts = set(path.parts)
         if parts & SKIP_PARTS:
             continue
         yield path

17-23: Gate “no matches” behaviour behind a flag.

Enable failing the step when nothing matches.

 VERSION_OPTION = typer.Option(..., envvar="RESOLVED_VERSION")
 PATTERN_OPTION = typer.Option("**/pyproject.toml", envvar="INPUT_TOML_GLOB")
 FAIL_ON_DYNAMIC_OPTION = typer.Option(
     "false",
     envvar="INPUT_FAIL_ON_DYNAMIC_VERSION",
 )
+FAIL_ON_EMPTY_OPTION = typer.Option(
+    "false",
+    envvar="INPUT_FAIL_ON_EMPTY",
+)

75-101: Implement FAIL_ON_EMPTY in control flow.

Emit error and exit non‑zero when requested.

-def main(
+def main(
     version: str = VERSION_OPTION,
     pattern: str = PATTERN_OPTION,
-    fail_on_dynamic: str = FAIL_ON_DYNAMIC_OPTION,
+    fail_on_dynamic: str = FAIL_ON_DYNAMIC_OPTION,
+    fail_on_empty: str = FAIL_ON_EMPTY_OPTION,
 ) -> None:
@@
-    files = list(_iter_files(pattern))
-    if not files:
-        typer.echo(f"::warning::No TOML files matched pattern {pattern}")
-        return
+    files = list(_iter_files(pattern))
+    if not files:
+        if _parse_bool(fail_on_empty):
+            typer.echo(f"::error::No TOML files matched pattern {pattern}", err=True)
+            raise typer.Exit(1)
+        typer.echo(f"::warning::No TOML files matched pattern {pattern}")
+        return
.github/actions/rust-build-release/tests/test_cross_install.py (2)

164-171: Windows prebuilt path validation looks good.

The test exercises the release installer, validates execution, and avoids cargo. No issues.


351-357: Add coverage for podman timeout during runtime detection.

Simulate subprocess.TimeoutExpired from podman info and assert the tool falls back to cargo (no cross invocation). Add the test to .github/actions/rust-build-release/tests/test_cross_install.py (e.g. test_falls_back_to_cargo_when_podman_times_out).

Run the repository search below and paste the output to confirm runtime detection catches subprocess.TimeoutExpired; if no handler exists, add handling and the test:

rg -n -C2 --hidden -S -g '!**/node_modules/**' -g '!**/dist/**' \
  'subprocess\.TimeoutExpired|TimeoutExpired|podman info|podman' .

If the search returns no matches, update the runtime detection to catch subprocess.TimeoutExpired raised by podman info and include the new test.

.github/actions/release-to-pypi-uv/action.yml (3)

47-56: Good: setup-uv pinned and wired to python-version with caching.

Pinning the action and enabling dependency cache is solid.


70-77: Surface both GH_TOKEN and GITHUB_TOKEN for downstream tools.

Already exported; keep this for compatibility. No change required.


84-90: Drop the explicit Python install step or move it immediately after setup-uv.

File: .github/actions/release-to-pypi-uv/action.yml (lines 84–90)

-    - name: Install Python
-      run: uv python install "${{ inputs.python-version }}"
-      shell: bash

Verify that setup-uv activates the requested interpreter. Run the check below in a runner with uv available; if it prints the requested version/executable, remove the step; otherwise move it immediately after setup-uv.

#!/bin/bash
set -eu
# Run this on a runner that has `uv` available (e.g., your GitHub Actions runner).
uv --version
uv run --script - <<'PY'
import sys
print("sys.version:", sys.version)
print("sys.executable:", sys.executable)
PY
.github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py (3)

66-68: Tighten success assertion to include checked count.

Assert the full, deterministic success message.

-    captured = capsys.readouterr()
-    assert "all versions match 1.0.0" in captured.out
+    captured = capsys.readouterr()
+    assert "Checked 1 PEP 621 project file(s); all versions match 1.0.0." in captured.out

320-324: Optionally count all mismatch messages to lock processing of multiple files.

Strengthen determinism by asserting at least one mismatch was reported per offending file.

-    assert "!= tag version" in captured.err
+    assert "!= tag version" in captured.err
+    assert captured.err.count("!= tag version") >= 1

255-284: Add failure case for missing [project].version.

Exercise the branch where version is absent and not marked dynamic.

@@
 def test_missing_project_section_is_ignored(
@@
     assert captured.err == ""
+
+
+def test_fails_when_project_version_missing(
+    project_root: Path,
+    module: ModuleType,
+    capsys: pytest.CaptureFixture[str],
+) -> None:
+    _write_pyproject(
+        project_root / "pkg",
+        """
+[project]
+name = "demo"
+""",
+    )
+    with pytest.raises(module.typer.Exit):
+        _invoke_main(module, version="1.0.0")
+    captured = capsys.readouterr()
+    assert "missing [project].version" in captured.err
.github/actions/release-to-pypi-uv/tests/test_determine_release.py (1)

117-134: Provide the repository or release-docs link

Paste the repo URL or the action README/release-docs snippet that defines allowed tag formats; if the repo is private, paste determine_release.py or the relevant parsing code.

Do the release docs for <INSERT_REPO_URL_HERE> state that pre-release (-rc) and build metadata (+build) tags are allowed for this action?

Comment thread .github/actions/release-to-pypi-uv/scripts/check_github_release.py Outdated
Comment thread .github/actions/release-to-pypi-uv/scripts/determine_release.py Outdated
Comment thread .github/actions/release-to-pypi-uv/tests/test_determine_release.py
Comment thread .github/actions/rust-build-release/tests/test_smoke.py
Comment thread .github/actions/rust-build-release/tests/test_utils.py Outdated
Comment thread CRUSH.md
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

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

⚠️ Outside diff range comments (4)
.github/actions/rust-build-release/tests/test_cross_install.py (4)

28-58: Assert no git fallback during initial install

Harden the test to prove the crates.io path was used (and not the git fallback).

Apply this diff:

@@
     install = next(
         cmd for cmd in harness.calls if cmd[:3] == ["cargo", "install", "cross"]
     )
     assert "--locked" in install
     idx = install.index("--version")
     assert install[idx + 1] == "0.2.5"
+    # Prove we did not take the git fallback path
+    assert "--git" not in install
+    assert "--tag" not in install

83-113: Strengthen upgrade assertions to avoid false positives

Pin that an actual versioned reinstall path was taken and git fallback was not.

Apply this diff:

@@
     assert "--locked" in install
     idx = install.index("--version")
     assert install[idx + 1] == "0.2.5"
+    # Ensure upgrade used crates.io, not the git fallback
+    assert "--git" not in install
+    assert "--tag" not in install

139-174: Capture the release version argument on Windows

Record the version passed to install_cross_release to assert the contract.

Apply this diff:

@@
-    release_called = {"value": False}
-
-    def fake_release(version: str) -> bool:
-        release_called["value"] = True
-        return True
+    release_call_args: list[str] = []
+
+    def fake_release(version: str) -> bool:
+        release_call_args.append(version)
+        return True
@@
-    assert release_called["value"] is True
+    assert release_call_args == ["0.2.5"]

353-393: Prove no container runtime invocation leaked through

Augment assertions to guarantee no docker/podman commands were attempted when both are unavailable.

Apply this diff:

@@
     assert install[idx + 1] == "0.2.5"
     build_cmd = app_env.calls[-1]
     assert build_cmd[0] == "cargo"
     assert build_cmd[1] == f"+{default_toolchain}-x86_64-unknown-linux-gnu"
+    # Ensure no container runtime calls were attempted
+    assert all(cmd[0] not in {"docker", "podman"} for cmd in app_env.calls)
+    assert all(cmd[0] not in {"docker", "podman"} for cmd in cross_env.calls)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between def8d17 and b63d8d1.

📒 Files selected for processing (15)
  • .github/actions/release-to-pypi-uv/scripts/determine_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/__init__.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/_helpers.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_check_github_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_confirm_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_determine_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_publish_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_write_summary.py (1 hunks)
  • .github/actions/rust-build-release/src/main.py (0 hunks)
  • .github/actions/rust-build-release/tests/test_cross_install.py (6 hunks)
  • .github/actions/rust-build-release/tests/test_target_install.py (5 hunks)
  • .github/actions/rust-build-release/tests/test_utils.py (3 hunks)
  • conftest.py (1 hunks)
💤 Files with no reviewable changes (1)
  • .github/actions/rust-build-release/src/main.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (.rules/python-return.md)

**/*.py: R501 — Avoid explicit return None if it's the only return in a function
R502 — Avoid implicit None in functions that may return a value; ensure all branches explicitly return a value if any branch does
R503 — Add an explicit return at the end; don't rely on implicit None
R504 — Avoid redundant variable assignment before return; inline the expression unless the variable is meaningfully reused
R505 — Eliminate unnecessary else after a return
R506 — Eliminate unnecessary else after a raise
R507 — Eliminate unnecessary else after a break
R508 — Eliminate unnecessary else after a continue

**/*.py: Use PEP 695 bracketed class-level type parameters for generic class declarations (class Box[T])
Use typing.Self in fluent/builder-style APIs to indicate methods return the same instance
Use @typing.override on methods that override a superclass method (PEP 698)
Use typing.TypeIs[T] for custom runtime type guards that narrow types (PEP 742)
Provide default TypeVar values (PEP 696) for ergonomic generics when appropriate
Prefer builtin generics (list, dict, tuple, etc.) over typing.List, typing.Dict, etc. (PEP 585)
Use PEP 604 union syntax A | B and A | None instead of Optional[A]
Define type aliases using the type keyword (type StrDict = dict[str, str])
When targeting Python < 3.12, use typing.TypeAlias and add "# noqa: UP040" to suppress ruff warning
Place type alias definitions after the import block
Add from future import annotations in modules using type annotations to defer evaluation
Use if typing.TYPE_CHECKING to guard imports needed only for static typing
Use standard aliases: import datetime as dt and collections.abc as cabc

**/*.py: Use PascalCase for class names
Use snake_case for variable and function names
Use UPPER_SNAKE_CASE for module-level constants
Prefix non-exported helpers or internal APIs with a single underscore (_)
Use typing everywhere and maintain full static type coverage with Pyright
Use TypedDict or datacl...

Files:

  • conftest.py

⚙️ CodeRabbit configuration file

**/*.py: - Keep cyclomatic complexity ≤ 12

  • Follow single responsibility and CQRS (command/query segregation)
  • Docstrings must follow the numpy style guide. Use a single-line summary for private functions and methods, and full structured docs for all public interfaces.
  • Move conditionals with >2 branches to predicate/helper functions
  • Avoid eval, exec, pickle, monkey-patching, ctypes, unsafe shell
  • Every module must begin with a triple-quoted docstring explaining its purpose, utility, and usage, including example calls if appropriate.
  • Comments and docs must follow en-GB-oxendict (-ize / -our) spelling and grammar
  • Lint suppressions:
    • Blanket # noqa, file-level skips, and categories are forbidden
    • Only narrow in-line disables (# noqa: XYZ) are permitted, and must be accompanied by FIXME: or a ticket link, and used only as a last resort.
  • Use pytest fixtures for shared setup (conftest.py or fixtures/)
  • Replace duplicate tests with @pytest.mark.parametrize
  • Prefer pytest-mock or unittest.mock for stubs/mocks
  • Use assert …, "message" over bare asserts
  • Reflect all API/behaviour changes in docs/ and update roadmap on completion
  • Files must not exceed 400 logical lines:
    • Decompose large modules into subpackages
    • Split large match/case or dispatch tables by domain and collocate with targets if appropriate
    • Move bulky data (fixtures, templates) to external files for parsing at runtime
  • Mutable defaults and shadowed built-ins are forbidden
  • All code must have clear type hints using modern style (A | B, list[str], class Foo[A]:, type Bar = int, etc.), with ABC imports drawn from the correct stdlib module.

Files:

  • conftest.py
🧬 Code graph analysis (11)
.github/actions/release-to-pypi-uv/tests/test_check_github_release.py (2)
.github/actions/release-to-pypi-uv/tests/_helpers.py (1)
  • load_script_module (21-31)
.github/actions/release-to-pypi-uv/scripts/check_github_release.py (1)
  • main (103-131)
.github/actions/release-to-pypi-uv/tests/test_write_summary.py (2)
.github/actions/release-to-pypi-uv/tests/_helpers.py (1)
  • load_script_module (21-31)
.github/actions/release-to-pypi-uv/scripts/write_summary.py (1)
  • main (20-51)
.github/actions/release-to-pypi-uv/tests/test_publish_release.py (2)
.github/actions/release-to-pypi-uv/tests/_helpers.py (1)
  • load_script_module (21-31)
.github/actions/release-to-pypi-uv/scripts/publish_release.py (1)
  • main (53-66)
.github/actions/release-to-pypi-uv/scripts/determine_release.py (3)
.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py (1)
  • main (74-153)
.github/actions/release-to-pypi-uv/scripts/write_summary.py (1)
  • main (20-51)
.github/actions/release-to-pypi-uv/scripts/check_github_release.py (1)
  • main (103-131)
.github/actions/release-to-pypi-uv/tests/test_confirm_release.py (1)
.github/actions/release-to-pypi-uv/tests/test_determine_release.py (1)
  • base_env (30-39)
.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py (1)
.github/actions/release-to-pypi-uv/scripts/determine_release.py (1)
  • main (26-70)
.github/actions/rust-build-release/tests/test_utils.py (2)
conftest.py (7)
  • CmdMox (46-61)
  • cmd_mox (114-116)
  • spy (54-55)
  • with_args (29-30)
  • returns (32-40)
  • replay (57-58)
  • verify (60-61)
.github/actions/rust-build-release/tests/conftest.py (1)
  • utils_module (212-214)
.github/actions/rust-build-release/tests/test_target_install.py (2)
conftest.py (7)
  • _register_cross_version_stub (71-87)
  • _register_docker_info_stub (99-106)
  • _register_rustup_toolchain_stub (90-96)
  • CmdMox (46-61)
  • cmd_mox (114-116)
  • replay (57-58)
  • verify (60-61)
.github/actions/rust-build-release/tests/conftest.py (3)
  • module_harness (106-115)
  • patch_shutil_which (89-91)
  • patch_attr (97-99)
.github/actions/release-to-pypi-uv/tests/test_determine_release.py (1)
.github/actions/generate-coverage/tests/test_scripts.py (1)
  • run_script (30-48)
.github/actions/rust-build-release/tests/test_cross_install.py (2)
conftest.py (7)
  • _register_cross_version_stub (71-87)
  • _register_docker_info_stub (99-106)
  • _register_rustup_toolchain_stub (90-96)
  • CmdMox (46-61)
  • cmd_mox (114-116)
  • replay (57-58)
  • verify (60-61)
.github/actions/rust-build-release/tests/conftest.py (3)
  • module_harness (106-115)
  • patch_shutil_which (89-91)
  • patch_run_cmd (78-87)
.github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py (2)
.github/actions/release-to-pypi-uv/tests/_helpers.py (1)
  • load_script_module (21-31)
.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py (2)
  • main (74-153)
  • _parse_bool (48-54)
⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Sourcery review
  • GitHub Check: python-tests (ubuntu-latest)
  • GitHub Check: python-tests (windows-latest)
  • GitHub Check: build-release (windows-latest, x86_64-pc-windows-gnu)
  • GitHub Check: build-release (windows-latest, aarch64-pc-windows-gnu)
🔇 Additional comments (33)
.github/actions/release-to-pypi-uv/tests/__init__.py (1)

1-1: LGTM — simple, focused package docstring.

.github/actions/release-to-pypi-uv/tests/test_check_github_release.py (1)

176-176: Cover terminal retry failure.

Add a test where all attempts raise URLError and the CLI exits non‑zero.

Apply this diff:

@@
     assert "GitHub Release 'ok' is published." in captured.out
+
+def test_retries_then_fail(
+    monkeypatch: pytest.MonkeyPatch,
+    module: ModuleType,
+    capsys: pytest.CaptureFixture[str],
+) -> None:
+    """Exit with an error after exhausting retries on transient failures."""
+    def failing_urlopen(request: typ.Any, timeout: float = 30) -> typ.Any:  # noqa: ANN401
+        raise module.urllib.error.URLError("temporary")
+
+    monkeypatch.setattr(module.urllib.request, "urlopen", failing_urlopen)
+    monkeypatch.setattr(module.time, "sleep", lambda _s: None)
+
+    with pytest.raises(module.typer.Exit):
+        module.main(tag="v1.0.0", token="token", repo="owner/repo")
+
+    captured = capsys.readouterr()
+    assert "temporary" in captured.err or "fetch" in captured.err
.github/actions/release-to-pypi-uv/tests/_helpers.py (1)

12-19: LGTM — resolve action/script paths robustly.

Path derivation and explicit module loading look sound.

.github/actions/release-to-pypi-uv/tests/test_confirm_release.py (1)

35-49: LGTM — exercise success and failure paths via uv run.

The helper isolates environment and asserts correct CLI outcomes.

.github/actions/release-to-pypi-uv/scripts/determine_release.py (2)

60-66: Confirm tag policy (pre‑releases/build metadata).

If tags like v1.2.3-rc.1 or v1.2.3+build.5 must be accepted, expand the regex; otherwise document that only vMAJOR.MINOR.PATCH is permitted.


20-24: Future‑proof GITHUB_OUTPUT writes using multiline syntax.

Guard against special characters/newlines in values.

Apply this diff:

 def _emit_outputs(dest: Path, tag: str, version: str) -> None:
-    with dest.open("a", encoding="utf-8") as fh:
-        fh.write(f"tag={tag}\n")
-        fh.write(f"version={version}\n")
+    with dest.open("a", encoding="utf-8") as fh:
+        for key, val in (("tag", tag), ("version", version)):
+            fh.write(f"{key}<<__EOF__\n{val}\n__EOF__\n")
.github/actions/release-to-pypi-uv/tests/test_publish_release.py (3)

16-18: Avoid accidental import shadowing — insert SCRIPTS_DIR on sys.path, not REPO_ROOT.

Prevent picking up unrelated cmd_utils from the repo root.

Apply this diff:

-from ._helpers import REPO_ROOT, load_script_module
+from ._helpers import SCRIPTS_DIR, load_script_module
@@
-    if str(REPO_ROOT) not in module.sys.path:  # type: ignore[attr-defined]
-        module.sys.path.insert(0, str(REPO_ROOT))  # type: ignore[attr-defined]
+    if str(SCRIPTS_DIR) not in module.sys.path:  # type: ignore[attr-defined]
+        module.sys.path.insert(0, str(SCRIPTS_DIR))  # type: ignore[attr-defined]

Also applies to: 9-9


21-35: Remove duplication — parametrise default/custom index tests.

Collapse the two tests into one parametrised case to reduce drift.

Apply this diff:

-def test_publish_default_index(
-    monkeypatch: pytest.MonkeyPatch, publish_module: ModuleType
-) -> None:
-    """Invoke ``uv publish`` without an index when none is provided."""
-    calls: list[list[str]] = []
-
-    def fake_run_cmd(args: list[str], **_: object) -> None:
-        calls.append(args)
-
-    monkeypatch.setattr(publish_module, "run_cmd", fake_run_cmd)
-
-    publish_module.main(index="")
-
-    assert calls == [["uv", "publish"]]
-
-
-def test_publish_custom_index(
-    monkeypatch: pytest.MonkeyPatch, publish_module: ModuleType
-) -> None:
-    """Add the ``--index`` flag when a custom index value is supplied."""
-    calls: list[list[str]] = []
-
-    def fake_run_cmd(args: list[str], **_: object) -> None:
-        calls.append(args)
-
-    monkeypatch.setattr(publish_module, "run_cmd", fake_run_cmd)
-
-    publish_module.main(index="testpypi")
-
-    assert calls == [["uv", "publish", "--index", "testpypi"]]
+@pytest.mark.parametrize(
+    "index,expected",
+    [
+        ("", [["uv", "publish"]]),
+        ("testpypi", [["uv", "publish", "--index", "testpypi"]]),
+    ],
+)
+def test_publish_index_variants(
+    monkeypatch: pytest.MonkeyPatch, publish_module: ModuleType, index: str, expected: list[list[str]]
+) -> None:
+    """Invoke ``uv publish`` with default and custom indices."""
+    calls: list[list[str]] = []
+
+    def fake_run_cmd(args: list[str], **_: object) -> None:
+        calls.append(args)
+
+    monkeypatch.setattr(publish_module, "run_cmd", fake_run_cmd)
+    publish_module.main(index=index)
+    assert calls == expected

Also applies to: 37-50


66-67: Tighten the failure contract — assert the error message.

Match the raised message to ensure the correct failure surfaces.

Apply this diff:

-with pytest.raises(DummyError):
+with pytest.raises(DummyError, match="uv publish failed"):
     publish_module.main(index="")
.github/actions/release-to-pypi-uv/tests/test_write_summary.py (1)

33-36: Assert the environment bullet too.

Verify the full summary content.

Apply this diff:

     content = summary_path.read_text(encoding="utf-8")
     assert "## Release summary" in content
     assert "- Released tag: v1.2.3" in content
     assert "- Publish index: pypi (default)" in content
+    assert "- Environment: pypi" in content
.github/actions/rust-build-release/tests/test_utils.py (1)

60-63: Attach assertion messages to aid triage.

Make failures self‑describing.

Apply this diff:

-    assert isinstance(result, subprocess.CompletedProcess)
-    assert result.args[0] == str(exe_path)
-    assert result.stdout == "ok"
-    assert spy.call_count == 1
+    assert isinstance(result, subprocess.CompletedProcess), "run_validated must return CompletedProcess"
+    assert result.args[0] == str(exe_path), f"unexpected executable path: {result.args[0]!r}"
+    assert result.stdout == "ok", f"unexpected stdout: {result.stdout!r}"
+    assert spy.call_count == 1, f"unexpected docker info call count: {spy.call_count}"
conftest.py (2)

14-21: Keep the UV gate and Windows skip mark.

The platform guard and REQUIRES_UV fixture wiring are clean and avoid spurious failures.


109-116: Retain plugin injection pattern.

The conditional pytest_plugins injection is sound and keeps Windows builds green.

.github/actions/rust-build-release/tests/test_target_install.py (4)

10-15: Import the cmd‑mox types and helpers centrally.

Centralising via shared_actions_conftest reads well and avoids duplication.

Also applies to: 21-22


26-67: Exercise cross‑available fallback path thoroughly.

The run_cmd side effect, toolchain stub, and which mapping correctly drive the path to use cross after rustup failure. Replay/verify bracketing is correct.


69-106: Assert the error branch when cross is unavailable.

The test pins rustup only and forces the add‑target failure, then asserts typer.Exit with a precise error. Good coverage.


108-145: Validate container failure fallback to cargo.

Raising exit 125 for cross and asserting cargo usage mirrors real‑world behaviour. Keep this.

.github/actions/release-to-pypi-uv/tests/test_determine_release.py (2)

9-12: Skip when uv is absent.

The module‑level marker prevents environment‑specific flakes.


14-27: Retain the script runner and env harness.

Encapsulating the UV call and stable base env keeps tests readable and reproducible.

Also applies to: 30-40, 42-53

.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py (3)

37-45: Stream and sort glob results to stabilise order.

Avoid building a list and stabilise logs.

-def _iter_files(pattern: str) -> typ.Iterable[Path]:
-    candidates = [Path(p) for p in glob.glob(pattern, recursive=True)]
-    for path in candidates:
+def _iter_files(pattern: str) -> typ.Iterable[Path]:
+    for candidate in sorted(glob.iglob(pattern, recursive=True)):
+        path = Path(candidate)
         if not path.is_file():
             continue
         parts = set(path.parts)
         if parts & SKIP_PARTS:
             continue
         yield path

23-32: Broaden the skip set to cut IO and noise.

Skip common caches/artefacts for faster deterministic scans.

 SKIP_PARTS = {
     ".git",
     ".venv",
     "venv",
     "node_modules",
     "dist",
     "build",
     ".direnv",
     ".mypy_cache",
+    ".pytest_cache",
+    ".cache",
+    "htmlcov",
 }

2-5: Pin Typer in script metadata.

Avoid unbounded dependency drift by pinning Typer to the tested minor.

 # /// script
 # requires-python = ">=3.13"
-# dependencies = ["typer"]
+# dependencies = ["typer>=0.12,<0.13"]
 # ///
.github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py (8)

266-266: Add a “no matches” test to lock warning behaviour.

Exercise the early‑return path when the glob matches nothing.

+def test_warns_when_no_files_match(
+    project_root: Path, module: ModuleType, capsys: pytest.CaptureFixture[str]
+) -> None:
+    _invoke_main(module, version="1.0.0", pattern="nope/**/pyproject.toml")
+    captured = capsys.readouterr()
+    assert "::warning::No TOML files matched pattern" in captured.out

29-33: Write TOML with UTF‑8 and create parent dirs to avoid flakiness.

Force UTF‑8 and allow nested dirs to be created for future tests (e.g. .venv/pkg).

 def _write_pyproject(base: Path, content: str) -> None:
     """Create a ``pyproject.toml`` file populated with the provided content."""
-    base.mkdir()
-    (base / "pyproject.toml").write_text(content.strip())
+    base.mkdir(parents=True, exist_ok=True)
+    (base / "pyproject.toml").write_text(content.strip(), encoding="utf-8")

55-59: Assert the full success message (incl. checked count).

Lock the message shape to catch regressions.

 captured = capsys.readouterr()
-assert "all versions match 1.0.0" in captured.out
+assert "Checked 1 PEP 621 project file(s); all versions match 1.0.0." in captured.out

125-132: Write the malformed TOML with UTF‑8.

Eliminate locale‑dependent behaviour on CI.

 (target / "pyproject.toml").write_text("this is not TOML")
+# Be explicit about encoding to avoid platform defaults
+(target / "pyproject.toml").write_text("this is not TOML", encoding="utf-8")

226-254: Add a test for missing [project].version (not dynamic).

Cover the explicit error branch for absent literal versions.

 def test_multiple_toml_files_mixed_validity(
@@
     assert "!= tag version" in captured.err
+
+
+def test_fails_when_project_version_missing(
+    project_root: Path,
+    module: ModuleType,
+    capsys: pytest.CaptureFixture[str],
+) -> None:
+    _write_pyproject(
+        project_root / "pkg_no_version",
+        """
+[project]
+name = "demo"
+""",
+    )
+    with pytest.raises(module.typer.Exit):
+        _invoke_main(module, version="1.0.0")
+    captured = capsys.readouterr()
+    assert "missing [project].version" in captured.err

29-33: Add ignored‑directories test (.venv/.mypy_cache) if the walker skips them.

Prove that discovery honours the ignore list.

+def test_skips_files_in_ignored_directories(
+    project_root: Path, module: ModuleType, capsys: pytest.CaptureFixture[str]
+) -> None:
+    _write_pyproject(project_root / ".venv" / "pkg", """
+[project]
+name = "ignored"
+version = "9.9.9"
+""")
+    _write_pyproject(project_root / ".mypy_cache" / "pkg", """
+[project]
+name = "ignored2"
+version = "9.9.9"
+""")
+    _invoke_main(module, version="1.0.0")
+    captured = capsys.readouterr()
+    assert "::warning::No TOML files matched pattern" in captured.out
Confirm that validate_toml_versions._iter_files excludes .venv and .mypy_cache. If not, implement the skip list before adding this test.

226-254: Assert only one mismatch is reported.

Tighten expectations for error fan‑out.

 captured = capsys.readouterr()
 assert "!= tag version" in captured.err
+assert captured.err.count("!= tag version") == 1

220-224: Also assert the zero‑checked success message.

Prove that non‑PEP‑621 files are ignored and still produce the deterministic summary.

 captured = capsys.readouterr()
 assert captured.err == ""
+assert "Checked 0 PEP 621 project file(s); all versions match 1.0.0." in captured.out
.github/actions/rust-build-release/tests/test_cross_install.py (3)

115-137: Cached cross path — LGTM

Avoids install and only verifies version; exactly the behaviour we want.


12-17: Approve shared cmd‑mox helpers — audit passed

Unify imports via shared_actions_conftest and tighten types — LGTM. Automated audit confirms every test that accepts cmd_mox is decorated with @CMD_MOX_UNSUPPORTED.


431-464: Add regression test for podman timeout → cargo fallback

Add an integration test in .github/actions/rust-build-release/tests/test_cross_install.py that simulates subprocess.TimeoutExpired for the "podman info" probe and asserts main(... ) invokes cargo and not cross. Runtime unit tests already exercise timeout behaviour (see .github/actions/rust-build-release/tests/test_runtime.py:test_podman_security_timeout_treated_as_unavailable); add this cross_install-level test to cover the end-to-end fallback path.

Run only the new test locally to verify:

pytest -q -k "podman and timeout"

Comment thread .github/actions/release-to-pypi-uv/tests/test_determine_release.py
Comment thread .github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py Outdated
Comment thread .github/actions/rust-build-release/tests/test_cross_install.py
Comment thread conftest.py Outdated
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Sep 21, 2025

@coderabbitai pause

@coderabbitai coderabbitai Bot added the codex label Sep 21, 2025
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b63d8d1 and e2e9ae9.

📒 Files selected for processing (15)
  • .github/actions/generate-coverage/scripts/run_rust.py (1 hunks)
  • .github/actions/generate-coverage/tests/test_scripts.py (3 hunks)
  • .github/actions/release-to-pypi-uv/scripts/check_github_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/scripts/determine_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/scripts/publish_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/_helpers.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_check_github_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_publish_release.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py (1 hunks)
  • .github/actions/release-to-pypi-uv/tests/test_write_summary.py (1 hunks)
  • .github/actions/rust-build-release/tests/test_target_install.py (6 hunks)
  • .github/actions/rust-build-release/tests/test_utils.py (3 hunks)
  • .github/workflows/ci.yml (1 hunks)
  • conftest.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (.rules/python-00.md)

**/*.py: Use PascalCase for class names
Use snake_case for variable and function names
Use UPPER_SNAKE_CASE for module-level constants
Prefix non-exported helpers or internal APIs with a single underscore (_)
Use typing everywhere and maintain full static type coverage with Pyright
Use TypedDict or dataclass for structured data; for internal-only data classes prefer @DataClass(slots=True)
Avoid Any; prefer precise types (TypeVar, Protocol, Literal, Union) and use typing.cast only when necessary with justification; use object for unknown opaque values
Be explicit with return types for all public functions and class methods (e.g., -> None, -> str)
Favor immutability: prefer tuples to lists and MappingProxyType for read-only mappings; document any frozendict dependency
Avoid side effects at import time—modules should not modify global state or perform actions on import
Use NumPy-style docstrings for public functions, classes, and modules
Explain tricky or non-obvious code with inline comments

**/*.py: Prefer context managers to encapsulate setup/teardown for resources (files, locks, connections, transactions) instead of manual try/finally
Use contextlib.contextmanager for straightforward, linear setup/teardown flows
Implement class-based context managers by defining enter and exit when resource management is stateful or spans multiple methods (e.g., re-entry or advanced features)
Use with open(...) for file handling instead of open() with manual try/finally

**/*.py: Suffix all concrete exception classes with "Error" (Ruff N818)
Avoid raising bare Exception; prefer specific built-ins (e.g., ValueError, TypeError) or domain-specific exceptions (TRY003/TRY004)
Preserve causal chains using "raise ... from ..." when wrapping lower-level errors (TRY201/TRY200)
Avoid blind except handlers like "except:" or "except Exception:"; catch only actionable exceptions (BLE001)
Use try/except/else with else for the happy path when appropriate (TRY300)
Construct exceptio...

Files:

  • conftest.py

⚙️ CodeRabbit configuration file

**/*.py: - Keep cyclomatic complexity ≤ 12

  • Follow single responsibility and CQRS (command/query segregation)
  • Docstrings must follow the numpy style guide. Use a single-line summary for private functions and methods, and full structured docs for all public interfaces.
  • Move conditionals with >2 branches to predicate/helper functions
  • Avoid eval, exec, pickle, monkey-patching, ctypes, unsafe shell
  • Every module must begin with a triple-quoted docstring explaining its purpose, utility, and usage, including example calls if appropriate.
  • Comments and docs must follow en-GB-oxendict (-ize / -our) spelling and grammar
  • Lint suppressions:
    • Blanket # noqa, file-level skips, and categories are forbidden
    • Only narrow in-line disables (# noqa: XYZ) are permitted, and must be accompanied by FIXME: or a ticket link, and used only as a last resort.
  • Use pytest fixtures for shared setup (conftest.py or fixtures/)
  • Replace duplicate tests with @pytest.mark.parametrize
  • Prefer pytest-mock or unittest.mock for stubs/mocks
  • Use assert …, "message" over bare asserts
  • Reflect all API/behaviour changes in docs/ and update roadmap on completion
  • Files must not exceed 400 logical lines:
    • Decompose large modules into subpackages
    • Split large match/case or dispatch tables by domain and collocate with targets if appropriate
    • Move bulky data (fixtures, templates) to external files for parsing at runtime
  • Mutable defaults and shadowed built-ins are forbidden
  • All code must have clear type hints using modern style (A | B, list[str], class Foo[A]:, type Bar = int, etc.), with ABC imports drawn from the correct stdlib module.

Files:

  • conftest.py
🧬 Code graph analysis (10)
.github/actions/release-to-pypi-uv/tests/test_write_summary.py (2)
.github/actions/release-to-pypi-uv/tests/_helpers.py (1)
  • load_script_module (24-35)
.github/actions/release-to-pypi-uv/scripts/write_summary.py (1)
  • main (20-51)
.github/actions/release-to-pypi-uv/scripts/determine_release.py (1)
.github/actions/generate-coverage/scripts/run_rust.py (1)
  • main (343-388)
.github/actions/release-to-pypi-uv/scripts/publish_release.py (1)
.github/actions/release-to-pypi-uv/scripts/write_summary.py (1)
  • main (20-51)
.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py (1)
.github/actions/release-to-pypi-uv/scripts/determine_release.py (1)
  • main (26-71)
.github/actions/rust-build-release/tests/test_utils.py (2)
conftest.py (7)
  • CmdMox (55-70)
  • cmd_mox (132-134)
  • spy (63-64)
  • with_args (32-33)
  • returns (35-43)
  • replay (66-67)
  • verify (69-70)
.github/actions/rust-build-release/tests/conftest.py (1)
  • utils_module (212-214)
.github/actions/release-to-pypi-uv/tests/test_publish_release.py (2)
.github/actions/release-to-pypi-uv/tests/_helpers.py (1)
  • load_script_module (24-35)
.github/actions/release-to-pypi-uv/scripts/publish_release.py (1)
  • main (50-63)
.github/actions/rust-build-release/tests/test_target_install.py (3)
conftest.py (7)
  • _register_cross_version_stub (89-105)
  • _register_docker_info_stub (117-124)
  • _register_rustup_toolchain_stub (108-114)
  • CmdMox (55-70)
  • cmd_mox (132-134)
  • replay (66-67)
  • verify (69-70)
.github/actions/rust-build-release/tests/conftest.py (3)
  • module_harness (106-115)
  • patch_shutil_which (89-91)
  • patch_attr (97-99)
.github/actions/rust-build-release/src/main.py (1)
  • main (54-191)
.github/actions/release-to-pypi-uv/scripts/check_github_release.py (1)
.github/actions/release-to-pypi-uv/tests/test_check_github_release.py (1)
  • read (38-40)
.github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py (2)
.github/actions/release-to-pypi-uv/tests/_helpers.py (1)
  • load_script_module (24-35)
.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py (2)
  • main (76-155)
  • _parse_bool (47-53)
.github/actions/release-to-pypi-uv/tests/test_check_github_release.py (2)
.github/actions/release-to-pypi-uv/tests/_helpers.py (1)
  • load_script_module (24-35)
.github/actions/release-to-pypi-uv/scripts/check_github_release.py (1)
  • main (119-147)
🔍 Remote MCP Deepwiki, Ref

Search attempts — results

  • Tried to fetch the PR/page and run documentation searches; requests failed with HTTP 402 (Request failed).
  • Tried Deepwiki repo index for leynos/shared-actions; returned “Repository not found” (suggested indexing).

Result: I could not retrieve additional external pages beyond the PR context you provided.

Concise, actionable reviewer checklist (from the provided PR context)

  1. Security/permissions: confirm the action’s required workflow permissions and that forwarding GITHUB_TOKEN to scripts (GH_TOKEN) is limited to the minimal scopes needed for trusted publishing.
  2. Tag/version resolution: verify determine_release enforces vX.Y.Z tags, strips leading "v" correctly, and writes GITHUB_OUTPUT as expected.
  3. TOML/version validation: confirm validate_toml_versions correctly handles PEP 621 dynamic vs static versions and that the fail-on-dynamic flag behaves as tested.
  4. Release checks: review check_github_release exponential-backoff/retry logic and error handling for 403/404/other HTTP errors (ensure error messages and exit codes are suitable).
  5. Publish path: ensure publish_release invokes uv publish with --index when provided and that the composite action’s uv setup/“python-version” input is correctly forwarded to the install step (tests were added to validate this).
  6. Tests/CI dependencies: note pyproject adds cmd-mox as a git dev-dependency and tests use REQUIRES_UV/HAS_UV markers — confirm CI runners can install these (and that uv is available when required).
  7. Known CI failure: CI logs (in PR comments) show rust-build-release timing out on /usr/bin/podman info (subprocess TimeoutExpired). There is an issue (#97) created describing the failure and mitigations — recommend adding TimeoutExpired handling, fallback logic, or a longer timeout for container runtime detection.
  8. Docs & changelog: README/action.yml/CHANGELOG additions should be checked for accuracy (inputs/outputs defaults, example usage, and changelog dates/entries).

If you want me to retry external lookups (PR page, issue #97, or action.yml) I can re-run searches — current attempts failed as shown above.

⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: python-tests (ubuntu-latest)
  • GitHub Check: python-tests (windows-latest)
  • GitHub Check: build-release (windows-latest, aarch64-pc-windows-gnu)
  • GitHub Check: build-release (windows-latest, x86_64-pc-windows-gnu)
  • GitHub Check: Sourcery review
🔇 Additional comments (31)
.github/actions/generate-coverage/tests/test_scripts.py (3)

231-233: Use local variable for exception message — OK

Raising with a pre-bound message is fine and keeps the regex assertion intact.


513-515: Pre-binding PermissionError message — OK

The change is neutral and maintains clarity without altering behaviour.


682-684: Pre-binding PermissionError message — OK

The refactor is safe and consistent with the surrounding tests.

.github/workflows/ci.yml (2)

38-41: Keep action pinning; verify the SHA maps to v2.0.2.

Pinning oven-sh/setup-bun is good. Confirm the commit SHA actually corresponds to tag v2.0.2 and is not revoked.

Does commit 735343b667d3e6f658f44d0eca948eb6282f2b76 correspond to oven-sh/setup-bun v2.0.2, and is it the current recommended pin?

46-53: Ensure fmt/lint run through the project's pinned uv runtime (avoid host-tool drift).

CI calls make check-fmt / make lint, but the Makefile invokes uvx (e.g. uvx ruff format --check, uvx ruff check). Verify that uvx executes the pinned uv environment; if it does not, update the Makefile targets to use explicit uv run ... so the CI uses the locked toolchain.

.github/actions/rust-build-release/tests/test_utils.py (2)

40-53: Adopt CmdMox correctly — good swap from monkeypatch.

The switch to CmdMox spies and environment shims is sound and aligns with the new harness.


47-47: Attach assertion messages to aid triage.

Add explicit messages to these asserts per the repo’s testing guidelines.

Apply:

-    assert shim_dir is not None
+    assert shim_dir is not None, "cmd-mox shim_dir must be initialised"

-    assert isinstance(result, subprocess.CompletedProcess)
-    assert result.args[0] == str(exe_path)
-    assert result.stdout == "ok"
-    assert spy.call_count == 1
+    assert isinstance(result, subprocess.CompletedProcess), (
+        f"expected subprocess.CompletedProcess, got {type(result)}"
+    )
+    assert result.args[0] == str(exe_path), (
+        f"unexpected executable path: expected {exe_path}, got {result.args[0]}"
+    )
+    assert result.stdout == "ok", f"unexpected stdout: expected 'ok', got {result.stdout!r}"
+    assert spy.call_count == 1, f"unexpected spy call count: expected 1, got {spy.call_count}"

Also applies to: 62-65

.github/actions/rust-build-release/tests/test_target_install.py (3)

26-67: Exercise cross‑path and docker probe — looks correct.

Stubbing rustup/cross/docker via CmdMox and validating cross build invocation is correct.


108-145: Validate cargo fallback on container error — good coverage.

The 125 exit code path is covered and asserts the cargo fallback correctly.


1-1: Handle podman/docker timeouts in runtime detection to unblock CI.

Catch subprocess.TimeoutExpired in runtime_available(...) and treat it as “runtime not available”; optionally increase the timeout and emit a warning. This prevents the hard failure observed in PR #90 logs.

Apply in the action code (reference path shown; adjust to the actual module):

--- a/.github/actions/rust-build-release/src/utils.py
+++ b/.github/actions/rust-build-release/src/utils.py
@@
-def runtime_available(name: str) -> bool:
-    """Return True if container runtime responds to 'info'."""
-    run_validated(
-        name,
-        ["info"],
-        allowed_names=(name, f"{name}.exe"),
-        timeout=10,
-        check=True,
-        capture_output=True,
-        text=True,
-    )
-    return True
+def runtime_available(name: str) -> bool:
+    """Return True if container runtime responds to 'info' promptly."""
+    try:
+        run_validated(
+            name,
+            ["info"],
+            allowed_names=(name, f"{name}.exe"),
+            timeout=20,
+            check=True,
+            capture_output=True,
+            text=True,
+        )
+        return True
+    except subprocess.TimeoutExpired:
+        typer.echo(f"::warning::{name} info timed out; treating as unavailable", err=True)
+        return False
+    except (subprocess.CalledProcessError, UnexpectedExecutableError):
+        return False

Likely an incorrect or invalid review comment.

.github/actions/release-to-pypi-uv/tests/test_check_github_release.py (1)

168-192: Cover terminal retry failure path.

Add a test that exhausts retries (URLError every time) and asserts exit(1) with an error message.

Apply:

@@
 def test_retries_then_success(
@@
     assert "GitHub Release 'ok' is published." in captured.out
+
+def test_retries_then_fail(
+    monkeypatch: pytest.MonkeyPatch,
+    module: ModuleType,
+    capsys: pytest.CaptureFixture[str],
+    fake_token: str,
+) -> None:
+    """Fail after exhausting retries on transient errors."""
+    monkeypatch.setattr(
+        module.urllib.request,
+        "urlopen",
+        lambda *_args, **_kwargs: (_ for _ in ()).throw(module.urllib.error.URLError("temporary")),
+    )
+    monkeypatch.setattr(module.time, "sleep", lambda _: None)
+
+    with pytest.raises(module.typer.Exit):
+        module.main(tag="v1.0.0", token=fake_token, repo="owner/repo")
+
+    captured = capsys.readouterr()
+    assert "temporary" in captured.err or "fetch" in captured.err
.github/actions/release-to-pypi-uv/tests/_helpers.py (1)

15-22: Resolve action paths robustly — LGTM.

Derive SCRIPTS_DIR and REPO_ROOT correctly for both GITHUB_ACTION_PATH and local runs.

.github/actions/release-to-pypi-uv/scripts/determine_release.py (2)

60-67: Confirm tag policy (pre‑releases/build metadata).

Decide whether v1.2.3‑rc.1 / v1.2.3+build.5 should be accepted; if yes, expand the regex; if no, document the restriction in the error message and README.


20-24: Future‑proof GITHUB_OUTPUT writes (multiline).

Use the delimiter form to avoid edge cases with special characters.

Apply:

 def _emit_outputs(dest: Path, tag: str, version: str) -> None:
-    with dest.open("a", encoding="utf-8") as fh:
-        fh.write(f"tag={tag}\n")
-        fh.write(f"version={version}\n")
+    with dest.open("a", encoding="utf-8") as fh:
+        for key, val in (("tag", tag), ("version", version)):
+            fh.write(f"{key}<<__EOF__\n{val}\n__EOF__\n")
.github/actions/release-to-pypi-uv/tests/test_publish_release.py (3)

12-21: Avoid sys.path shadowing; insert scripts directory, not repo root.

Prevent accidental imports by adding SCRIPTS_DIR to sys.path.

Apply:

-from ._helpers import REPO_ROOT, load_script_module
+from ._helpers import SCRIPTS_DIR, load_script_module
@@
-    module = load_script_module("publish_release")
-    if str(REPO_ROOT) not in module.sys.path:  # type: ignore[attr-defined]
-        module.sys.path.insert(0, str(REPO_ROOT))  # type: ignore[attr-defined]
+    module = load_script_module("publish_release")
+    if str(SCRIPTS_DIR) not in module.sys.path:  # type: ignore[attr-defined]
+        module.sys.path.insert(0, str(SCRIPTS_DIR))  # type: ignore[attr-defined]
     return module

24-54: Remove duplication; parametrise default/custom index tests.

Merge the two tests into one parametrised case.

Apply:

-def test_publish_default_index(
-    monkeypatch: pytest.MonkeyPatch, publish_module: ModuleType
-) -> None:
-    """Invoke ``uv publish`` without an index when none is provided."""
-    calls: list[list[str]] = []
-
-    def fake_run_cmd(args: list[str], **_: object) -> None:
-        calls.append(args)
-
-    monkeypatch.setattr(publish_module, "run_cmd", fake_run_cmd)
-
-    publish_module.main(index="")
-
-    assert calls == [["uv", "publish"]]
-
-
-def test_publish_custom_index(
-    monkeypatch: pytest.MonkeyPatch, publish_module: ModuleType
-) -> None:
-    """Add the ``--index`` flag when a custom index value is supplied."""
-    calls: list[list[str]] = []
-
-    def fake_run_cmd(args: list[str], **_: object) -> None:
-        calls.append(args)
-
-    monkeypatch.setattr(publish_module, "run_cmd", fake_run_cmd)
-
-    publish_module.main(index="testpypi")
-
-    assert calls == [["uv", "publish", "--index", "testpypi"]]
+@pytest.mark.parametrize(
+    "index,expected",
+    [
+        ("", [["uv", "publish"]]),
+        ("testpypi", [["uv", "publish", "--index", "testpypi"]]),
+    ],
+)
+def test_publish_index_variants(
+    monkeypatch: pytest.MonkeyPatch, publish_module: ModuleType, index: str, expected: list[list[str]]
+) -> None:
+    """Invoke ``uv publish`` with default and custom indices."""
+    calls: list[list[str]] = []
+    monkeypatch.setattr(publish_module, "run_cmd", lambda args, **__: calls.append(args))
+    publish_module.main(index=index)
+    assert calls == expected

56-71: Assert the error message to tighten the contract.

Verify the propagated message.

Apply:

-    with pytest.raises(DummyError):
+    with pytest.raises(DummyError, match="uv publish failed"):
         publish_module.main(index="")
.github/actions/release-to-pypi-uv/tests/test_write_summary.py (1)

22-39: Assert the environment bullet as well.

Tighten the assertions by checking the environment line.

Apply:

     content = summary_path.read_text(encoding="utf-8")
     assert "## Release summary" in content
     assert "- Released tag: v1.2.3" in content
     assert "- Publish index: pypi (default)" in content
+    assert "- Environment: pypi" in content
.github/actions/release-to-pypi-uv/scripts/publish_release.py (2)

33-37: Remove dead truthiness check on Path.

Drop the always‑true guard; keep the existence check.

Apply:

-    for candidate in candidates:
-        if not candidate:
-            continue
+    for candidate in candidates:
         if not candidate.exists():
             continue

47-67: Decouple CLI from API and normalise input.

Keep Typer types out of the function signature and strip whitespace on index.

Apply:

-INDEX_OPTION = typer.Option("", envvar="INPUT_UV_INDEX")
+INDEX_OPTION = typer.Option(
+    "",
+    envvar="INPUT_UV_INDEX",
+    help="Optional index name or URL for uv publish.",
+)

-def main(index: str = INDEX_OPTION) -> None:
+def main(index: str = "") -> None:
@@
-    if index:
+    index = index.strip()
+    if index:
         typer.echo(f"Publishing with uv to index '{index}'")
         run_cmd(["uv", "publish", "--index", index])
     else:
         typer.echo("Publishing with uv to default index (PyPI)")
         run_cmd(["uv", "publish"])
 
-
-if __name__ == "__main__":
-    typer.run(main)
+def cli(index: str = INDEX_OPTION) -> None:
+    """CLI entrypoint."""
+    main(index=index)
+
+if __name__ == "__main__":
+    typer.run(cli)
.github/actions/release-to-pypi-uv/scripts/check_github_release.py (3)

2-5: Pin Typer in script metadata.

Pin Typer to a tested minor to avoid upstream breakage.

 # /// script
 # requires-python = ">=3.13"
-# dependencies = ["typer"]
+# dependencies = ["typer>=0.12,<0.13"]
 # ///

64-71: Handle HTTP 401 explicitly.

Surface unauthorised tokens with a clear, actionable message.

         if exc.code == 404:
             message = (
                 "No GitHub release found for tag "
                 f"{tag}. Create and publish the release first."
             )
             raise GithubReleaseError(message) from exc
+        if exc.code == 401:
+            message = "GitHub API returned 401 Unauthorized. Ensure GH_TOKEN is valid and not expired."
+            raise GithubReleaseError(message) from exc
         if exc.code == 403:
             permission_message = (
                 "GitHub token lacks permission to read releases or has expired. "
                 "Use a token with contents:read scope."
             )

27-41: Support GitHub Enterprise and unify User-Agent

+import os
@@
-def _fetch_release(repo: str, tag: str, token: str) -> dict[str, object]:
-    api = f"https://api.github.com/repos/{repo}/releases/tags/{tag}"
+def _fetch_release(repo: str, tag: str, token: str) -> dict[str, object]:
+    api_base = os.getenv("GITHUB_API_URL", "https://api.github.com").rstrip("/")
+    api = f"{api_base}/repos/{repo}/releases/tags/{tag}"
@@
-            "User-Agent": "release-to-pypi-action",
+            "User-Agent": "release-to-pypi-uv",

Scan repository for remaining hard-coded api.github.com URLs and mismatched User-Agent strings and replace them.

conftest.py (1)

32-46: Return Self from fluent doubles.

Modernise the protocol to use typing.Self for fluent API.

-from __future__ import annotations
+from __future__ import annotations
@@
-class CmdDouble(typ.Protocol):
+from typing import Self
+
+class CmdDouble(typ.Protocol):
@@
-    def with_args(self, *args: str) -> CmdDouble:
+    def with_args(self, *args: str) -> Self:
         """Set the expected argv for the double."""
@@
-    ) -> CmdDouble:
+    ) -> Self:
         """Provide canned output for the command invocation."""
@@
-    def runs(self, handler: cabc.Callable[[object], tuple[str, str, int]]) -> CmdDouble:
+    def runs(self, handler: cabc.Callable[[object], tuple[str, str, int]]) -> Self:
         """Execute a handler when the double is invoked."""
.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py (3)

2-5: Pin Typer in script metadata.

Align dependencies with a safe, tested range.

 # /// script
 # requires-python = ">=3.13"
-# dependencies = ["typer"]
+# dependencies = ["typer>=0.12,<0.13"]
 # ///

22-31: Broaden SKIP_PARTS to cut IO noise.

Skip common caches/artefacts in mono‑repos.

 SKIP_PARTS = {
     ".git",
     ".venv",
     "venv",
     "node_modules",
     "dist",
     "build",
     ".direnv",
     ".mypy_cache",
+    "__pycache__",
+    ".pytest_cache",
+    ".tox",
+    ".cache",
+    "htmlcov",
 }

36-45: Stream and stabilise discovery.

Use iglob + sorted for deterministic, memory‑friendly walks.

-def _iter_files(pattern: str) -> typ.Iterable[Path]:
-    root = Path()
-    for path in root.glob(pattern):
+def _iter_files(pattern: str) -> typ.Iterable[Path]:
+    import glob
+    root = Path()
+    for candidate in sorted(glob.iglob(str(root / pattern), recursive=True)):
+        path = Path(candidate)
         if not path.is_file():
             continue
         parts = set(path.parts)
         if parts & SKIP_PARTS:
             continue
         yield path
.github/actions/release-to-pypi-uv/tests/test_validate_toml_versions.py (4)

29-33: Write files with explicit encoding.

Eliminate locale‑dependent flakiness.

 def _write_pyproject(base: Path, content: str) -> None:
     """Create a ``pyproject.toml`` file populated with the provided content."""
     base.mkdir()
-    (base / "pyproject.toml").write_text(content.strip())
+    (base / "pyproject.toml").write_text(content.strip(), encoding="utf-8")

35-40: Allow None for omitted flags.

Model “flag omitted entirely” by accepting None.

-from types import ModuleType
+from types import ModuleType
@@
-def _invoke_main(module: ModuleType, **kwargs: str) -> None:
+def _invoke_main(module: ModuleType, **kwargs: typ.Any) -> None:
     """Invoke ``module.main`` with defaults tailored for the tests."""
     kwargs.setdefault("pattern", "**/pyproject.toml")
     kwargs.setdefault("fail_on_dynamic", "false")
     module.main(**kwargs)

202-203: Pass None to truly omit the flag.

Match the test’s intent and rely on _parse_bool(None) → False.

-    _invoke_main(module, version="1.0.0", fail_on_dynamic="")
+    _invoke_main(module, version="1.0.0", fail_on_dynamic=None)

229-257: Strengthen multi-file assertions and add “no matches” coverage.

Assert deterministic success text and cover empty-match scenarios. Append the following tests:

@@
 def test_multiple_toml_files_mixed_validity(
@@
     assert "!= tag version" in captured.err
+
+
+def test_warns_when_no_files_match(project_root: Path, module: ModuleType, capsys: pytest.CaptureFixture[str]) -> None:
+    _invoke_main(module, version="1.0.0")
+    captured = capsys.readouterr()
+    assert "::warning::No TOML files matched pattern" in captured.out
+
+
+def test_fails_when_no_files_match_and_flag_enabled(
+    project_root: Path,
+    module: ModuleType,
+    capsys: pytest.CaptureFixture[str],
+) -> None:
+    with pytest.raises(module.typer.Exit):
+        _invoke_main(module, version="1.0.0", fail_on_empty="true")
+    captured = capsys.readouterr()
+    assert "::error::No TOML files matched pattern" in captured.err

Comment thread .github/actions/generate-coverage/scripts/run_rust.py Outdated
Comment thread .github/actions/release-to-pypi-uv/scripts/check_github_release.py
Comment thread .github/actions/rust-build-release/tests/test_target_install.py Outdated
Comment thread .github/workflows/ci.yml Outdated
Comment thread conftest.py
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Sep 21, 2025

✅ Actions performed

Reviews paused.

@leynos leynos force-pushed the python-lib-release-action branch 2 times, most recently from 075fda7 to 2f73d93 Compare September 23, 2025 16:32
leynos and others added 25 commits September 24, 2025 12:46
* Use cmd-mox for rust build command tests

* Guard cmd-mox integration on Windows

* Refactor cmd-mox helpers into shared conftest

* Apply cmd-mox fake which simplification
* Mark Windows smoke tests as xfail

* Tighten Windows smoke test expectations
* Address review comments for release to PyPI action

* Add tests and docs for python-version handling
* Fix cmd_mox fixture usage and expose dev extras

* Remove pyproject optional dev extra

* Remove Pyright configuration
* Add toolchain-specific cargo cache key and clean shellstub imports (#88)

* Address review feedback for shellstub imports

* Fix MSYS2 install comments breaking pacman (#89)

* Fix MSYS2 install comments breaking pacman

* Fix Windows llvm-mingw metadata export

* Handle runtime detection timeouts

* Fix rust host detection timeout and Windows bin path (#91)

* Fix rust host detection timeout and Windows bin path

* Add timeout coverage for rust host detection

* Unify runtime probe timeout configuration

* test: capture rustc probe kwargs in timeout test

* Remove inline comments from Windows GNU MSYS2 package list (#99)

* Remove inline MSYS2 install comments

* Skip nfpm steps on Windows builds

* Handle nfpm packaging only for Linux

* Fix Windows invocation of rust-build-release (#102)

* Silence type-check import lints for release action

* Adds ignore rule for Crush agent artifacts

Ignores files generated by the Crush AI agent to prevent them from being tracked in version control.

Adds a symlink to AGENTS.md for discovery by Crush

* Format code after rebase validation to satisfy repo style checks.

💘 Generated with Crush
Co-Authored-By: Crush <crush@charm.land>

* fmt: extend fmt target to run ruff check --select D202,I001 --fix; apply resulting fixes

💘 Generated with Crush
Co-Authored-By: Crush <crush@charm.land>

---------

Co-authored-by: Payton McIntosh <pmcintosh@df12.net>
Co-authored-by: Crush <crush@charm.land>
* Add missing docstrings for release-to-pypi-uv tests

* Simplify cmd-mox typing and streamline docstrings

* Apply formatting

* chore: remove unused imports across scripts and tests

* style(_helpers): alphabetise __all__ tuple

* tests: drop superfluous parentheses from @pytest.fixture usage

---------

Co-authored-by: Payton McIntosh <pmcintosh@df12.net>
* Scope type-checking imports

* Fix lint warnings across release scripts

* Clarify cargo stream capture error

* ci: add lint and format checks

* Install action-validator in CI lint workflow

* Install bun validator packages in CI
* Address review feedback

* Add CLI coverage for publish and validate scripts

* Add deterministic validation tests and bunx validator checks
* Fix Windows xfail marker removal for pytest 8

* Tighten Windows xfail marker filtering
* Handle runtime probe timeouts and expand release tests

* Add _probe_runtime coverage and assert timeout warnings
* Expand pyproject skip list handling

* Strengthen TOML and runtime timeout tests

* Assert cargo fallback logs podman failure

* Parameterize skip-directory regression
* Refine skip directory regression parameterization

* Document skip directories and DRY timeout assertions

* Parametrize publish index test

* Refactor runtime timeout tests

* Rebase onto origin/python-lib-release-action and resolve conflicts

- Resolve merge in validate_toml_versions tests by standardizing module loading
- Register dynamically loaded scripts in sys.modules to support reload semantics
- Make toolchain triple test robust to host arch by selecting matching target
- Run formatting, lint, typecheck, and tests to validate integration

💘 Generated with Crush
Co-Authored-By: Crush <crush@charm.land>

---------

Co-authored-by: Crush <crush@charm.land>
* Improve release validation jitter handling

* Adjust runtime fallback host triples per platform

* Make release validation deterministic and configurable
…preserving branch intent; drop uv.lock in favor of main; fix tests and typing/lint issues; all tests pass and linters clean
* Close cargo pipes when missing

* Ensure cargo pipes close on all paths

* Ensure guard closes cargo pipes before exiting
* Handle release script auth failure and multiline outputs

* Harden windows toolchain setup and extend runtime tests

* Handle release auth errors and tweak toolchain retries
* Fix cross install warning assertion

* Harden cross install harness checks

* Sanitize runtime probe timeout and guard actions

* Refine runtime probe helpers and add timeout tests

* Fix runtime probe lint findings

* Share echo recorder fixture across runtime tests
…ts from main while preserving branch improvements; ensure formatting passes and all tests/linters are green.

💘 Generated with Crush
Co-Authored-By: Crush <crush@charm.land>
@leynos leynos force-pushed the python-lib-release-action branch from dbbd359 to fb721b7 Compare September 24, 2025 12:08
@leynos leynos merged commit 2f6400f into main Sep 24, 2025
6 of 8 checks passed
@leynos leynos deleted the python-lib-release-action branch September 24, 2025 12:16
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Sep 24, 2025

@sourcery-ai review

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes - here's some feedback:

  • This PR is very large and touches many areas—consider splitting it into smaller, domain-focused PRs (e.g. action metadata, helper script implementations, and tests) to make review and maintenance easier.
  • The exponential backoff and jitter logic in setup-windows-gnu’s YAML is quite complex—consider extracting it into a shared script or template to keep the action definition concise.
  • Tests for runtime probe timeouts have a lot of repeated setup—extracting a fixture or helper for patching run_validated on timeout would reduce duplication and improve readability.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- This PR is very large and touches many areas—consider splitting it into smaller, domain-focused PRs (e.g. action metadata, helper script implementations, and tests) to make review and maintenance easier.
- The exponential backoff and jitter logic in setup-windows-gnu’s YAML is quite complex—consider extracting it into a shared script or template to keep the action definition concise.
- Tests for runtime probe timeouts have a lot of repeated setup—extracting a fixture or helper for patching `run_validated` on timeout would reduce duplication and improve readability.

## Individual Comments

### Comment 1
<location> `.github/actions/setup-windows-gnu/action.yml:173` </location>
<code_context>
         if ! command -v x86_64-w64-mingw32-gcc >/dev/null 2>&1; then
           echo "::warning::x86_64 MinGW GCC not found" >&2
         fi
+        require_a64="${{ inputs.require-aarch64 }}"
         if ! command -v aarch64-w64-mingw32-gcc >/dev/null 2>&1 \
           && ! command -v aarch64-w64-mingw32-clang >/dev/null 2>&1; then
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Shell quoting for require_aarch64 may be fragile.

Double-quoting the GitHub input may not handle all cases, such as unexpected whitespace or non-boolean values. Use a stricter parsing method to ensure reliable boolean handling in bash.
</issue_to_address>

### Comment 2
<location> `.github/actions/generate-coverage/scripts/run_rust.py:160` </location>
<code_context>
+        stream.close()
+
+
 def _run_cargo(args: list[str]) -> str:
     """Run ``cargo`` with ``args`` streaming output and return ``stdout``."""
     typer.echo(f"$ cargo {shlex.join(args)}")
</code_context>

<issue_to_address>
**issue (complexity):** Consider extracting the platform-specific output pumping logic into a helper function to make `_run_cargo` flatter and more focused.

Consider extracting the two big “pump” branches (win vs posix) into a helper so that `_run_cargo` becomes flat and focused on orchestration (spawn → pump → wait → cleanup). For example:

```python
def _pump_cargo_output(proc: subprocess.Popen[str]) -> list[str]:
    """Pump proc.stdout/stderr to console, return collected stdout lines."""
    stdout_lines: list[str] = []

    if os.name == "nt":
        thread_excs: list[Exception] = []
        def _win_pump(src: TextIO, out: TextIO, collect: bool):
            try:
                for line in iter(src.readline, ""):
                    out.write(line); out.flush()
                    if collect:
                        stdout_lines.append(line.rstrip("\r\n"))
            except Exception as ex:
                thread_excs.append(ex)

        threads = [
            threading.Thread(target=_win_pump, args=(proc.stdout, sys.stdout, True), daemon=True),
            threading.Thread(target=_win_pump, args=(proc.stderr, sys.stderr, False), daemon=True),
        ]
        for t in threads: t.start()
        for t in threads: t.join()
        if thread_excs:
            proc.kill(); proc.wait()
            raise thread_excs[0]

    else:
        sel = selectors.DefaultSelector()
        sel.register(proc.stdout, selectors.EVENT_READ, "out")
        sel.register(proc.stderr, selectors.EVENT_READ, "err")
        try:
            while sel.get_map():
                for key, _ in sel.select():
                    line = key.fileobj.readline()
                    if not line:
                        sel.unregister(key.fileobj)
                        continue
                    if key.data == "out":
                        typer.echo(line, nl=False)
                        stdout_lines.append(line.rstrip("\r\n"))
                    else:
                        typer.echo(line, err=True, nl=False)
        finally:
            sel.close()

    return stdout_lines
```

Then shrink `_run_cargo` to:

```python
def _run_cargo(args: list[str]) -> str:
    typer.echo(f"$ cargo {shlex.join(args)}")
    proc = cargo[args].popen(...)

    # early stream‐check + cleanup omitted for brevity

    try:
        stdout_lines = _pump_cargo_output(proc)
        ret = proc.wait()
        if ret != 0:
            typer.echo(f"cargo ... failed with code {ret}", err=True)
            raise typer.Exit(code=ret or 1)
        return "\n".join(stdout_lines)
    finally:
        _safe_close_text_stream(proc.stdout)
        _safe_close_text_stream(proc.stderr)
```

This removes two nested `if/else` layers inside `_run_cargo`, collapses common cleanup, and keeps all behavior identical.
</issue_to_address>

### Comment 3
<location> `.github/actions/release-to-pypi-uv/scripts/check_github_release.py:56` </location>
<code_context>
def _fetch_release(repo: str, tag: str, token: str) -> dict[str, object]:
    api = f"https://api.github.com/repos/{repo}/releases/tags/{tag}"
    parsed = urllib.parse.urlsplit(api)
    if parsed.scheme != "https":  # pragma: no cover - defensive guard
        message = f"Unsupported URL scheme '{parsed.scheme}' for GitHub API request."
        raise GithubReleaseError(message)
    request = urllib.request.Request(  # noqa: S310 - https scheme enforced above
        api,
        headers={
            "Authorization": f"Bearer {token}",
            "Accept": "application/vnd.github+json",
            "X-GitHub-Api-Version": "2022-11-28",
            "User-Agent": "release-to-pypi-action",
        },
    )
    max_attempts = 5
    backoff_factor = 1.5
    delay = 1.0
    payload: str | None = None

    for attempt in range(1, max_attempts + 1):
        try:
            with urllib.request.urlopen(request, timeout=30) as response:  # noqa: S310
                payload = response.read().decode("utf-8")
            break
        except urllib.error.HTTPError as exc:  # pragma: no cover - network failure path
            detail = (
                exc.read().decode("utf-8", errors="ignore")
                if hasattr(exc, "read")
                else ""
            )
            match exc.code:
                case 401:
                    context = detail or exc.reason
                    message = (
                        "GitHub rejected the token (401 Unauthorized). "
                        "Verify that GH_TOKEN is correct and has not expired."
                    )
                    if context:
                        message = f"{message} ({context})"
                    raise GithubReleaseError(message) from exc
                case 403:
                    permission_message = (
                        "GitHub token lacks permission to read releases "
                        "or has expired. "
                        "Use a token with contents:read scope."
                    )
                    context = detail or exc.reason
                    message = f"{permission_message} ({context})"
                    raise GithubReleaseError(message) from exc
                case 404:
                    message = (
                        "No GitHub release found for tag "
                        f"{tag}. Create and publish the release first."
                    )
                    raise GithubReleaseError(message) from exc
                case _ if attempt == max_attempts:
                    failure_reason = detail or exc.reason
                    message = (
                        "GitHub API request failed with status "
                        f"{exc.code}: {failure_reason}"
                    )
                    raise GithubReleaseError(message) from exc
                case _:
                    retry_after = None
                    if hasattr(exc, "headers") and exc.headers is not None:
                        retry_after = exc.headers.get("Retry-After")
                    if retry_after:
                        with contextlib.suppress(Exception):
                            delay = float(retry_after)
                    _sleep_with_jitter(delay)
                    delay *= backoff_factor
        except urllib.error.URLError as exc:  # pragma: no cover - network failure path
            if attempt == max_attempts:
                message = f"Failed to reach GitHub API: {exc.reason}"
                raise GithubReleaseError(message) from exc
            _sleep_with_jitter(delay)
            delay *= backoff_factor
    else:  # pragma: no cover - loop exhausted without break
        message = "GitHub API request failed after retries."
        raise GithubReleaseError(message)

    try:
        return json.loads(payload or "")
    except json.JSONDecodeError as exc:  # pragma: no cover - unexpected payload
        message = "GitHub API returned invalid JSON"
        raise GithubReleaseError(message) from exc

</code_context>

<issue_to_address>
**issue (code-quality):** Low code quality found in \_fetch\_release - 21% ([`low-code-quality`](https://docs.sourcery.ai/Reference/Default-Rules/comments/low-code-quality/))

<br/><details><summary>Explanation</summary>The quality score for this function is below the quality threshold of 25%.
This score is a combination of the method length, cognitive complexity and working memory.

How can you solve this?

It might be worth refactoring this function to make it shorter and more readable.

- Reduce the function length by extracting pieces of functionality out into
  their own functions. This is the most important thing you can do - ideally a
  function should be less than 10 lines.
- Reduce nesting, perhaps by introducing guard clauses to return early.
- Ensure that variables are tightly scoped, so that code using related concepts
  sits together within the function rather than being scattered.</details>
</issue_to_address>

### Comment 4
<location> `.github/actions/release-to-pypi-uv/scripts/publish_release.py:34-35` </location>
<code_context>
def _extend_sys_path() -> None:
    candidates: list[Path] = []
    action_path_env = os.getenv("GITHUB_ACTION_PATH")
    if action_path_env:
        action_path = Path(action_path_env).resolve()
        candidates.append(action_path / "scripts")
        with contextlib.suppress(IndexError):
            candidates.append(action_path.parents[2])
    else:
        script_path = Path(__file__).resolve()
        scripts_dir = script_path.parent
        candidates.append(scripts_dir)
        with contextlib.suppress(IndexError):
            candidates.append(scripts_dir.parents[3])

    for candidate in candidates:
        if not candidate.exists():
            continue
        path_str = str(candidate)
        if path_str not in sys.path:
            sys.path.insert(0, path_str)

</code_context>

<issue_to_address>
**suggestion (code-quality):** Use named expression to simplify assignment and conditional ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))

```suggestion
    if action_path_env := os.getenv("GITHUB_ACTION_PATH"):
```
</issue_to_address>

### Comment 5
<location> `.github/actions/release-to-pypi-uv/scripts/validate_toml_versions.py:80-82` </location>
<code_context>
def _parse_bool(value: str | None) -> bool:
    if value is None:
        return False
    normalized = value.strip().lower()
    if not normalized:
        return False
    return normalized in TRUTHY_STRINGS

</code_context>

<issue_to_address>
**suggestion (code-quality):** We've found these issues:

- Lift code into else after jump in control flow ([`reintroduce-else`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/reintroduce-else/))
- Replace if statement with if expression ([`assign-if-exp`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/assign-if-exp/))

```suggestion
    return False if not normalized else normalized in TRUTHY_STRINGS
```
</issue_to_address>

### Comment 6
<location> `.github/actions/release-to-pypi-uv/tests/_helpers.py:14` </location>
<code_context>
"""Test helpers for release-to-pypi-uv action scripts."""

from __future__ import annotations

import importlib.util
import os
import sys
import typing as typ
from pathlib import Path

if typ.TYPE_CHECKING:  # pragma: no cover - imported for annotations only
    from types import ModuleType

_ACTION_PATH = os.environ.get("GITHUB_ACTION_PATH")

if _ACTION_PATH:
    _action_root = Path(_ACTION_PATH).resolve()
    SCRIPTS_DIR = _action_root / "scripts"
    REPO_ROOT = _action_root.parents[2]
else:
    SCRIPTS_DIR = Path(__file__).resolve().parents[1] / "scripts"
    REPO_ROOT = SCRIPTS_DIR.parents[3]

</code_context>

<issue_to_address>
**issue (code-quality):** Use named expression to simplify assignment and conditional ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))
</issue_to_address>

### Comment 7
<location> `.github/actions/rust-build-release/src/main.py:143` </location>
<code_context>
@app.command()
def main(
    target: str = typer.Argument("", help="Target triple to build"),
    toolchain: str = typer.Option(
        DEFAULT_TOOLCHAIN,
        envvar="RBR_TOOLCHAIN",
        help="Rust toolchain version",
    ),
) -> None:
    """Build the project for *target* using *toolchain*."""
    if not target:
        target = os.environ.get("RBR_TARGET", "")
    if not target:
        env_rbr_target = os.environ.get("RBR_TARGET", "<unset>")
        env_input_target = os.environ.get("INPUT_TARGET", "<unset>")
        env_github_ref = os.environ.get("GITHUB_REF", "<unset>")
        typer.echo(
            "::error:: no build target specified; "
            "set input 'target' or env RBR_TARGET\n"
            f"RBR_TARGET={env_rbr_target} "
            f"INPUT_TARGET={env_input_target} "
            f"GITHUB_REF={env_github_ref}",
            err=True,
        )
        raise typer.Exit(1)

    rustup_path = shutil.which("rustup")
    if rustup_path is None:
        typer.echo("::error:: rustup not found", err=True)
        raise typer.Exit(1)
    try:
        rustup_exec = ensure_allowed_executable(rustup_path, ("rustup", "rustup.exe"))
    except UnexpectedExecutableError:
        typer.echo("::error:: unexpected rustup executable", err=True)
        raise typer.Exit(1) from None
    installed_names = _list_installed_toolchains(rustup_exec)
    toolchain_name = _resolve_toolchain_name(toolchain, target, installed_names)
    if not toolchain_name:
        try:
            run_cmd(
                [
                    rustup_exec,
                    "toolchain",
                    "install",
                    toolchain,
                    "--profile",
                    "minimal",
                    "--no-self-update",
                ]
            )
        except subprocess.CalledProcessError:
            typer.echo(
                f"::error:: failed to install toolchain '{toolchain}'",
                err=True,
            )
            typer.echo(
                f"::error:: requested toolchain '{toolchain}' not installed",
                err=True,
            )
            raise typer.Exit(1) from None
        installed_names = _list_installed_toolchains(rustup_exec)
        toolchain_name = _resolve_toolchain_name(toolchain, target, installed_names)
    if not toolchain_name:
        # Fallback: any installed variant that starts with the channel name.
        channel_prefix = f"{toolchain}-"
        toolchain_name = next(
            (
                name
                for name in installed_names
                if name == toolchain or name.startswith(channel_prefix)
            ),
            "",
        )
    if not toolchain_name:
        # Accept host-architecture-suffixed installations of the requested channel
        channel_prefix = f"{toolchain}-"
        toolchain_name = next(
            (name for name in installed_names if name.startswith(channel_prefix)), ""
        )
    if not toolchain_name:
        typer.echo(
            f"::error:: requested toolchain '{toolchain}' not installed",
            err=True,
        )
        raise typer.Exit(1)
    target_installed = True
    try:
        run_cmd([rustup_exec, "target", "add", "--toolchain", toolchain_name, target])
    except subprocess.CalledProcessError:
        typer.echo(
            f"::warning:: toolchain '{toolchain_name}' does not support "
            f"target '{target}'; continuing",
            err=True,
        )
        target_installed = False

    configure_windows_linkers(toolchain_name, target, rustup_exec)

    cross_path, cross_version = ensure_cross("0.2.5")
    docker_present = False
    podman_present = False
    if should_probe_container(sys.platform, target):
        docker_present = _probe_runtime("docker")
        podman_present = _probe_runtime("podman")
    has_container = docker_present or podman_present

    use_cross = cross_path is not None and has_container
    cargo_toolchain_spec = f"+{toolchain_name}"
    cross_toolchain_spec = cargo_toolchain_spec
    if use_cross:
        cross_toolchain_name = _toolchain_channel(toolchain_name)
        if (
            cross_toolchain_name != toolchain_name
            and cross_toolchain_name not in installed_names
        ):
            try:
                run_cmd(
                    [
                        rustup_exec,
                        "toolchain",
                        "install",
                        cross_toolchain_name,
                        "--profile",
                        "minimal",
                        "--no-self-update",
                    ]
                )
            except subprocess.CalledProcessError:
                typer.echo(
                    "::warning:: failed to install sanitized toolchain; using cargo",
                    err=True,
                )
                use_cross = False
            else:
                installed_names = _list_installed_toolchains(rustup_exec)
        if use_cross:
            cross_toolchain_spec = f"+{cross_toolchain_name}"

    if not use_cross and not target_installed:
        typer.echo(
            f"::error:: toolchain '{toolchain_name}' does not support "
            f"target '{target}'",
            err=True,
        )
        raise typer.Exit(1)

    if not use_cross:
        if cross_path is None:
            typer.echo("cross missing; using cargo")
        elif not has_container:
            typer.echo(
                f"cross ({cross_version}) requires a container runtime; using cargo "
                f"(docker={docker_present}, podman={podman_present})"
            )
    else:
        typer.echo(f"Building with cross ({cross_version})")

    build_cmd = [
        "cross" if use_cross else "cargo",
        cross_toolchain_spec if use_cross else cargo_toolchain_spec,
        "build",
        "--release",
        "--target",
        target,
    ]
    try:
        run_cmd(build_cmd)
    except subprocess.CalledProcessError as exc:
        if use_cross and exc.returncode in CROSS_CONTAINER_ERROR_CODES:
            typer.echo(
                "::warning:: cross failed to start a container; retrying with cargo",
                err=True,
            )
            fallback_cmd = [
                "cargo",
                cargo_toolchain_spec,
                "build",
                "--release",
                "--target",
                target,
            ]
            run_cmd(fallback_cmd)
        else:
            raise

</code_context>

<issue_to_address>
**issue (code-quality):** We've found these issues:

- Hoist conditional out of nested conditional ([`hoist-if-from-if`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/hoist-if-from-if/))
- Swap if/else branches ([`swap-if-else-branches`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/swap-if-else-branches/))
- Merge else clause's nested if statement into elif ([`merge-else-if-into-elif`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/merge-else-if-into-elif/))
- Low code quality found in main - 15% ([`low-code-quality`](https://docs.sourcery.ai/Reference/Default-Rules/comments/low-code-quality/))

<br/><details><summary>Explanation</summary>


The quality score for this function is below the quality threshold of 25%.
This score is a combination of the method length, cognitive complexity and working memory.

How can you solve this?

It might be worth refactoring this function to make it shorter and more readable.

- Reduce the function length by extracting pieces of functionality out into
  their own functions. This is the most important thing you can do - ideally a
  function should be less than 10 lines.
- Reduce nesting, perhaps by introducing guard clauses to return early.
- Ensure that variables are tightly scoped, so that code using related concepts
  sits together within the function rather than being scattered.</details>
</issue_to_address>

### Comment 8
<location> `.github/actions/rust-build-release/tests/test_cross_install.py:500-502` </location>
<code_context>
@CMD_MOX_UNSUPPORTED
def test_falls_back_to_cargo_when_podman_unusable(
    main_module: ModuleType,
    cross_module: ModuleType,
    module_harness: HarnessFactory,
    cmd_mox: CmdMox,
) -> None:
    """Falls back to cargo when podman exists but is unusable."""
    cross_env = module_harness(cross_module)
    app_env = module_harness(main_module)

    default_toolchain = main_module.DEFAULT_TOOLCHAIN
    rustup_stdout = f"{default_toolchain}-x86_64-unknown-linux-gnu\n"
    cross_path = _register_cross_version_stub(cmd_mox)
    rustup_path = _register_rustup_toolchain_stub(cmd_mox, rustup_stdout)
    podman_path = _register_podman_info_stub(cmd_mox, exit_code=1)

    def fake_which(name: str) -> str | None:
        if name == "podman":
            return podman_path
        if name == "cross":
            return cross_path
        if name == "rustup":
            return rustup_path
        return None

    cross_env.patch_shutil_which(fake_which)
    app_env.patch_shutil_which(fake_which)

    cmd_mox.replay()
    main_module.main("x86_64-unknown-linux-gnu", default_toolchain)
    cmd_mox.verify()

    assert any(cmd[0] == "cargo" for cmd in app_env.calls)
    assert all(cmd[0] != "cross" for cmd in app_env.calls)

</code_context>

<issue_to_address>
**suggestion (code-quality):** We've found these issues:

- Lift code into else after jump in control flow ([`reintroduce-else`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/reintroduce-else/))
- Replace if statement with if expression ([`assign-if-exp`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/assign-if-exp/))

```suggestion
        return rustup_path if name == "rustup" else None
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

if ! command -v x86_64-w64-mingw32-gcc >/dev/null 2>&1; then
echo "::warning::x86_64 MinGW GCC not found" >&2
fi
require_a64="${{ inputs.require-aarch64 }}"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (bug_risk): Shell quoting for require_aarch64 may be fragile.

Double-quoting the GitHub input may not handle all cases, such as unexpected whitespace or non-boolean values. Use a stricter parsing method to ensure reliable boolean handling in bash.

stream.close()


def _run_cargo(args: list[str]) -> str:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (complexity): Consider extracting the platform-specific output pumping logic into a helper function to make _run_cargo flatter and more focused.

Consider extracting the two big “pump” branches (win vs posix) into a helper so that _run_cargo becomes flat and focused on orchestration (spawn → pump → wait → cleanup). For example:

def _pump_cargo_output(proc: subprocess.Popen[str]) -> list[str]:
    """Pump proc.stdout/stderr to console, return collected stdout lines."""
    stdout_lines: list[str] = []

    if os.name == "nt":
        thread_excs: list[Exception] = []
        def _win_pump(src: TextIO, out: TextIO, collect: bool):
            try:
                for line in iter(src.readline, ""):
                    out.write(line); out.flush()
                    if collect:
                        stdout_lines.append(line.rstrip("\r\n"))
            except Exception as ex:
                thread_excs.append(ex)

        threads = [
            threading.Thread(target=_win_pump, args=(proc.stdout, sys.stdout, True), daemon=True),
            threading.Thread(target=_win_pump, args=(proc.stderr, sys.stderr, False), daemon=True),
        ]
        for t in threads: t.start()
        for t in threads: t.join()
        if thread_excs:
            proc.kill(); proc.wait()
            raise thread_excs[0]

    else:
        sel = selectors.DefaultSelector()
        sel.register(proc.stdout, selectors.EVENT_READ, "out")
        sel.register(proc.stderr, selectors.EVENT_READ, "err")
        try:
            while sel.get_map():
                for key, _ in sel.select():
                    line = key.fileobj.readline()
                    if not line:
                        sel.unregister(key.fileobj)
                        continue
                    if key.data == "out":
                        typer.echo(line, nl=False)
                        stdout_lines.append(line.rstrip("\r\n"))
                    else:
                        typer.echo(line, err=True, nl=False)
        finally:
            sel.close()

    return stdout_lines

Then shrink _run_cargo to:

def _run_cargo(args: list[str]) -> str:
    typer.echo(f"$ cargo {shlex.join(args)}")
    proc = cargo[args].popen(...)

    # early stream‐check + cleanup omitted for brevity

    try:
        stdout_lines = _pump_cargo_output(proc)
        ret = proc.wait()
        if ret != 0:
            typer.echo(f"cargo ... failed with code {ret}", err=True)
            raise typer.Exit(code=ret or 1)
        return "\n".join(stdout_lines)
    finally:
        _safe_close_text_stream(proc.stdout)
        _safe_close_text_stream(proc.stderr)

This removes two nested if/else layers inside _run_cargo, collapses common cleanup, and keeps all behavior identical.

"""Raised when the GitHub release is not ready for publishing."""


def _fetch_release(repo: str, tag: str, token: str) -> dict[str, object]:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (code-quality): Low code quality found in _fetch_release - 21% (low-code-quality)


ExplanationThe quality score for this function is below the quality threshold of 25%.
This score is a combination of the method length, cognitive complexity and working memory.

How can you solve this?

It might be worth refactoring this function to make it shorter and more readable.

  • Reduce the function length by extracting pieces of functionality out into
    their own functions. This is the most important thing you can do - ideally a
    function should be less than 10 lines.
  • Reduce nesting, perhaps by introducing guard clauses to return early.
  • Ensure that variables are tightly scoped, so that code using related concepts
    sits together within the function rather than being scattered.

Comment on lines +34 to +35
action_path_env = os.getenv("GITHUB_ACTION_PATH")
if action_path_env:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (code-quality): Use named expression to simplify assignment and conditional (use-named-expression)

Suggested change
action_path_env = os.getenv("GITHUB_ACTION_PATH")
if action_path_env:
if action_path_env := os.getenv("GITHUB_ACTION_PATH"):

Comment on lines +80 to +82
if not normalized:
return False
return normalized in TRUTHY_STRINGS
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (code-quality): We've found these issues:

Suggested change
if not normalized:
return False
return normalized in TRUTHY_STRINGS
return False if not normalized else normalized in TRUTHY_STRINGS

if typ.TYPE_CHECKING: # pragma: no cover - imported for annotations only
from types import ModuleType

_ACTION_PATH = os.environ.get("GITHUB_ACTION_PATH")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (code-quality): Use named expression to simplify assignment and conditional (use-named-expression)



@app.command()
def main(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (code-quality): We've found these issues:


Explanation

The quality score for this function is below the quality threshold of 25%.
This score is a combination of the method length, cognitive complexity and working memory.

How can you solve this?

It might be worth refactoring this function to make it shorter and more readable.

  • Reduce the function length by extracting pieces of functionality out into
    their own functions. This is the most important thing you can do - ideally a
    function should be less than 10 lines.
  • Reduce nesting, perhaps by introducing guard clauses to return early.
  • Ensure that variables are tightly scoped, so that code using related concepts
    sits together within the function rather than being scattered.

Comment on lines +500 to +502
if name == "rustup":
return rustup_path
return None
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (code-quality): We've found these issues:

Suggested change
if name == "rustup":
return rustup_path
return None
return rustup_path if name == "rustup" else None

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant