Skip to content

Harden cross command construction#258

Open
leynos wants to merge 21 commits intomainfrom
feat/harden-cross-commands
Open

Harden cross command construction#258
leynos wants to merge 21 commits intomainfrom
feat/harden-cross-commands

Conversation

@leynos
Copy link
Copy Markdown
Owner

@leynos leynos commented Apr 27, 2026

Summary

This branch hardens rust-build-release cross command construction so cross cannot receive a +<toolchain> CLI override. The action continues to rely on rust-toolchain.toml or RUSTUP_TOOLCHAIN for cross builds, while cargo-only paths and cargo fallback commands still permit the existing +<toolchain> argument.

It also makes the repository check targets more resilient in hook environments by resolving Ruff and action-validator from known executable locations before falling back to command-name lookup.

No linked issue, roadmap task, or execplan was found for this branch.

Review walkthrough

Validation

  • make check-fmt: passed.
  • make typecheck: passed.
  • make lint: passed.
  • make test: passed, 669 passed, 90 skipped.
  • env PATH=/usr/bin:/bin HOME="$HOME" make check-fmt lint typecheck: passed.
  • Dry cross argv materialisation: cross build --manifest-path Cargo.toml --release --target aarch64-unknown-linux-gnu.

Notes

The PR is draft because it was opened by an agent and should receive maintainer review before it is marked ready.

Summary by Sourcery

Harden rust-build-release cross command construction to forbid passing toolchain overrides to cross while preserving cargo fallback behaviour, and make local lint tooling resolution more robust in constrained environments.

Enhancements:

  • Add a guard to ensure constructed cross commands never include a + override and log the final cross argv for debugging.
  • Ensure cargo fallback commands invoked after cross container errors retain any configured + argument.
  • Resolve Ruff and action-validator from common local installation paths before falling back to generic command invocations to better support restricted PATH setups.

Tests:

  • Add regression tests covering cross command construction, cargo fallback behaviour, and the cross command guard rejecting + overrides.

leynos added 2 commits April 27, 2026 20:29
Reject cross command vectors that accidentally inject a `+<toolchain>`
argument while preserving cargo fallback behaviour. Add regression tests for
normal cross command construction, the cargo fallback path, and the guard's
negative case.
Avoid requiring `uv`, `uvx`, or `action-validator` to be discoverable through
hook PATH settings. Prefer known local executable locations for Ruff and the
GitHub Action validator while keeping command-name fallbacks for other
developer environments.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 27, 2026

Note

Reviews paused

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Refactor action to build explicit argv lists for cross and cargo invocations; add GitHub Actions logging helpers; enforce rejecting +<toolchain> overrides for cross; centralise command assembly and error handling; add Hypothesis-backed regression tests and Makefile tool autodiscovery.

Changes

Cohort / File(s) Summary
Build Action Refactoring
.github/actions/rust-build-release/src/main.py
Construct explicit argv lists for cross and cargo; move formulate validation into helper; centralise command selection and error handling in _assemble_build_command; apply RUSTUP_TOOLCHAIN in the command environment when explicit_toolchain is set; log final argv with debug helper; convert cross +<toolchain> violations into GHA errors and exit.
GHA Logging Helpers
.github/actions/rust-build-release/src/gha.py
Add debug(), warning(), and error() functions that emit GitHub Actions workflow commands via a typed echo adapter, replacing ad-hoc typer.echo annotations.
Tests & Fixtures
.github/actions/rust-build-release/tests/test_commands.py, .github/actions/rust-build-release/tests/conftest.py, .github/actions/rust-build-release/tests/__snapshots__/test_commands.ambr
Add comprehensive pytest suite with Hypothesis checks for argv assembly and cross-toolchain rejection; refactor echo_recorder fixture into an _EchoRecorder instance for global typer.echo capture; add snapshot for debug output.
Container Fallback Handling
.github/actions/rust-build-release/src/main.py (fallback paths)
Change container-start failure reporting to use gha_warning/gha_debug; implement fallback cargo re-run preserving +<toolchain> only when appropriate; re-raise or exit depending on error code and requires_cross_container.
Tool Discovery & Dev Dependencies
Makefile, pyproject.toml
Implement candidate-path discovery for RUFF, ACTION_VALIDATOR, and MARKDOWNLINT with first-available fallback; update lint/fmt targets to use discovered binaries; add hypothesis and syrupy to dev dependency group.
Docs & Changelog
docs/developers-guide.md, CHANGELOG.md
Document argv construction, cross-toolchain validation guard, Makefile tool resolution, and record the refactor and tests in the changelog.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant CLI as Runner/CLI
  participant Main as action:main.py
  participant GHA as gha helper
  participant Exec as cross/cargo
  participant Container as Docker/container

  CLI->>Main: invoke with config (use_cross, toolchain, target)
  Main->>Main: _assemble_build_command(build_info)
  Main->>GHA: debug("final argv: ...")
  Main->>Exec: execute argv
  Exec-->>Main: ProcessExecutionError(retcode)
  alt retcode == 125 and requires_cross_container
    Main->>GHA: warning("container startup failed, falling back to cargo")
    Main->>Exec: execute fallback cargo argv (may include +toolchain)
  else other retcode
    Main->>GHA: error("cross command rejected +toolchain" / re-raise)
    Main->>CLI: exit(1)
  end
Loading

Poem

🔧 Arrange the argv lines precise and neat,
Cross toolchain overrides met their defeat,
Tests dance with Hypothesis, bold and spry,
GHA whispers debug, warning, error nigh,
Builds rerun, fallback preserved — then repeat.


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (2 errors, 6 warnings)

