Skip to content

Ruff fix functions return successful Changesets after Ruff exits with a usage error #99

Closed

Description

@Kewe63

Summary

At b0e9d0ca81328d3c91a08ab8c7a0e8963c2fa7ae, both RuffChecker.fix and RuffFormatter.fix suppress nonzero Ruff execution and return a Changeset.

Expected behavior

An auto-fix call must fail when Ruff itself fails with a usage, configuration, or runtime error. Normal lint/fix findings are distinct from Ruff process failure.

Actual behavior

Both methods use expect=dagger.ReturnType.ANY, await only combined output, print the error, and construct a Changeset without checking the exit status.

Reproduction

PYTHONPATH=repositories/daggerverse/github/src:repositories/daggerverse/ruff/src:repositories/daggerverse/uv/src \
  evidence/daggerverse/venv314/bin/pytest -q \
  evidence/daggerverse/repros/test_candidates.py -k ruff_fix

Observed: both RED cases fail with Failed: DID NOT RAISE after Ruff's error: unexpected argument '--definitely-invalid' output. Independent Ruff 0.16.0 probes return exit 2 for the same invalid commands.

The paired exit-0 controls return Changesets successfully.

Impact

Misspelled or unsupported extra_args, invalid configuration, and Ruff runtime failures can look like successful auto-fix operations, potentially yielding empty or partial Changesets.

Root cause

  • ruff/src/ruff/checker.py:57-64
  • ruff/src/ruff/formatter.py:57-64

Both opt out of Dagger's default fail-closed process handling and never restore an exit-status check.

Suggested fix and regression test

Use normal fail-closed execution, or explicitly inspect and reject nonzero status before constructing the Changeset. Parameterize checker/formatter with exit 2 and exit 0 controls.

Duplicate check

The complete tracker, comments, reviews, timelines, and open patches were inspected. Fresh searches covered exact error text, both class methods, ReturnType.ANY, exit 2, failure suppression, and fail-closed synonyms. PRs #34, #36, and #38 are adjacent generator/exit-code history but do not mention or fix this Ruff behavior.

Activity

  1. aoright commented on Sep 24, 2026

    @aoright

    I implemented and verified a targeted fix for this issue on fork branch fix/ruff-fix-exit-code (commit ac3baf0).

    Compare: main...aoright:daggerverse:fix/ruff-fix-exit-code

    1. Root Cause & Ruff Exit Code Semantics

    • expect=dagger.ReturnType.ANY was introduced so that ruff check --fix does not fail Dagger's execution when unfixable lint violations remain (which Ruff signals via exit code 1 while still applying auto-fixes to disk).
    • However, Ruff adheres to standard CLI conventions:
      • Exit 0: Success (clean / all fixes applied).
      • Exit 1: Lint violations remain (only possible in check).
      • Exit 2+: Command-line usage errors (e.g. invalid/unsupported extra_args), invalid configuration, syntax parsing crashes, or internal runtime failure.
    • Similarly, ruff format (without --check) succeeds on exit 0; any non-zero exit code represents a failure.
    • By omitting an explicit exit code check after result = ctr.with_exec(..., expect=dagger.ReturnType.ANY), exit code 2 errors were suppressed and resulted in false-success Changeset objects.

    2. Implementation

    In ruff/src/ruff/checker.py:

            ctr = self._container(source)
            result = ctr.with_exec(args, expect=dagger.ReturnType.ANY)
            output = await result.combined_output()
            if output.strip():
                sys.stderr.write(output)
            exit_code = await result.exit_code()
            if exit_code > 1:
                msg = f"Ruff check failed with exit code {exit_code}:\n{output}"
                raise RuntimeError(msg)
            before = dag.directory().with_directory(".", source)
            fixed = result.directory("/work").without_directory(".ruff_cache")
            after = dag.directory().with_directory(".", fixed)
            return after.changes(before)

    In ruff/src/ruff/formatter.py:

            ctr = self._container(source)
            result = ctr.with_exec(args, expect=dagger.ReturnType.ANY)
            output = await result.combined_output()
            if output.strip():
                sys.stderr.write(output)
            exit_code = await result.exit_code()
            if exit_code != 0:
                msg = f"Ruff format failed with exit code {exit_code}:\n{output}"
                raise RuntimeError(msg)
            before = dag.directory().with_directory(".", source)
            fixed = result.directory("/work").without_directory(".ruff_cache")
            after = dag.directory().with_directory(".", fixed)
            return after.changes(before)

    Verified with ruff check and ruff format (0 findings).

  2. aoright commented on Sep 24, 2026

    @aoright

    Opened PR #106 with the zero-regression fix and test coverage.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions