Skip to content

Cycle-aware pin semantics + stale pin cleanup#79

Merged
CybotTM merged 4 commits into
mainfrom
feat/pin-cycle-semantics
Apr 21, 2026
Merged

Cycle-aware pin semantics + stale pin cleanup#79
CybotTM merged 4 commits into
mainfrom
feat/pin-cycle-semantics

Conversation

@CybotTM
Copy link
Copy Markdown
Member

@CybotTM CybotTM commented Apr 21, 2026

Summary

Follow-up to #78 (merged). Three issues surfaced once users saw real pin data in the audit table:

  1. Cycle vs patch pins were indistinguishable. ~/.config/cli-audit/pins.json stores per-cycle pins as {cycle: value} but value can mean either "hold this cycle, any patch" ("3.12") or "hold this exact patch" ("3.12.7"). The previous code treated every non-never pin as exact-version, flagging valid cycle-holds as conflicts.
  2. Stale skip-markers shouted as conflicts. guide.sh pins to $installed or $latest from several code paths (pin-on-failure prompt, s skip). When the world moved past those pins, my rendering escalated them to ⚠️ even though the pin no longer applies meaningfully.
  3. Auto-update state was invisible. User-configured auto_update: true existed in config.yml but had no marker on rows.

Changes

Atomic commits:

  1. feat(pins) — new classify_pin(pin, cycle) returns none|never|cycle|version. apply_pin_to_status grows a cycle parameter: 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. New pin_label() produces PIN:never / CYCLE:3.12 / PIN:x / PIN:x stale — one source of truth for the rendered suffix.
  2. 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 is never (contradictory state). Notes column slims to just the install method.
  3. 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, never pins, and honored patch-holds are preserved.

Before / after on this repo

before:                                              after:
⚠  php@8.5   8.5.5 [PIN:8.5.3]      apt            ✓  php@8.5   8.5.5                 apt
⚠  php@8.4   8.4.20 [PIN:8.4.18]    apt            ✓  php@8.4   8.4.20                apt
✓  python@3.12  3.12 [PIN:3.12]     apt            ✓  python@3.12  3.12 [CYCLE:3.12 AUTO]  apt
✓  python@3.14  3.14.4              manual · auto  ✓  python@3.14  3.14.4 [AUTO]      manual
⚠  ruby@3.3  3.3.6 [PIN:never AUTO] manual · auto  ⚠  ruby@3.3  3.3.6 [PIN:never]     manual

Stale pins in the user's pins.json (php@8.5: 8.5.3, php@8.4: 8.4.18) cleaned with the new --stale flag; config is now internally consistent.

Test plan

  • uv run pytest — 608 passing (up from 585; 23 new tests: 17 in test_pins.py covering classify_pin / cycle-aware apply_pin_to_status / pin_label, 6 in test_render.py covering cycle-hold / stale / single-version conflict / AUTO visibility / base-tool inheritance).
  • ./scripts/test_smoke.sh passes.
  • flake8 clean on changed files.
  • scripts/reset_pins.sh --stale --dry-run dry-runs correctly against the real snapshot.
  • Reviewer: verify your pins.json classifies as expected — run bash scripts/reset_pins.sh --stale --dry-run.

CybotTM added 3 commits April 21, 2026 15:10
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>
Copilot AI review requested due to automatic review settings April 21, 2026 13:11
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 21, 2026

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 21, 2026

Codecov Report

❌ Patch coverage is 97.14286% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.47%. Comparing base (219495e) to head (f1dd641).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
cli_audit/render.py 94.73% 2 Missing ⚠️
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     
Flag Coverage Δ
unittests 69.47% <97.14%> (+0.37%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 unified pin_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.sh with --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.

Comment thread scripts/reset_pins.sh
Comment thread scripts/reset_pins.sh Outdated
Comment thread cli_audit/render.py Outdated
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread scripts/reset_pins.sh Outdated
Comment thread scripts/reset_pins.sh Outdated
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>
@CybotTM CybotTM merged commit ed23db2 into main Apr 21, 2026
18 checks passed
@CybotTM CybotTM deleted the feat/pin-cycle-semantics branch April 21, 2026 13:54
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.

2 participants