Check name Status Explanation Resolution
Testing (Overall) ❌ Error Test test_main_emits_error_annotation_when_cross_guard_raises uses only substring matching and does not verify the ::error:: GitHub Actions workflow command prefix, making it vacuous with respect to the stated behavioural change. Strengthen assertion to explicitly check error_lines[0].startswith("::error::") instead of substring matching, ensuring the test fails if gha_error() is replaced with plain typer.echo().
Unit Architecture ❌ Error PR violates architectural boundaries by binding dependencies at module-import time rather than at call sites, preventing tests from intercepting side-effects via monkeypatch. Inject the echo parameter explicitly at every gha_* call site in main.py, enabling tests to monkeypatch gha module attributes or pass mock echo factories.
User-Facing Documentation ⚠️ Warning New validation guard rejecting + overrides in cross commands was introduced but not documented in the action's primary user-facing README. Update .github/actions/rust-build-release/README.md to document the cross command validation constraint, explain toolchain selection must use the toolchain input or rust-toolchain.toml, and provide error examples and correct usage guidance.
Developer Documentation ⚠️ Warning Developer documentation omits MDLINT/MARKDOWNLINT_BASE tool discovery, gha module abstraction, environment binding pattern, and RUFF version pinning despite explicit review flags. Update docs/developers-guide.md to document MDLINT_CANDIDATES, MARKDOWNLINT_BASE, gha module's _Echo protocol, with_env() pattern, and pin RUFF via uv tool run --from ruff==.
Testing (Unit) ⚠️ Warning Unit tests fail to verify observable behaviour due to patching order issues and excessive internal mocking of gha functions. Patch gha_debug, gha_error, and gha_warning in the dynamically loaded module before invoking main() to capture actual workflow command output, and add true end-to-end tests with minimal mocking.
Testing (Compile-Time / Ui) ⚠️ Warning Snapshot testing implementation is incomplete: one debug output snapshot test is broken due to parameter binding at import time (flagged in review), and ::error:: and ::warning:: output rely on brittle substring assertions rather than snapshot tests. Restructure echo parameter binding to occur at call time rather than import time. Convert all GitHub Actions workflow command assertions (::debug::, ::error::, ::warning::) to snapshot tests with appropriate redaction of non-deterministic fields.
Domain Architecture ⚠️ Warning Domain logic is tightly coupled to infrastructure and adapter concerns; domain functions directly emit GitHub Actions commands, call typer functions, access environment variables, and manipulate configuration. Extract GitHub Actions annotation emission to CLI adapter layer, refactor domain functions to raise domain-specific exceptions, pass configuration explicitly as parameters, and move environment variable binding to main().
Observability ⚠️ Warning Logging lacks sufficient detail at critical failure boundaries; exit codes, ProcessExecutionError details, and fallback cargo failures are not logged. Log exit codes before raising typer.Exit, capture and log stdout/stderr from ProcessExecutionError, wrap fallback cargo in try-except with logging, and add correlation IDs for related log lines.
✅ Passed checks (10 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Harden cross command construction' directly captures the main objective: reinforcing cross command assembly to reject + overrides.
Description check ✅ Passed The description comprehensively covers the changeset: cross command hardening, Makefile tool resilience, validation summaries, and review guidance that aligns with the file-level summaries.
Docstring Coverage ✅ Passed Docstring coverage is 91.18% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Module-Level Documentation ✅ Passed All modules carry appropriate module-level docstrings and all module-level public functions and classes are fully documented with docstrings.
Testing (Property / Proof) ✅ Passed The PR implements property-based tests using Hypothesis to validate that cross commands must never contain + tokens.
Security And Privacy ✅ Passed Pull request introduces no security or privacy risks; command construction uses safe parameterised argv lists, environment variables are legitimate workflow inputs, and input validation follows secure practices throughout.
Performance And Resource Use ✅ Passed Core logic avoids nested loops and unbounded operations; I/O single-pass; runtime probes and toolchain queries bounded; Hypothesis tests with max_examples=40 mitigate resource risk despite missing explicit max_size constraints.
Concurrency And State ✅ Passed This pull request does not introduce concurrency, shared state, or synchronisation concerns. The code is entirely single-threaded and synchronous with no async/await, threading, locks, or multiprocessing constructs.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/harden-cross-commands

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

codescene-delta-analysis[bot]

This comment was marked as outdated.

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Apr 27, 2026

Reviewer's Guide

Hardens rust-build-release cross command construction to forbid passing + overrides to cross while keeping cargo fallbacks unchanged, and makes Makefile lint targets resolve Ruff and action-validator from known locations before falling back to PATH-based resolution.

Sequence diagram for hardened cross command construction and execution

sequenceDiagram
    participant Main as main
    participant CrossDecision as _CrossDecision
    participant BuildCross as _build_cross_command
    participant Guard as _assert_cross_command_has_no_toolchain_override
    participant Executor as plumbum_local_cross
    participant Cross as cross_process

    Main->>CrossDecision: compute_decision()
    alt decision_use_cross
        Main->>BuildCross: _build_cross_command(decision, target_to_build, manifest_path, features)
        BuildCross->>Executor: select_executor(cross_executable)
        BuildCross->>Guard: _assert_cross_command_has_no_toolchain_override(cmd)
        Guard-->>BuildCross: return
        BuildCross->>Executor: executor[cmd_slice]
        BuildCross-->>Main: build_cmd
        alt decision_has_toolchain_name
            Main->>Main: build_cmd.with_env(RUSTUP_TOOLCHAIN)
        end
        Main->>Main: typer.echo("::debug:: cross argv: {build_cmd}")
        Main->>Cross: execute(build_cmd)
    else decision_use_cargo
        Main->>Main: _build_cargo_command(...)
        Main->>Cross: execute(cargo_build_cmd)
    end
Loading

Class diagram for updated rust-build-release cross command helpers

classDiagram
    class main {
        +main()
    }

    class _CrossDecision {
        +cross_path: str | None
        +cargo_toolchain_spec: str | None
        +toolchain_name: str | None
    }

    class _build_cross_command {
        +_build_cross_command(decision, target_to_build, manifest_path, features) SupportsFormulate
    }

    class _assert_cross_command_has_no_toolchain_override {
        +_assert_cross_command_has_no_toolchain_override(cmd) None
    }

    class _build_cargo_command {
        +_build_cargo_command(cargo_toolchain_spec, target_to_build, manifest_argument, features) SupportsFormulate
    }

    class plumbum_local_executor {
        +__getitem__(argv_slice) SupportsFormulate
    }

    main --> _CrossDecision : uses
    main --> _build_cross_command : uses for cross builds
    main --> _build_cargo_command : uses for cargo fallback
    _build_cross_command --> _CrossDecision : reads
    _build_cross_command --> _assert_cross_command_has_no_toolchain_override : validates cmd
    _build_cross_command --> plumbum_local_executor : wraps cross_executable
Loading

File-Level Changes

Change Details Files
Disallow + overrides in cross commands and log constructed cross argv for debugging.
  • Add helper _assert_cross_command_has_no_toolchain_override to validate cross argv does not contain + arguments in the initial positions.
  • Refactor _build_cross_command to assemble a plain argv list, run it through the guard, then create the plumbum executor command from the validated arguments.
  • Emit a ::debug:: log line in main() with the final cross command before execution when using cross, while still supporting RUSTUP_TOOLCHAIN env overrides.
.github/actions/rust-build-release/src/main.py
Add regression tests covering cross command construction, cargo fallback behaviour, and the cross argv guard.
  • Introduce test_commands.py to load the main module in isolation for command-construction tests.
  • Add test ensuring _build_cross_command never injects + overrides and always starts with cross build.
  • Add test ensuring cross container error fallback invokes cargo with the original + override.
  • Add test ensuring the cross command guard raises when a + override is present in the guarded positions.
.github/actions/rust-build-release/tests/test_commands.py
Make Makefile lint/fmt targets robust to constrained PATHs by resolving Ruff and action-validator from common installation paths.
  • Define ACTION_VALIDATOR_CANDIDATES and ACTION_VALIDATOR variables that prefer user and system paths for action-validator before falling back to the bare command name.
  • Define RUFF_CANDIDATES and RUFF variables that prefer a project .venv, user-local, and system Ruff binaries, falling back to uv tool run ruff.
  • Update lint, fmt, and check-fmt targets to call $(RUFF) and $(ACTION_VALIDATOR) instead of hard-coded uvx ruff/action-validator invocations.
Makefile

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

@leynos leynos marked this pull request as ready for review April 27, 2026 18:45
sourcery-ai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

Scan the full cross argv after the executable for accidental `+<toolchain>`
overrides. Strengthen command construction tests to assert the expected
`cross build` prefix and cover cargo fallback behaviour when no toolchain
specification is configured.
codescene-delta-analysis[bot]

This comment was marked as outdated.

Use real executable permission bits when creating the fake cross binary in
command tests. Include Bun's global binary directory when resolving
`action-validator` for restricted hook environments.
codescene-delta-analysis[bot]

This comment was marked as outdated.

coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

Build the full cross argv, including normalised feature arguments, before
checking for accidental `+<toolchain>` overrides. This keeps the guard aligned
with the command vector passed to plumbum for execution.
codescene-delta-analysis[bot]

This comment was marked as outdated.

Tighten the no-toolchain fallback test to assert the complete cargo argv.
Omit the cargo toolchain slot when the specification is blank so fallback
commands do not carry an empty argument.
codescene-delta-analysis[bot]

This comment was marked as outdated.

coderabbitai[bot]

This comment was marked as resolved.

Build cargo commands from a single argv list before handing them to
plumbum so feature arguments are part of the exact command lookup.
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Apr 27, 2026

@coderabbitai resume

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 27, 2026

✅ Actions performed

Reviews resumed.

Make command wrapper warning output injectable and report cross argv
validation failures from main with a GitHub Actions error annotation.

Add property and unit coverage for cross command guards plus root development and changelog notes for the argv assembly pattern.
codescene-delta-analysis[bot]

This comment was marked as outdated.

Discover markdownlint from common Bun, local, and system install paths.
Limit the markdownlint target to Markdown files changed from the configured base so existing unrelated documentation debt does not block this PR gate.
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Apr 27, 2026

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

Complex Method

.github/actions/rust-build-release/src/main.py: main

What lead to degradation?

main increases in cyclomatic complexity from 10 to 12, threshold = 9

Why does this problem occur?

A Complex Method has a high cyclomatic complexity. The recommended threshold for the Python language is a cyclomatic complexity lower than 9.

How to fix it?

There are many reasons for Complex Method. Sometimes, another design approach is beneficial such as a) modeling state using an explicit state machine rather than conditionals, or b) using table lookup rather than long chains of logic. In other scenarios, the function can be split using EXTRACT FUNCTION. Just make sure you extract natural and cohesive functions. Complex Methods can also be addressed by identifying complex conditional expressions and then using the DECOMPOSE CONDITIONAL refactoring.

