fix(scripts): run ruff format check and lock-bound ruff in lint:py - #2709
fix(scripts): run ruff format check and lock-bound ruff in lint:py#2709Bill Berry (WilliamBerryiii) wants to merge 1 commit into
Conversation
- add lock-aware ruff resolution honoring each project's uv.lock - run ruff check and ruff format --check in the default lint lane - extend the owning Pester suites and correct runner docs - use uv sync --locked for devcontainer skill provisioning Closes #2694 ✅ - Generated by Copilot
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2709 +/- ##
==========================================
- Coverage 82.65% 82.64% -0.01%
==========================================
Files 166 166
Lines 22214 22200 -14
Branches 30 30
==========================================
- Hits 18361 18348 -13
+ Misses 3850 3849 -1
Partials 3 3
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
Eval Execution |
Jamie Kim (jkim323)
left a comment
There was a problem hiding this comment.
Thanks for addressing the script issues and tightening the local/CI Python lint behavior. Have two things for you to look at.
| } | ||
|
|
||
| $found = if ($resolution.mismatches) { "found $($resolution.mismatches -join ', ')" } else { 'found no ruff candidate' } | ||
| $resolution.reason = "uv.lock requires ruff $lockedVersion but $found. Run 'uv sync --locked' in this project." |
There was a problem hiding this comment.
Hm could we consider using uv sync --locked followed by uv run ruff here, matching python-lint.yml and the siblingInvoke-PythonTests.ps1 runner, instead of manually parsing uv.lock and comparing ruff --version strings? uv sync --locked would provide the missing fresh-environment bootstrap, while uv run ruff would use the project’s lock-selected executable—the same model CI uses. The current resolver recreates only part of uv’s lock/environment-selection behavior and creates a divergent provisioning contract from test:py for the same discovered projects. Was the verify-only divergence intentional and tested against every discovered project?
|
|
||
| echo "Syncing Python environments for skills..." | ||
| find .github/skills -name pyproject.toml -type f -execdir uv sync \; | ||
| find .github/skills -name pyproject.toml -type f -execdir uv sync --locked \; |
There was a problem hiding this comment.
Does this propagate a failed per-project uv sync --locked? With find -execdir ... \;, a nonzero command exit only makes that find expression evaluate false; it does not cause find itself to fail, so set -euo pipefail will not stop container setup. A stale skill lock can therefore leave its .venv uncreated while on-create.sh still reports success, and only fail later in lint:py as a missing/mismatched Ruff environment. Could we replace this with an explicit loop or wrapper that returns nonzero when any project sync fails, matching the direct moderation sync below?
Katrien De Graeve (katriendg)
left a comment
There was a problem hiding this comment.
Thanks for taking this on — closing the local/CI gap on ruff format --check and binding ruff selection to uv.lock is a genuine correctness win, and the per-phase exit codes plus the "Python Lint Parity" section make the remaining differences legible instead of folklore. I pulled the branch and ran both Pester suites in isolation (62 passed, 1 skipped), checked PSScriptAnalyzer against the repo settings (clean), and validated Get-LockedRuffVersion against all 16 committed uv.lock files — it extracted the right version every time and matched the installed .venv ruff wherever one exists. The doc claims about python-lint.yml and the per-PR matrix exclusion also check out against the workflows.
I've left three inline comments, all about the blast radius of the new verify-only contract rather than the implementation itself: one project that nothing provisions, the moderation eval becoming a hard blocker for validate:local, and an asymmetry with the sibling Invoke-PythonTests.ps1 lane. None of them are objections to the approach — flagging them so the decisions are explicit rather than implicit.
|
|
||
| echo "Syncing Python environments for skills..." | ||
| find .github/skills -name pyproject.toml -type f -execdir uv sync \; | ||
| find .github/skills -name pyproject.toml -type f -execdir uv sync --locked \; |
There was a problem hiding this comment.
The --locked change is right, but this loop is now load-bearing for the new lint contract and there's a project it doesn't reach.
Get-PythonSkill scans the whole repo. Outside .github/skills there are two Python projects:
./scripts/evals/moderation [uv.lock / no .venv]
./.github/hooks/shared/telemetry [uv.lock, ruff 0.15.16]
.github/hooks/shared/telemetry is provisioned by neither this loop (.github/skills only) nor copilot-setup-steps.yml. Under the new verify-only contract, a fresh devcontainer or coding-agent runner will fail npm run lint:py — and therefore npm run validate:local — on that project with uv.lock requires ruff 0.15.16 … Run 'uv sync --locked'. CI won't catch it, because python-lint.yml syncs each matrix directory itself, and it happens to have a .venv in existing containers, which is why local validation passed here.
Two options: extend provisioning to cover non-skill projects, or narrow local discovery to the roots that are actually provisioned.
Separately, copilot-setup-steps.yml:123 still runs bare uv sync while this moved to --locked; the repo instructions ask that both environments be evaluated together, so it's worth deciding deliberately whether they should diverge.
| `npm run lint:py` runs the same command set as `python-lint.yml`: `ruff check` followed by the non-mutating `ruff format --check`. Execution conditions still differ in three ways: | ||
|
|
||
| * Provisioning: the hosted lane runs `uv sync --locked` itself, while the local runner only verifies that a project committing `uv.lock` already provides that exact ruff version. When it does not, the local run fails before ruff executes and reports `uv sync --locked` as the setup action. Projects without a `uv.lock` fall back to the project `.venv` ruff and then a global ruff, with no version guarantee. | ||
| * Project scope: local discovery covers every directory containing a `pyproject.toml`, including projects that `pr-validation.yml` excludes from its per-PR matrix. |
There was a problem hiding this comment.
This bullet accurately describes the scope difference, but it understates the consequence now that resolution is strict.
scripts/evals/moderation is excluded from the per-PR matrix precisely because of the torch/detoxify stack (~935 MB). Local discovery still picks it up, and it now requires an exactly-locked ruff there. npm run lint:py is part of validate:local, so the practical effect is that every contributor must provision that heavyweight environment just to run the local aggregate — including contributors who previously had a green run via a global ruff.
Could this bullet say explicitly that the wider local scope is now fatal rather than merely broader? And is it worth giving the runner an exclusion that mirrors the CI one (or a -SkipProject), so the local aggregate doesn't inherit a cost that CI deliberately declined to pay per-PR?
| Push-Location $skillPath | ||
| try { | ||
| $ruffCmd = Resolve-RuffCommand -SkillPath $skillPath -GlobalRuffAvailable $globalRuffAvailable | ||
| $resolution = Resolve-ProjectRuff -SkillPath $skillPath -GlobalRuffAvailable $globalRuffAvailable |
There was a problem hiding this comment.
Worth reconciling this with the sibling lane. scripts/linting/Invoke-PythonTests.ps1 (lines ~89-96) already does lock-aware resolution, but it provisions — uv sync --locked --dev when uv.lock exists, uv sync --dev otherwise — and then executes through uv run.
So after this change, npm run test:py repairs exactly the environment npm run lint:py refuses to repair, and a contributor blocked by the lint failure can unblock it by running the test lane. Going through uv run ruff here would have given exact CI command parity and automatic provisioning for free.
The verify-only choice is defensible — the PR description makes a good case that lint shouldn't implicitly install 935 MB — but right now it's a silent departure from an established in-repo precedent. A short comment here (or a line in the parity section) stating that lint intentionally verifies while the test lane provisions would keep the next person from "fixing" the inconsistency in the wrong direction.
fix(scripts): run ruff format check and lock-bound ruff in lint:py
Description
npm run lint:pyranruff checkonly, while the CI Python lint job ranruff checkandruff format --check. A locally green Python change could still fail CI. The local runner also picked whichever ruff it found first — the project.venv, otherwise a global install — so local and CI runs could execute different ruff versions and reach different formatter verdicts.This change made the default local lane execute the same command set as CI and bound ruff selection to each project's committed lockfile.
fix(scripts): addedruff format --check .afterruff check .in the default branch ofInvoke-PythonLint.ps1. Both gates always run, so a lint failure cannot mask a formatting failure, and each result recordscheckExitCodeandformatExitCodeseparately with its own console diagnostic.fix(scripts): added lock-aware ruff resolution toPythonLintHelpers.psm1through three new exported functions.Get-LockedRuffVersionreads the ruff version pinned byuv.lock,Get-RuffVersionStringreports a candidate binary's version, andResolve-ProjectRuffaccepts only an exact match — evaluating the project.venvbinary (Linux, then Windows layout) before a globalruff, and retaining every rejected candidate as diagnostic evidence.uv sync --lockedas the setup action.uv.lockkeep the previous.venv-then-global fallback and report an explicitunlocked-fallbackmode, making no version claim.-Fixmode resolves through the same resolver, so checks and fixes cannot run different ruff versions. Its command sequence remainscheck . --fixfollowed byformat ..test(scripts): extended the two owning Pester suites.PythonLintHelpers.Tests.ps1gained coverage for lock parsing (valid, ruff-free, malformed, and version-bleed locks), exact.venvmatching on both layouts, global fallback after a.venvmismatch, global mismatch rejection, and all three unlocked-fallback outcomes.Invoke-PythonLint.Tests.ps1gained a call ledger asserting exact ruff arguments and ordering, a non-mutation assertion for default mode, separate lint-only and format-only failure contexts, a resolution-failure context asserting ruff is never invoked, an exact-Fixsequence assertion, and serialized per-phase field assertions. The former generic failure context was reclassified as combined failure rather than removed, and the existing missing-ruff, exception, discovery, output-persistence, and real-ruff I001 contexts were left intact.docs(architecture): corrected the twolint:pyrows indocs/architecture/workflows.mdand added a "Python Lint Parity" subsection naming the three remaining execution differences — CI provisions the environment while the local runner only verifies it, local discovery covers projects the per-PR matrix excludes, and the hosted lane defaults to running only when a PR changes.pyor.pyifiles.docs(scripts): rewrote theInvoke-PythonLint.ps1Features, Parameters, and Usage text inscripts/linting/README.mdto describe lock-aware verify-only resolution, the default formatter check, per-phase exit codes, and unlocked fallback.fix(devcontainer): changed the per-skill provisioning loop in.devcontainer/scripts/on-create.shfromuv synctouv sync --locked, so a fresh container installs the exact ruff versions the verify-only lane requires. Everypyproject.tomlunder.github/skillshas a committeduv.lock, so no project loses provisioning.package.jsonwas not modified..github/instructions/ci-owned-validation.instructions.mdwas consulted first, and the existinglint:pywrapper already satisfied the requirement without a script-taxonomy change.Related Issue(s)
Closes #2694
Type of Change
Select all that apply:
Code & Documentation:
Infrastructure & Configuration:
AI Artifacts:
hve-builderand addressed all actionable findings.github/instructions/*.instructions.md).github/prompts/*.prompt.md).github/agents/*.agent.md).github/skills/*/SKILL.md).github/hooks/*/*.json)evals/)Other:
.ps1,.sh,.py)Testing
Bounded Pester suites, run through
npm run test:ps:scripts/tests/linting/PythonLintHelpers.Tests.ps1scripts/tests/linting/Invoke-PythonLint.Tests.ps1Repository validation:
npm run lint:pynpm run lint:psnpm run lint:mdnpm run lint:tablesnpm run lint:frontmatternpm run validate:skillsnpm run lint:dependency-pinningshellcheck .devcontainer/scripts/on-create.shThe verify-only contract was exercised for real during validation.
scripts/evals/moderationhad ruff 0.16.1 in its.venvagainst a 0.16.2 lock, so the firstlint:pyrun failed that project before ruff executed and reporteduv sync --locked. Running that command as a separate setup step brought the environment to the lock, after which the lane passed. The 935 MB moderation environment was never provisioned implicitly by the lint path.A scoped diff inspection confirmed that exactly the seven intended files changed, with no
.py,.pyi,uv.lock, orpyproject.tomldiff — satisfying the requirement that no source is reformatted as an incidental side effect.Checklist
Required Checks
AI Artifact Contributions
hve-builderreview mode to review contributionhve-builderreviewRequired Local Checks
The following local-safe validation commands must pass before merging:
npm run validate:localnpm run validate:docsnpm run spell-checknpm run lint:md-linksSecurity Considerations
Additional Notes
Behavior change for contributors: a project that commits
uv.lockmust now have a matching ruff already installed beforenpm run lint:pywill check it. A stale environment fails that project with the required version anduv sync --lockedas the named setup action, rather than silently linting with a different ruff. Lint never installs or synchronizes dependencies.The same
--lockedflag now applies to devcontainer skill provisioning. Becauseon-create.shruns underset -euo pipefailandfind -execdirreturns non-zero when an invocation fails, a skill whose committed lock no longer resolves would stop container creation instead of silently relocking. The existingpytest-tests.ymlandfuzz-tests.ymllanes already runuv sync --lockedagainst the same projects, so a stale lock fails CI before it can reach a fresh container.Ruff version convergence across skills was deliberately left out of scope. Lock-bound execution makes the selected version deterministic per project, but the repository still spans several ruff 0.15.x minors. Ruff 0.16 brings Markdown code blocks into formatter scope and at least one existing Markdown file would fail under it, so that belongs to its own change rather than riding along with a parity fix.
✅ - Generated by Copilot