Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions docs/CI-RUNNER-ROUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <paths>
# equivalent: uvx ruff==$(awk '/^ruff==/{sub(/^ruff==/,""); sub(/[[:space:]\\].*$/,""); print; exit}' .github/requirements-ci.txt) check <paths>
```

`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==<pin>`. 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)
Expand Down
2 changes: 1 addition & 1 deletion plugins/source-control/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
21 changes: 21 additions & 0 deletions plugins/source-control/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Comment thread
kyle-sexton marked this conversation as resolved.

### 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==<pin>`. 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
Expand Down
23 changes: 18 additions & 5 deletions plugins/source-control/skills/babysit-prs/scripts/engine.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 =="
Expand Down
70 changes: 70 additions & 0 deletions scripts/run-ruff.sh
Original file line number Diff line number Diff line change
@@ -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==<pin>` 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
Comment thread
kyle-sexton marked this conversation as resolved.

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