Helpful refactoring examples

To get a general understanding of what this code health issue looks like - and how it might be addressed - we have prepared some diffs for illustrative purposes.

SAMPLE

# complex_method.js
 function postItem(item) {
   if (!item.id) {
-    if (item.x != null && item.y != null) {
-      post(item);
-    } else {
-      throw Error("Item must have x and y");
-    }
+    // extract a separate function for creating new item
+    postNew(item);
   } else {
-    if (item.x < 10 && item.y > 25) {
-      put(item);
-    } else {
-      throw Error("Item must have an x and y value between 10 and 25");
-    }
+    // and one for updating existing items
+    updateItem(item);
   }
 }
+
+function postNew(item) {
+  validateNew(item);
+  post(item);
+}
+
+function updateItem(item) {
+  validateUpdate(item);
+  put(item);
+}
+

codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Apr 27, 2026

@coderabbitai have the following now been resolved?

.github/actions/rust-build-release/src/main.py (2)

693-724: ⚠️ Potential issue | 🟡 Minor
Keep container-engine cleanup on the validation-failure path.
Move the try/finally so it wraps command construction as well as execution. Right now Line 701 can raise typer.Exit before the cleanup block is entered, which leaves a temporary CROSS_CONTAINER_ENGINE behind if _configure_cross_container_engine() set one.

🔧 Patch
previous_engine, applied_engine = _configure_cross_container_engine(decision)
 
     manifest_argument = _manifest_argument(manifest_path)
-    if decision.use_cross:
-        try:
-            build_cmd = _build_cross_command(
-                decision, target_to_build, manifest_argument, features
-            )
-        except ValueError as exc:
-            typer.echo(
-                f"::error:: cross command validation failed for target "
-                f"'{target_to_build}': {exc}",
-                err=True,
-            )
-            raise typer.Exit(1) from None
-        if explicit_toolchain:
-            build_cmd = typ.cast("_SupportsEnvFormulate", build_cmd).with_env(
-                RUSTUP_TOOLCHAIN=toolchain_name
-            )
-        typer.echo(f"::debug:: cross argv: {build_cmd}")
-    else:
-        build_cmd = _build_cargo_command(
-            decision.cargo_toolchain_spec, target_to_build, manifest_argument, features
-        )
     try:
