Skip to content

lint gate: server-side ruff autofix for same-repo PRs#264

Draft
cailmdaley wants to merge 1 commit into
developfrom
lint-gate-autofix
Draft

lint gate: server-side ruff autofix for same-repo PRs#264
cailmdaley wants to merge 1 commit into
developfrom
lint-gate-autofix

Conversation

@cailmdaley

Copy link
Copy Markdown
Collaborator

What

Extends the develop lint gate so that, on a PR whose head branch lives in this repo, it doesn't just report ruff violations — it fixes the mechanical ones for you.

When the first ruff pass on such a PR isn't clean, the gate:

  1. runs ruff check --fix-only . + ruff format . over the PR head checkout,
  2. commits the diff as the github-actions[bot] and pushes it back to the PR branch (message: ruff autofix (format + safe lint fixes)),
  3. re-lints the fixed tree in the same run and gates on that:
    • fully clean → job green, PR comment: 🤖 autofix pushed <sha>, ruff is clean;
    • residual unsafe/judgement lint (e.g. F821 undefined name, unused vars) → job red, PR comment lists only the residual, noting the formatting/safe fixes were already pushed.

Fork PRs and direct develop pushes are unchanged (report-only: PR comment / lint-debt issue).

The model

  • Gated on github.event.pull_request.head.repo.full_name == github.repository — same-repo branches only, because only there can CI push back with the workflow token. Forks get the existing read-only behaviour.
  • Adds contents: write to permissions (scoped comment in the workflow). Checkout keeps persist-credentials: false; the push authenticates explicitly via x-access-token:${{ github.token }} in the remote URL.

Security (pull_request_target)

Running tooling over an untrusted PR tree is safe here because we run only ruff — a static analyzer that parses files but never imports/executes them — and we push back only the diff ruff itself produced. No PR-authored code runs with the write token. uvx --no-config still neutralizes any uv.toml/[tool.uv] in the PR tree.

Two subtleties worth flagging for review

  • Fork limitation. The autofix path can only fire on same-repo PRs; there is no way to push to a fork's branch from the base-repo workflow token. This is by design, and fork PRs keep the comment-based flow.
  • No retrigger. A GITHUB_TOKEN push does not trigger a new workflow run — so there's no recursion, but also no fresh CI on the bot commit. That's exactly why the gate re-lints and decides pass/fail within the same run on the fixed tree, rather than waiting for a rerun that never comes.

Verification

The gate runs the base branch's copy of the workflow, so this PR won't exercise its own changes. Verified instead by:

  • YAML parse — valid; step graph as intended.
  • Local simulation of the exact ruff command sequence (ruff@0.15.18, --no-config) against dirty fixtures, covering all three paths:
    • fully fixable (bad formatting + unused import) → autofix produces a diff, residual clean → residual_passed=true (green);
    • residual (bad formatting + F821 undefined name) → autofix pushes formatting/safe fixes, F821 survives → residual_passed=false, residual report lists only the F821 (red);
    • unfixable but already-formatted (F821 alone, clean formatting) → ruff produces no diff → pushed=false, gate falls back to the first-pass result with the original report (red).

Also updates the "Code style and the lint gate" section of CONTRIBUTING.md and the workflow's header comment block to describe the new model.

🤖 Generated with Claude Code

On a PR whose head branch lives in this repo, the lint gate now applies
ruff's own fixes (`ruff format` + `check --fix-only`), commits them as the
github-actions bot, and pushes them back to the PR branch, then re-lints the
fixed tree in the same run and gates on the residual. Fully clean -> green
with an "autofix pushed" comment; unsafe/judgement lint that survives ->
red, comment lists only the residual. Fork PRs and develop pushes keep their
existing report-only behaviour.

Safe under pull_request_target: only ruff (static, never executes PR code)
runs over the untrusted tree, and only ruff's own diff is pushed. Push uses
the workflow token explicitly (checkout keeps persist-credentials: false);
adds contents: write. A GITHUB_TOKEN push doesn't retrigger the workflow, so
the pass/fail is decided in this run rather than by a rerun that never comes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AAwoWLvWvNdt18RZJR2mek
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