diff --git a/docs/CI-RUNNER-ROUTING.md b/docs/CI-RUNNER-ROUTING.md index aa568e03cc..c281a346d4 100644 --- a/docs/CI-RUNNER-ROUTING.md +++ b/docs/CI-RUNNER-ROUTING.md @@ -42,6 +42,32 @@ wheel and SHA-256 in `.github/requirements-ci.txt`, and Dependabot tracks both dependency roots. CI consumes those manifests with `npm ci` and hash-required `pip`, never mutable global installs. +### Local / workstation ruff + +Do **not** trust a bare `ruff` on `PATH` for verification in this repository. +A workstation `ruff` at a different version from the one CI installs disagrees +with CI in both directions — it reports findings on an unmodified `main` tree +that CI accepts, and misses findings CI raises — because a release can move a +rule into or out of the default set, as 0.16.0 did for eighteen `E`/`F` rules. +Resolve the tool from the pin instead of from `PATH`: + +```shell +scripts/run-ruff.sh check +# equivalent: uvx ruff==$(awk '/^ruff==/{sub(/^ruff==/,""); sub(/[[:space:]\\].*$/,""); print; exit}' .github/requirements-ci.txt) check +``` + +`scripts/run-ruff.sh` uses a PATH `ruff` only when it already reports the pinned +version (the CI install path); otherwise it runs `uvx ruff==`. Plugin +contract tests that lint Python (`engine.test.sh`) invoke that wrapper. The pin +is read at run time, so the wrapper follows the repository's version wherever it +goes and carries no copy of its own. + +Bumping the pin is a deliberate Dependabot change, and its direction is set +elsewhere: the pin is held equal to the fleet inventory in +melodic-software/dotfiles (`.chezmoidata/uv-tools.yaml`), which is what installs +a developer's local toolchain, so CI never lints with a ruff nobody runs. Do not +"fix" a clean tree by adopting a newer ruff's new rules in an unrelated PR. + ## Authoritative references - [Reuse workflows and pin a commit SHA](https://docs.github.com/en/actions/how-tos/reuse-automations/reuse-workflows) diff --git a/plugins/source-control/.claude-plugin/plugin.json b/plugins/source-control/.claude-plugin/plugin.json index c8464accef..79c019d81f 100644 --- a/plugins/source-control/.claude-plugin/plugin.json +++ b/plugins/source-control/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "source-control", - "version": "0.47.1", + "version": "0.47.2", "description": "Git and GitHub delivery workflow: /commit (Conventional Commits + Co-Authored-By trailer via safe heredoc mechanics), /pull-request (prep, create, CI monitoring, review-comment triage, merge, CI-log fetch), /babysit-prs (self-pacing fleet loop — safe by default; opt-in worker/autopilot tiers add gate-checked merge and thread resolution behind a deterministic Python engine), /babysit-loop (the loop-lane merge lane: a standing or drain loop that invokes babysit-prs per cycle, configured through repo-scoped babysit_loop_* keys on the layered source-control.md seam, with merge authority human-only until the target repo's tracked config adopts the lane, a gate-proven C2-mechanical baseline once adopted, and standing merge-rung raises binding from the team-tracked layer only — with one named exception, where an invocation line explicitly typing both the autopilot tier keyword and the dedicated raise argument --merge c3-this-run widens that single invocation's merge authority up to C3 behind a fresh independent frontier-tier resolver, while C4-structural and C5-untrusted-provenance stay unconditionally human-merge), /worktree (create, status, cleanup, audit for parallel-session isolation), /setup (check the effective commit-subject / PR-title convention merged across its config layers and the babysit-prs config, or apply — interview the repo and write the convention config to a chosen layer), and /resolve-conflicts (intent-first merge/rebase conflict resolution with a semantic-conflict sweep — never --abort). The commit-subject / PR-title convention is configurable via a source-control.md config written by a re-runnable setup skill, layered across a ~/.claude user-global file, the tracked team file, and a gitignored .claude/source-control.local.md personal overlay merged per key; Conventional Commits is the default when no convention is declared.", "author": { "name": "Melodic Software", diff --git a/plugins/source-control/CHANGELOG.md b/plugins/source-control/CHANGELOG.md index d217531488..fb6039b883 100644 --- a/plugins/source-control/CHANGELOG.md +++ b/plugins/source-control/CHANGELOG.md @@ -3,6 +3,27 @@ All notable changes to the `source-control` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.47.2] + +### Fixed + +- **`babysit-prs` `engine.test.sh` resolves ruff from the declared pin instead of PATH (#1856).** + A workstation `ruff` at a different version from the one CI installs made the harness report + findings on an unmodified tree that CI does not, or miss findings CI raises — the two disagree in + both directions once a release changes the default rule set, as 0.16.0 did. The lint pass now + goes through `scripts/run-ruff.sh`, which uses a PATH `ruff` only when it already matches the pin + in `.github/requirements-ci.txt` and otherwise runs `uvx ruff==`. The pin is read at run + time, so this follows the repository's version wherever it goes rather than freezing a value + here. + +- **`engine.test.sh` finds that wrapper on a relative invocation, so the lint pass actually runs.** + The suite re-derived the repository root from `BASH_SOURCE` *after* `cd`-ing into its own + directory. `BASH_SOURCE` holds the path as invoked, so a relative invocation resolved against the + new working directory and landed outside the repository: the wrapper was never found, the harness + printed `SKIP: scripts/run-ruff.sh not found (lint pass omitted)`, and the suite exited 0 having + linted nothing. `scripts/run-plugin-tests.sh` runs it from the repository root, so that was the + path CI took every time. The script directory is now captured once, before the `cd`. + ## [0.47.1] ### Changed diff --git a/plugins/source-control/skills/babysit-prs/scripts/engine.test.sh b/plugins/source-control/skills/babysit-prs/scripts/engine.test.sh index 924f6fc80b..1a04dcdd69 100755 --- a/plugins/source-control/skills/babysit-prs/scripts/engine.test.sh +++ b/plugins/source-control/skills/babysit-prs/scripts/engine.test.sh @@ -6,7 +6,11 @@ # repo test-runner convention for optional toolchains. set -uo pipefail -cd "$(dirname "${BASH_SOURCE[0]}")" || exit 1 +# Resolve before the cd: BASH_SOURCE is the path as invoked, so re-deriving it +# afterwards resolves a relative invocation against the NEW cwd and silently +# lands somewhere else. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" || exit 1 +cd "$SCRIPT_DIR" || exit 1 PY="" for candidate in python3 python; do @@ -30,13 +34,22 @@ if ! "$PY" -m unittest discover -s tests -p 'test_*.py'; then FAILED=1 fi -if command -v ruff >/dev/null 2>&1; then - echo "== ruff ==" - if ! ruff check . tests; then +# Lint through the repo's CI pin (.github/requirements-ci.txt) rather than a +# bare PATH ruff: a workstation global that auto-upgraded past the pin false- +# reds a clean tree with findings CI does not report (#1856). +REPO_ROOT="$(cd "$SCRIPT_DIR/../../../../.." && pwd)" +RUN_RUFF="$REPO_ROOT/scripts/run-ruff.sh" +if [[ -f "$RUN_RUFF" ]]; then + echo "== ruff (CI pin via scripts/run-ruff.sh) ==" + ruff_rc=0 + bash "$RUN_RUFF" check . tests || ruff_rc=$? + if [[ "$ruff_rc" -eq 127 ]]; then + echo "SKIP: pinned ruff not available (install uv or the CI pin; lint pass omitted)" + elif [[ "$ruff_rc" -ne 0 ]]; then FAILED=1 fi else - echo "SKIP: ruff not installed (lint pass omitted)" + echo "SKIP: scripts/run-ruff.sh not found (lint pass omitted)" fi echo "== guarded-wrapper behavior ==" diff --git a/scripts/run-ruff.sh b/scripts/run-ruff.sh new file mode 100755 index 0000000000..174792edc4 --- /dev/null +++ b/scripts/run-ruff.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# Run ruff at the version pinned in .github/requirements-ci.txt. +# +# CI installs that pin via hash-locked pip (Linux x64 wheels only). Local +# workstations often carry a newer global ruff that auto-upgraded past the pin; +# a bare `ruff check` then reports findings CI does not (and that are out of +# scope for an ordinary lane). This wrapper keeps local verification aligned +# with the pin without requiring a mass lint cleanup for every ruff minor. +# +# Resolution order: +# 1. `ruff` on PATH when it already reports the pinned version (CI path) +# 2. `uvx ruff==` when uv is available (cross-platform local path) +# 3. exit 2 if PATH ruff exists but drifts and uvx is unavailable +# 4. exit 127 if neither a matching ruff nor uvx is available +# +# Usage: scripts/run-ruff.sh [ruff-args...] +# Example: scripts/run-ruff.sh check plugins/source-control +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +req="$repo_root/.github/requirements-ci.txt" + +if [[ ! -f "$req" ]]; then + echo "error: missing $req" >&2 + exit 2 +fi + +# First `ruff==VERSION` line; ignore trailing backslashes / hash pins. +pin="$(awk '/^ruff==/ { + sub(/^ruff==/, "") + sub(/[[:space:]\\].*$/, "") + print + exit +}' "$req")" + +if [[ -z "$pin" ]]; then + echo "error: could not parse ruff==VERSION from $req" >&2 + exit 2 +fi + +# `ruff --version` prints "ruff X.Y.Z"; anything else (including a failed probe) +# is reported as the caller's fallback rather than treated as a version. +ruff_reported_version() { + local out + out="$(ruff --version 2>/dev/null)" || return 1 + printf '%s\n' "${out##* }" +} + +if command -v ruff >/dev/null 2>&1; then + # shellcheck disable=SC2310 # the probe's only failure is "no version", handled by the fallback + got="$(ruff_reported_version)" || got="" + if [[ "$got" == "$pin" ]]; then + exec ruff "$@" + fi +fi + +if command -v uvx >/dev/null 2>&1; then + exec uvx "ruff==${pin}" "$@" +fi + +if command -v ruff >/dev/null 2>&1; then + # shellcheck disable=SC2310 # same probe, same fallback — reported, never trusted + got="$(ruff_reported_version)" || got="unknown" + echo "error: ruff on PATH is ${got}, but CI pins ruff==${pin} (.github/requirements-ci.txt)." >&2 + echo "error: install the pin, or install uv and re-run (uvx ruff==${pin} ...)." >&2 + exit 2 +fi + +echo "error: ruff==${pin} not available (no matching PATH ruff, no uvx)" >&2 +exit 127