+        if decision.use_cross:
+            try:
+                build_cmd = _build_cross_command(
+                    decision, target_to_build, manifest_argument, features
+                )
+            except ValueError as exc:
+                typer.echo(
+                    f"::error:: cross command validation failed for target "
+                    f"'{target_to_build}': {exc}",
+                    err=True,
+                )
+                raise typer.Exit(1) from None
+            if explicit_toolchain:
+                build_cmd = typ.cast("_SupportsEnvFormulate", build_cmd).with_env(
+                    RUSTUP_TOOLCHAIN=toolchain_name
+                )
+            typer.echo(f"::debug:: cross argv: {build_cmd}")
+        else:
+            build_cmd = _build_cargo_command(
+                decision.cargo_toolchain_spec,
+                target_to_build,
+                manifest_argument,
+                features,
+            )
         run_cmd(build_cmd)
     except ProcessExecutionError as exc:
         _handle_cross_container_error(
             exc, decision, target_to_build, manifest_argument, features
         )
🤖 Prompt for AI Agents

118-142: 🛠️ Refactor suggestion | 🟠 Major
Separate query logic from output in _CommandWrapper.formulate().
Remove the _warn() calls from lines 126 and 138. The method is semantically a query returning argv, yet it performs I/O through the injected echo callable. This violates command-query separation—stringifying a wrapper should not emit warnings as a side effect, and code that only needs formulated argv (test doubles, internal logic) must not trigger unwanted output. Return a fallback value or raise instead, then let the caller that owns user-facing output emit the warning.

🤖 Prompt for AI Agents

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Apr 27, 2026

@coderabbitai have the following now been resolved?

.github/actions/rust-build-release/src/main.py (1)

540-554: 🧹 Nitpick | 🔵 Trivial
Construct cargo argv in a single pass.
Append normalized --features directly to cmd before executor[cmd] and remove post-construction mutation. Keep one canonical argv construction path and simplify assertion/debug flows.

Refactor patch
def _build_cargo_command(
     cargo_toolchain_spec: str, target_to_build: str, manifest_path: Path, features: str
 ) -> SupportsFormulate:
     executor = local["cargo"]
     cmd = [
         "build",
         "--manifest-path",
         str(manifest_path),
         "--release",
         "--target",
         target_to_build,
     ]
     if cargo_toolchain_spec:
         cmd.insert(0, cargo_toolchain_spec)
-    build_cmd = executor[cmd]
     normalized_features = _normalize_features(features)
     if normalized_features:
-        build_cmd = build_cmd["--features", normalized_features]
+        cmd.extend(["--features", normalized_features])
+    build_cmd = executor[cmd]
     return _CommandWrapper(build_cmd, "cargo")
🤖 Prompt for AI Agents

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 27, 2026

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
cat .github/actions/rust-build-release/src/main.py

Length of output: 24778


@leynos: Both issues are resolved in the current branch.

Issue 1 — container-engine cleanup on the validation-failure path

_configure_cross_container_engine is called immediately before a try/finally block that now encloses both _assemble_build_command and run_cmd(build_cmd). Because _assemble_build_command raises typer.Exit(1) (a SystemExit subclass) on a ValueError from the guard, and finally blocks execute unconditionally, _restore_container_engine is guaranteed to run on every exit path including validation failure. ✅

Issue 2 — _CommandWrapper.formulate() performing I/O

formulate() is now a pure query. Both previously-noisy branches (formulate_callable not callable; exception during call) return the fallback [self._display_name] with no call to _warn() or self._echo. The injected echo callable is used only by _warn(), which is not called from formulate(). ✅

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 27, 2026

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
grep -n "_build_cargo_command" .github/actions/rust-build-release/src/main.py
echo "---"
sed -n '530,570p' .github/actions/rust-build-release/src/main.py

Length of output: 1388


@leynos: Yes, this is resolved. The current _build_cargo_command assembles the complete cmd list — appending --features via cmd.extend(...) and inserting cargo_toolchain_spec at index 0 via cmd.insert(0, …) — before the single executor[cmd] call at the end. There is no post-construction mutation through plumbum subscript chaining (build_cmd["--features", …]). The canonical single-pass construction invariant is satisfied.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Apr 27, 2026

@coderabbitai have the following now been resolved?

Annul any requirements that violate the en-GB-oxendict spelling (-ize / -yse / -our) conventions (for example a request to replace "normalize" with "normalise" or "artefact" with "artifact"), or where the requirement unnecessarily increases cyclomatic complexity.

Please address the comments from this code review:

## Overall Comments
- The `_assert_cross_command_has_no_toolchain_override` helper currently only inspects `cmd[1:3]`, so a `+<toolchain>` injected later in the argv (e.g. by future changes) would be missed; consider checking all arguments except the executable (e.g. `cmd[1:]`) to make the guard more robust.

## Individual Comments

### Comment 1
<location path=".github/actions/rust-build-release/src/main.py" line_range="504-513" />
<code_context>
     return ",".join(normalized)


+def _assert_cross_command_has_no_toolchain_override(cmd: cabc.Sequence[object]) -> None:
+    # Cross must not be given a +<toolchain>; rely on rust-toolchain.toml /
+    # rustup override.
</code_context>
<issue_to_address>
**issue (bug_risk):** The toolchain override check only inspects `cmd[1:3]`, which may miss `+toolchain` arguments in other positions.

Because the current construction puts "build" and "--manifest-path" at `cmd[1]` and `cmd[2]`, this guard will never see a `+<toolchain>` argument, and future flags/args could further shift positions. To reliably enforce that no `+<toolchain>` is passed, consider scanning all elements after the executable, e.g. `cmd[1:]`, rather than only `cmd[1:3]`.
</issue_to_address>

