Cycle-aware pin semantics + stale pin cleanup#79
Conversation
Pins for multi-version tools store three distinct things in the same slot: - ``"never"`` — never install / update - the cycle string (``"3.12"``) — hold this cycle, any patch - an explicit version (``"3.12.7"``)— hold exactly this patch The previous Python side treated everything except ``"never"`` as an exact-version pin, which mis-flagged valid cycle-holds and escalated harmless stale skip-markers (e.g. pin ``"8.5.3"`` on ``php@8.5`` when installed is ``"8.5.5"``) to a loud CONFLICT. Additions to ``cli_audit/pins.py``: - ``classify_pin(pin, cycle)`` — returns ``none|never|cycle|version``. - ``apply_pin_to_status`` grows an optional ``cycle`` arg. Cycle-holds are honored when installed is within the cycle; multi-version rows with a stale patch-pin pass their upstream status through unchanged rather than escalating. - ``pin_label(pin, cycle, installed)`` — single source of truth for the rendered suffix: ``PIN:never`` / ``CYCLE:3.12`` / ``PIN:x`` / ``PIN:x stale``. 17 new tests in ``tests/test_pins.py`` cover the full matrix. Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
Three rendering changes drive the install column from "version" to "version + declared intent": - The row's cycle is extracted (``_row_cycle``) and threaded through to the pin-aware status computation, so ``python@3.12`` pinned to ``"3.12"`` (cycle-hold) renders ``✅`` on any ``3.12.x`` installed. - Pin + AUTO markers are combined into a single bracketed suffix on the installed column (``3.14.4 [AUTO]``, ``3.12.7 [CYCLE:3.12 AUTO]``, ``8.5.5 [PIN:8.5.3 stale]``) via ``_installed_markers``. - ``AUTO`` is suppressed when the pin is ``never`` — those two states contradict each other and the user read the combination as a bug. - ``load_config`` promoted to a module-level import so tests can ``monkeypatch.setattr`` it, and an autouse fixture neutralizes the developer's real ``~/.config/cli-audit/config.yml`` so other tests don't leak stray ``[AUTO]`` markers into assertions. 6 new render tests: cycle-hold, stale-patch, violated-never, single-version conflict, AUTO visibility, AUTO base-tool inheritance. Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
Add ``--stale`` (and ``--dry-run``) to ``reset_pins.sh``. Stale = patch-level pin on a tool that is currently installed at a different version, AND the pin is not a cycle-string, AND the pin is not ``"never"``. These are almost always fossil records from the ``s`` (skip) or failed-upgrade prompt paths in ``guide.sh``, left behind after the system moved past them. Preserved by design: - ``PIN:never`` — deliberate "don't install". - ``CYCLE:<cycle>`` — the user wants to hold a cycle. - Pins where ``installed == pin`` — the pin is actively honored. - Pins for tools not in the current snapshot — can't judge staleness. ``--dry-run`` prints the classification for every pin without touching the file; good for sanity-checking before committing to a cleanup. ``scripts/lib/pins.sh`` already exposes ``pins_remove`` and ``pins_remove_cycle``; no library changes needed. Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #79 +/- ##
==========================================
+ Coverage 69.10% 69.47% +0.37%
==========================================
Files 22 22
Lines 3301 3345 +44
==========================================
+ Hits 2281 2324 +43
- Misses 1020 1021 +1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Enhances pin semantics and audit rendering to correctly interpret cycle-level holds vs exact version pins, reduce false conflict noise from stale pins, and surface explicit auto-update configuration in the rendered audit table.
Changes:
- Add cycle-aware pin classification (
none|never|cycle|version) and a unifiedpin_label()for consistent rendering. - Update rendering to thread cycle info into pin/status evaluation and display
[AUTO]/pin markers in the installed column. - Extend
scripts/reset_pins.shwith--stale(and--dry-run) to remove only stale patch-level pins using the current snapshot.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
cli_audit/pins.py |
Adds cycle-aware pin classification, status adjustment rules, and pin label rendering. |
cli_audit/render.py |
Threads cycle into pin evaluation; renders pin/AUTO markers in installed column; simplifies notes. |
scripts/reset_pins.sh |
Adds stale-only cleanup mode driven by the snapshot, plus dry-run support. |
tests/test_pins.py |
Adds unit tests for classify_pin, cycle-aware status behavior, and pin_label. |
tests/test_render.py |
Adds renderer tests for cycle-holds, stale pins, single-version conflicts, and AUTO marker visibility. |
cli_audit/__init__.py |
Exports new pin helpers (classify_pin, pin_label). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Code Review
This pull request implements cycle-aware version pinning to better manage multi-version tools, ensuring that stale patch-level pins do not trigger unnecessary conflict statuses. The CLI rendering is updated to show human-readable pin labels and explicit auto-update markers within the installed version column. Additionally, the reset_pins.sh script is expanded with a --stale mode for targeted pin removal and a dry-run option. Review feedback recommends enhancing the shell script by properly handling potential JSON parsing errors and adopting mktemp for secure temporary file creation.
Three issues, five review comments on PR #79: 1. ``scripts/reset_pins.sh`` silently swallowed invalid JSON (both the pins file and the snapshot). Add a ``validate_json_object`` helper that emits "not valid JSON" or "valid JSON but not a top-level object" with the offending path before exiting non-zero. Applies to both ``--stale`` and the default all-pins mode. (reviewers: gemini, copilot) 2. ``scripts/reset_pins.sh`` wrote to a predictable ``/tmp`` path and only installed the cleanup trap *after* the loop. Replaced with ``mktemp -t cli-audit-stale-pins.XXXXXX``, trap on ``EXIT INT TERM`` installed immediately after creation, and no temp file written at all in ``--dry-run`` mode (the plan is printed from an in-memory array now). (reviewers: gemini, copilot) 3. ``cli_audit/render.py`` header comment still said the ``notes`` column carried the auto-update flag; updated to reflect the actual placement (pin + AUTO live next to ``installed``, ``notes`` is method-only). (reviewer: copilot) 608 tests still pass; manually verified the new JSON-validation paths reject a malformed pins file and a top-level list with clear errors, and no ``cli-audit-stale-pins.*`` temp files leak on normal or interrupted runs. Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
Summary
Follow-up to #78 (merged). Three issues surfaced once users saw real pin data in the audit table:
~/.config/cli-audit/pins.jsonstores per-cycle pins as{cycle: value}butvaluecan mean either "hold this cycle, any patch" ("3.12") or "hold this exact patch" ("3.12.7"). The previous code treated every non-neverpin as exact-version, flagging valid cycle-holds as conflicts.guide.shpins to$installedor$latestfrom several code paths (pin-on-failure prompt,sskip). When the world moved past those pins, my rendering escalated them to⚠️even though the pin no longer applies meaningfully.auto_update: trueexisted inconfig.ymlbut had no marker on rows.Changes
Atomic commits:
feat(pins)— newclassify_pin(pin, cycle)returnsnone|never|cycle|version.apply_pin_to_statusgrows acycleparameter: cycle-holds are honored when installed is within-cycle; multi-version rows with stale patch-pins pass their upstream status through (no CONFLICT escalation); single-version tools still conflict on mismatch. Newpin_label()producesPIN:never/CYCLE:3.12/PIN:x/PIN:x stale— one source of truth for the rendered suffix.feat(render)— cycle threaded into status computation; pin + AUTO combined in a single bracketed suffix on the installed column (3.14.4 [AUTO],3.12.7 [CYCLE:3.12 AUTO],8.5.5 [PIN:8.5.3 stale]); AUTO suppressed when pin isnever(contradictory state). Notes column slims to just the install method.feat(scripts)—scripts/reset_pins.sh --stale [--dry-run]removes only stale patch-level pins (installed present, pin != installed, pin != cycle, pin !=never). Cycle-holds,neverpins, and honored patch-holds are preserved.Before / after on this repo
Stale pins in the user's
pins.json(php@8.5: 8.5.3,php@8.4: 8.4.18) cleaned with the new--staleflag; config is now internally consistent.Test plan
uv run pytest— 608 passing (up from 585; 23 new tests: 17 intest_pins.pycoveringclassify_pin/ cycle-awareapply_pin_to_status/pin_label, 6 intest_render.pycovering cycle-hold / stale / single-version conflict / AUTO visibility / base-tool inheritance)../scripts/test_smoke.shpasses.flake8clean on changed files.scripts/reset_pins.sh --stale --dry-rundry-runs correctly against the real snapshot.pins.jsonclassifies as expected — runbash scripts/reset_pins.sh --stale --dry-run.