### Comment 2
<location path=".github/actions/rust-build-release/tests/test_commands.py" line_range="51-60" />
<code_context>
+def test_build_cross_command_never_injects_toolchain_override(
</code_context>
<issue_to_address>
**suggestion (testing):** Strengthen the cross command construction test to assert the full argv shape, not just the absence of `+toolchain`

The regression test effectively guards against `+toolchain` being added to the cross argv, but the docstring also guarantees that commands “start with cross build”. Currently we only check `parts[0] == "cross"`. Please also assert the initial slice of the command (e.g. `parts[:3] == ["cross", "build", "--manifest-path"]`) or at least that `parts[1] == "build"`, so the test better enforces the expected `cross build ...` invocation and is more robust to accidental changes in verb or argument ordering.

Suggested implementation:

```python
def test_build_cross_command_never_injects_toolchain_override(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    """Cross commands start with `cross build` and omit +toolchain arguments."""
    main_module = _load_main_module(monkeypatch)
    cross_path = _make_cross_executable(tmp_path)
    decision = main_module._CrossDecision(
        cross_path=str(cross_path),
        cross_version="0.2.5",
        use_cross=True,

```

```python
    # existing construction of the command, e.g.
    parts = decision.build_command(...)
    # Ensure we are invoking `cross build` with the expected leading arguments,
    # not just `cross` with an arbitrary verb.
    assert parts[:3] == ["cross", "build", "--manifest-path"]
    assert "+toolchain" not in parts

```

I only see the beginning of the test, so you will need to:

1. Adjust the `SEARCH` block for the body of the test to match your actual command construction (e.g. if it uses a different variable name than `parts`, or calls a different helper than `decision.build_command(...)`).
2. If the argument ordering differs (for example, if `--manifest-path` is not the third element), update the slice assertion accordingly—for instance, you can switch to:
   - `assert parts[0] == "cross"; assert parts[1] == "build"` if you prefer not to pin the flag order, or
   - `assert parts[:2] == ["cross", "build"]` plus any additional checks as appropriate.
3. Ensure the negative assertion matches your actual toolchain flag string (e.g. `"+nightly"` vs `"+toolchain"`), based on what the test is guarding against.
</issue_to_address>

### Comment 3
<location path=".github/actions/rust-build-release/tests/test_commands.py" line_range="83-92" />
<code_context>
+def test_cross_container_fallback_keeps_cargo_toolchain_override(
</code_context>
<issue_to_address>
**suggestion (testing):** Add a variant of the fallback test for when no cargo toolchain override is configured

Please also add a complementary test case where `cargo_toolchain_spec` is empty/`None`, and assert that the fallback command contains no `+<toolchain>` segment. That will verify the fallback behavior when no toolchain override is configured.

Suggested implementation:

```python
def test_cross_container_fallback_keeps_cargo_toolchain_override(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    """The container-error fallback remains cargo-only and keeps +toolchain."""
    main_module = _load_main_module(monkeypatch)
    calls: list[list[str]] = []

    def fake_run_cmd(cmd: object) -> None:
        formulated = list(cmd.formulate())
        if formulated:

```

You’ll need to add a new test function adjacent to `test_cross_container_fallback_keeps_cargo_toolchain_override` that mirrors its structure but clears the toolchain override before triggering the fallback. The outline should look like this (adapt names/attributes to match your actual code):

```python
def test_cross_container_fallback_without_cargo_toolchain_override(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    """The container-error fallback remains cargo-only and uses no +toolchain when not configured."""
    main_module = _load_main_module(monkeypatch)
    calls: list[list[str]] = []

    # Ensure there is no toolchain override configured for this test case.
    # Adjust this to match how your module stores the override.
    # Examples:
    #   main_module.CARGO_TOOLCHAIN_SPEC = ""
    #   main_module.CARGO_TOOLCHAIN_SPEC = None
    #   main_module.config.cargo_toolchain_spec = None
    main_module.CARGO_TOOLCHAIN_SPEC = ""

    def fake_run_cmd(cmd: object) -> None:
        formulated = list(cmd.formulate())
        if formulated and formulated[0] == "cross":
            # Simulate a container failure to force the fallback path.
            calls.append(formulated)
            raise main_module.subprocess.CalledProcessError(1, formulated)
        calls.append(formulated)

    monkeypatch.setattr(main_module, "run_cmd", fake_run_cmd)

    cmd = main_module.build_release_command(
        "aarch64-unknown-linux-gnu",
        tmp_path / "Cargo.toml",
        "",
    )

    # Run and expect the simulated failure, which should trigger the fallback logic.
    with pytest.raises(main_module.subprocess.CalledProcessError):
        main_module.run_cmd(cmd)

    # Verify that at least one fallback cargo invocation was produced.
    fallback_calls = [c for c in calls if c and c[0] == "cargo"]
    assert fallback_calls

    # And that those fallback commands do not contain any +toolchain overrides.
    for call in fallback_calls:
        assert_no_toolchain_override(call)
```

Concretely:
1. Copy the same setup/fake run machinery used in `test_cross_container_fallback_keeps_cargo_toolchain_override` so that you hit the **same** fallback behavior.
2. Right after `_load_main_module`, explicitly clear the toolchain override in whatever way is correct for your module (e.g. set `CARGO_TOOLCHAIN_SPEC` to `""` or `None`, or update a config object).
3. After triggering the fallback, use the existing `assert_no_toolchain_override` helper (or an equivalent assertion) on the captured fallback cargo command to ensure there is no `+<toolchain>` segment in the command line.
</issue_to_address>

@leynos

This comment was marked as resolved.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 27, 2026

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "===== _assert_cross_command_has_no_toolchain_override ====="
grep -n "_assert_cross_command_has_no_toolchain_override" .github/actions/rust-build-release/src/main.py
echo "---"
sed -n '504,525p' .github/actions/rust-build-release/src/main.py

echo ""
echo "===== test_build_cross_command_never_injects_toolchain_override ====="
grep -n "test_build_cross_command_never_injects_toolchain_override" .github/actions/rust-build-release/tests/test_commands.py
echo "---"
# Get the full function body
awk '/^def test_build_cross_command_never_injects_toolchain_override/,/^def /' .github/actions/rust-build-release/tests/test_commands.py | head -40

echo ""
echo "===== fallback tests ====="
grep -n "def test_cross_container_fallback" .github/actions/rust-build-release/tests/test_commands.py
echo "---"
awk '/^def test_cross_container_fallback/,/^def /' .github/actions/rust-build-release/tests/test_commands.py | head -80

Length of output: 1547


🏁 Script executed:

#!/bin/bash
echo "===== test_build_cross_command_never_injects_toolchain_override body ====="
sed -n '93,140p' .github/actions/rust-build-release/tests/test_commands.py

echo ""
echo "===== test_cross_container_fallback_cargo_toolchain body ====="
sed -n '424,510p' .github/actions/rust-build-release/tests/test_commands.py

Length of output: 4351


@leynos: All three issues are resolved. No requirements needed annulling — none of the three comments touched en-GB spelling or introduced additional branching.

Comment Resolved? Evidence
Overall + Comment 1 — guard scans cmd[1:] ✅ Yes _assert_cross_command_has_no_toolchain_override now uses any(… for arg in cmd[1:]), scanning every argument after the executable with no position cap.
Comment 2 — assert full argv shape ✅ Yes test_build_cross_command_never_injects_toolchain_override asserts parts[:3] == ["cross", "build", "--manifest-path"] in addition to assert_no_toolchain_override(parts).
Comment 3 — complementary fallback test for empty toolchain spec ✅ Yes The parametrised test_cross_container_fallback_cargo_toolchain (applied from the prior exchange) covers both cargo_toolchain_spec="+bogus-nightly" and cargo_toolchain_spec="", asserting in each case that calls == [expected] where expected for the empty spec contains no +… token.

@coderabbitai

This comment was marked as resolved.

Move workflow command formatting into a small `gha` adapter and keep
`_CommandWrapper` focused on command display behaviour. Add snapshot and
entrypoint coverage for cross debug and validation annotations, and move
the developer guide under `docs/`.
codescene-delta-analysis[bot]

This comment was marked as outdated.

@coderabbitai

This comment was marked as resolved.

Use `PurePosixPath` for the snapshot manifest fixture so the recorded
cross argv stays identical on Windows and Unix hosts.
codescene-delta-analysis[bot]

This comment was marked as outdated.

Keep `_build_cargo_command` as a pure command builder and emit the cargo
argv debug line from `_assemble_build_command` after construction.
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Apr 27, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 27, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are 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: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/actions/rust-build-release/src/main.py:
- Around line 634-663: The function _assemble_build_command currently declares
an unused parameter toolchain_name; remove toolchain_name from the function
signature and update all call sites (e.g., the invocation in main() that passes
toolchain_name) to stop passing that argument, adjust the type hints/signature
documentation if present, and run a quick static check to ensure no other
references remain to toolchain_name in _assemble_build_command,
_build_cross_command, or related callers.

In @.github/actions/rust-build-release/tests/conftest.py:
- Around line 246-262: Add a clear class-level docstring to _EchoRecorder that
explains its dual-purpose design: that the class itself subclasses list[str] to
capture global echo messages, while calling _EchoRecorder(module) (the __call__
method) patches module.typer.echo and returns a separate list[tuple[str, bool]]
of (message, err) pairs for module-scoped captures with stderr tracking; mention
key symbols (_EchoRecorder, __call__, fake_echo, and monkeypatch) so maintainers
know where to look and how the two capture modes differ and interact.

In @.github/actions/rust-build-release/tests/test_commands.py:
- Around line 50-72: The conditional cleanup of packaging.version in
_load_main_module should be extracted into a small context manager (e.g.,
_preserve_packaging_version) so the restore logic is reusable and clearer;
implement _preserve_packaging_version to import packaging and packaging.version,
yield, and in its finally block delete packaging.version only if it still equals
the captured packaging_version, then call spec.loader.exec_module(module) inside
a with _preserve_packaging_version(): block in _load_main_module and remove the
inline finally block.
- Around line 138-144: The function injecting_guard currently defines an unused
parameter cmd which triggers ARG001; rename the parameter to _cmd (i.e., change
def injecting_guard(cmd: object) -> None: to def injecting_guard(_cmd: object)
-> None:) so the intent to ignore the argument is explicit; update the
monkeypatch.setattr reference remains the same for
_assert_cross_command_has_no_toolchain_override and no other behavior changes.

In `@Makefile`:
- Around line 75-76: The Makefile target using MARKDOWNLINT_BASE calls git diff
which errors when the base (e.g., origin/main) doesn't exist; fix by guarding or
defaulting MARKDOWNLINT_BASE before running the git diff command: add a short
check that verifies the ref (use git rev-parse --verify) and if it fails set
MARKDOWNLINT_BASE to a safe fallback (like empty string or HEAD) so the pipeline
`git diff --name-only --diff-filter=ACMRT $(MARKDOWNLINT_BASE) -- '*.md' | xargs
... $(MDLINT)` won't exit with non-zero; update the Makefile variable/recipe
that references MARKDOWNLINT_BASE and keep the MDLINT usage unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 14460415-ed2c-459f-ae05-53e5ba3fbf20

📥 Commits

Reviewing files that changed from the base of the PR and between 3fc35ef and 07a4264.

📒 Files selected for processing (8)
  • .github/actions/rust-build-release/src/gha.py
  • .github/actions/rust-build-release/src/main.py
  • .github/actions/rust-build-release/tests/__snapshots__/test_commands.ambr
  • .github/actions/rust-build-release/tests/conftest.py
  • .github/actions/rust-build-release/tests/test_commands.py
  • Makefile
  • docs/developers-guide.md
  • pyproject.toml

Comment thread .github/actions/rust-build-release/src/main.py Outdated
Comment thread .github/actions/rust-build-release/tests/conftest.py
Comment thread .github/actions/rust-build-release/tests/test_commands.py
Comment thread .github/actions/rust-build-release/tests/test_commands.py Outdated
Comment thread Makefile Outdated
Remove the unused `_assemble_build_command` parameter, clarify echo
recorder capture modes, extract packaging import cleanup for command tests,
and make markdown lint tolerate missing diff bases.
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Apr 27, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 27, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Apr 27, 2026

@coderabbitai Have the following failed checks now been resolved?

If further work is required, please provide an AI agent prompt for the remaining work to be done to address these failures.

Do not treat warnings as optional or aspirational.

❌ Failed checks (2 errors, 5 warnings)

Check name Status Explanation Resolution
Testing (Overall) ❌ Error Tests for cross command guard are vacuous, never exercising the guard's actual integration into command construction during normal execution. Restructure tests to verify the guard is genuinely invoked during _build_cross_command execution via call-count inspection or end-to-end testing without mocking intermediate functions.
Unit Architecture ❌ Error The code violates separation of concerns by embedding side-effects (gha_debug, gha_error, typer.Exit) within query functions, mixing command assembly with error annotation emission and exit control. Extract side-effect emission from _assemble_build_command() to make it a pure query; inject echo parameters at composition boundaries; rename _handle_cross_container_error() to clarify orchestration responsibilities; update tests to assert on side-effect emission.
User-Facing Documentation ⚠️ Warning PR introduces user-facing behavioural change rejecting + overrides in cross commands, but documents this constraint only internally, not in user-facing README. Update .github/actions/rust-build-release/README.md to document the toolchain override constraint and explain correct toolchain specification methods.
Developer Documentation ⚠️ Warning Developer documentation covers argv assembly, validation guard, and Makefile discovery, but omits gha module, helper functions, error handling flow, test dependencies rationale, and _CommandWrapper refactoring. Expand docs/developers-guide.md to document gha module's role, _assemble_build_command and _check_target_support helpers, ValueError-to-error annotation flow, _CommandWrapper refactoring, and test strategy including Hypothesis and syrupy rationale.
Testing (Unit) ⚠️ Warning Unit test suite lacks dedicated tests for _build_cargo_command and core functional paths, relying excessively on mocks that obscure actual behaviour. Add unit tests for _build_cargo_command argv assembly, refactor mocked tests to exercise real logic, and create end-to-end tests for _handle_cross_container_error fallback invocation.
Domain Architecture ⚠️ Warning Domain functions dishonestly emit framework and adapter side-effects (typer.Exit, gha_error, typer.echo) whilst being named as pure operations, violating separation of concerns. Move all framework and adapter concerns to main(): extract gha_error/warning calls from domain helpers, return error states as domain values, and handle typer.Exit() only at the boundary.
Observability ⚠️ Warning The PR introduces meaningful logging at error decision points but omits observability signals for build success, fallback completion, non-container error context, and operational metrics. Wrap non-container ProcessExecutionError re-raises with gha_error() annotations. Log confirmation when build and fallback cargo execution complete. Implement metrics tracking for fallback invocations and outcomes.

@coderabbitai

This comment was marked as resolved.

Return command assembly errors as values so `main` owns annotations and
process exits. Add guard spy coverage, cargo argv-shape tests, completion
logging, and document cross toolchain selection.
codescene-delta-analysis[bot]

This comment was marked as outdated.

Treat `subprocess.CalledProcessError` the same as plumbum process failures
when cargo-based cross installation fails, so Windows fallback handling can
continue without cross as intended.
Copy link
Copy Markdown

@codescene-delta-analysis codescene-delta-analysis Bot left a comment

Choose a reason for hiding this comment

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

Gates Failed
Enforce advisory code health rules (1 file with Excess Number of Function Arguments)

Gates Passed
5 Quality Gates Passed

See analysis details in CodeScene

Reason for failure
Enforce advisory code health rules Violations Code Health Impact
test_commands.py 1 advisory rule 9.69 Suppress

Quality Gate Profile: Pay Down Tech Debt
Install CodeScene MCP: safeguard and uplift AI-generated code. Catch issues early with our IDE extension and CLI tool.

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: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/actions/rust-build-release/tests/conftest.py:
- Around line 261-269: echo_recorder.__call__ currently only replaces
module.typer.echo so emitted workflow commands sent via gha_debug, gha_warning,
and gha_error (which capture typer.echo at import time) are not recorded; update
__call__ to also patch the imported emitters by setting module.gha_debug.echo =
fake_echo, module.gha_warning.echo = fake_echo, and module.gha_error.echo =
fake_echo (or replace their callables to route through fake_echo) so messages
from gha_debug/gha_warning/gha_error are captured; alternatively remove the
callable capture mode and enforce global capture before importing the module
under test so those emitters bind to the patched echo.

In @.github/actions/rust-build-release/tests/test_commands.py:
- Around line 466-474: Replace the loose substring check for the validation
error with an exact GitHub Actions annotation assertion: after calling
main_module.main("aarch64-unknown-linux-gnu", "") and catching typer.Exit,
locate the recorded line in echo_recorder that exactly starts with the
"::error::" prefix and matches the full failure message produced by the code
(e.g. "::error::cross command validation failed: aarch64-unknown-linux-gnu");
assert that such a line exists and contains the target string so the test
verifies the exact ::error:: workflow annotation produced by main_module.main.

In `@Makefile`:
- Around line 17-19: Update docs/developers-guide.md to match the Makefile
changes: document the new MARKDOWNLINT_BASE variable and its role as the
diff-base/fallback, explain how MDLINT_CANDIDATES is discovered (checks
HOME/.bun/bin/markdownlint, HOME/.local/bin/markdownlint,
/usr/local/bin/markdownlint, /usr/bin/markdownlint) and that MDLINT chooses the
first found candidate or falls back to the literal "markdownlint"; also mirror
this explanation where the doc previously described the older behavior (the
section that discussed markdownlint discovery/diff-base) so the docs no longer
drift from the Makefile.
- Line 24: Add a deterministic Ruff version and use it for the fallback:
introduce a RUFF_VERSION variable (e.g., RUFF_VERSION ?= 0.11.13) and update the
RUFF fallback assignment so the fallback uses `uv tool run --from
ruff==$(RUFF_VERSION) ruff` instead of unpinned `uv tool run ruff`; update the
existing RUFF ?= ... line and add the RUFF_VERSION ?= ... declaration near it to
ensure consistent lint/format behavior across runs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 7c92d9d1-3121-49bd-a7bb-561714f30541

📥 Commits

Reviewing files that changed from the base of the PR and between 07a4264 and f8e4d6e.

📒 Files selected for processing (4)
  • .github/actions/rust-build-release/src/main.py
  • .github/actions/rust-build-release/tests/conftest.py
  • .github/actions/rust-build-release/tests/test_commands.py
  • Makefile

Comment on lines +261 to +269
def __call__(self, module: ModuleType) -> list[tuple[str, bool]]:
"""Patch *module*.typer.echo to record (message, err) tuples."""
messages: list[tuple[str, bool]] = []

def fake_echo(message: str, *, err: bool = False) -> None:
messages.append((message, err))

self._monkeypatch.setattr(module.typer, "echo", fake_echo)
return messages
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Callable echo_recorder() usages:"
rg -n '\becho_recorder\s*\(' .github/actions/rust-build-release/tests

echo
echo "main.py gha imports:"
rg -n 'from gha import (debug|error|warning)' .github/actions/rust-build-release/src/main.py

echo
echo "gha helper definitions:"
rg -n 'def (debug|warning|error)\(' .github/actions/rust-build-release/src/gha.py -A2 -B1

Repository: leynos/shared-actions

Length of output: 1728


Patch the gha_* emitters in module-scoped capture mode.

echo_recorder(module) only swaps module.typer.echo. main.py now emits through gha_debug, gha_warning, and gha_error, which bind echo=typer.echo at import time. Tests using callable mode cease to observe workflow-command output on this changed code path. Eight test cases are affected. Patch the imported gha_* callsites, or remove callable mode and enforce global capture before importing the module under test.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/actions/rust-build-release/tests/conftest.py around lines 261 - 269,
echo_recorder.__call__ currently only replaces module.typer.echo so emitted
workflow commands sent via gha_debug, gha_warning, and gha_error (which capture
typer.echo at import time) are not recorded; update __call__ to also patch the
imported emitters by setting module.gha_debug.echo = fake_echo,
module.gha_warning.echo = fake_echo, and module.gha_error.echo = fake_echo (or
replace their callables to route through fake_echo) so messages from
gha_debug/gha_warning/gha_error are captured; alternatively remove the callable
capture mode and enforce global capture before importing the module under test
so those emitters bind to the patched echo.

Comment on lines +466 to +474
with pytest.raises(main_module.typer.Exit) as exc_info:
main_module.main("aarch64-unknown-linux-gnu", "")

assert exc_info.value.exit_code == 1
error_lines = [
line for line in echo_recorder if "cross command validation failed" in line
]
assert len(error_lines) == 1
assert "aarch64-unknown-linux-gnu" in error_lines[0]
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.

⚠️ Potential issue | 🟠 Major

Assert the full ::error:: workflow command here.

The current substring check still passes if this branch stops emitting a GitHub Actions annotation and falls back to plain typer.echo with the same text. Assert the exact ::error:: prefix and failure message so this test actually locks the contract the PR claims to cover.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/actions/rust-build-release/tests/test_commands.py around lines 466 -
474, Replace the loose substring check for the validation error with an exact
GitHub Actions annotation assertion: after calling
main_module.main("aarch64-unknown-linux-gnu", "") and catching typer.Exit,
locate the recorded line in echo_recorder that exactly starts with the
"::error::" prefix and matches the full failure message produced by the code
(e.g. "::error::cross command validation failed: aarch64-unknown-linux-gnu");
assert that such a line exists and contains the target string so the test
verifies the exact ::error:: workflow annotation produced by main_module.main.

Comment thread Makefile
Comment on lines +17 to +19
MARKDOWNLINT_BASE ?= origin/main
MDLINT_CANDIDATES := $(wildcard $(HOME)/.bun/bin/markdownlint $(HOME)/.local/bin/markdownlint /usr/local/bin/markdownlint /usr/bin/markdownlint)
MDLINT ?= $(if $(MDLINT_CANDIDATES),$(firstword $(MDLINT_CANDIDATES)),markdownlint)
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.

⚠️ Potential issue | 🟡 Minor

Synchronize developer documentation with markdownlint behaviour.

Update docs/developers-guide.md to document MDLINT candidate discovery and MARKDOWNLINT_BASE/diff-base fallback behaviour introduced on Lines 17-19 and Lines 75-77. Remove the current doc/code drift.

Also applies to: 75-77

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Makefile` around lines 17 - 19, Update docs/developers-guide.md to match the
Makefile changes: document the new MARKDOWNLINT_BASE variable and its role as
the diff-base/fallback, explain how MDLINT_CANDIDATES is discovered (checks
HOME/.bun/bin/markdownlint, HOME/.local/bin/markdownlint,
/usr/local/bin/markdownlint, /usr/bin/markdownlint) and that MDLINT chooses the
first found candidate or falls back to the literal "markdownlint"; also mirror
this explanation where the doc previously described the older behavior (the
section that discussed markdownlint discovery/diff-base) so the docs no longer
drift from the Makefile.

Comment thread Makefile
ACTION_VALIDATOR_CANDIDATES := $(wildcard $(HOME)/.cargo/bin/action-validator $(HOME)/.bun/bin/action-validator /usr/local/bin/action-validator /usr/bin/action-validator)
ACTION_VALIDATOR ?= $(if $(ACTION_VALIDATOR_CANDIDATES),$(firstword $(ACTION_VALIDATOR_CANDIDATES)),action-validator)
RUFF_CANDIDATES := $(wildcard $(CURDIR)/.venv/bin/ruff $(HOME)/.local/bin/ruff /usr/local/bin/ruff /usr/bin/ruff)
RUFF ?= $(if $(RUFF_CANDIDATES),$(firstword $(RUFF_CANDIDATES)),uv tool run ruff)
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

uv tool run unpinned package version resolution documentation

💡 Result:

When using uv tool run (or its alias uvx) without specifying a version (i.e., unpinned tool name like ruff), uv resolves and installs the latest compatible version of the tool package and its dependencies into a temporary, cached virtual environment isolated from any project. By default, uv's resolution strategy prefers the highest (latest) compatible version for each package, both direct (the tool) and transitive (its dependencies). This is the standard behavior across uv commands like uv pip install, uv run --with, and uv tool run. For uv tool run specifically: - On first run, it performs full resolution to select the latest versions satisfying the tool's declared requirements. - Subsequent runs reuse the cached environment unless the cache is pruned/refreshed, a different version is requested (e.g., ruff@latest or --from ruff==0.3.0), or --isolated is used. - The cache is disposable (in ~/.cache/uv), unlike installed tools via uv tool install which are persistent. To override the default "highest" resolution (e.g., for testing lowest versions), use the --resolution flag, which applies to uv tool run as it inherits from uv's run interface: - --resolution lowest: Lowest compatible versions for all dependencies. - --resolution lowest-direct: Lowest for direct (tool), highest for transitive. Example: uv tool run --resolution lowest-direct ruff check . Official documentation confirms uv resolves unpinned requirements to latest versions by default, with configurable strategies via --resolution. For reproducible exact versions, specify the tool version explicitly (e.g., uvx ruff@0.3.0) or use uv tool install first. No dedicated lockfile for tools; caching provides reproducibility until refreshed.

Citations:


Pin the Ruff fallback version.

The unpinned uv tool run ruff fallback resolves to the latest compatible version of Ruff on first run and reuses the cached environment thereafter. However, uv's cache is disposable, meaning the fallback can drift to newer Ruff releases across CI and local runs, destabilising lint and format output. Specify an explicit version using --from ruff==<version> to ensure deterministic behaviour.

Apply a deterministic Ruff fallback
+RUFF_VERSION ?= 0.11.13
-RUFF ?= $(if $(RUFF_CANDIDATES),$(firstword $(RUFF_CANDIDATES)),uv tool run ruff)
+RUFF ?= $(if $(RUFF_CANDIDATES),$(firstword $(RUFF_CANDIDATES)),uv tool run --from ruff==$(RUFF_VERSION) ruff)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Makefile` at line 24, Add a deterministic Ruff version and use it for the
fallback: introduce a RUFF_VERSION variable (e.g., RUFF_VERSION ?= 0.11.13) and
update the RUFF fallback assignment so the fallback uses `uv tool run --from
ruff==$(RUFF_VERSION) ruff` instead of unpinned `uv tool run ruff`; update the
existing RUFF ?= ... line and add the RUFF_VERSION ?= ... declaration near it to
ensure consistent lint/format behavior across runs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant