Skip to content

feat(badge): opt-in update badge around brew update/upgrade - #150

Merged
shrwnsan merged 19 commits into
mainfrom
feat/badge-integration
Oct 6, 2026
Merged

shrwnsan merged 19 commits into
mainfrom
feat/badge-integration

Conversation

@shrwnsan

@shrwnsan shrwnsan commented Oct 6, 2026

Copy link
Copy Markdown
Owner

Summary

  • Opt-in shell-wrapper badge: eval "$(brew-change init zsh)" prints a one-line risk verdict from the cached assessment after every brew update/brew upgrade — counts, breaking packages (red), no-signal (green), unknown (yellow), age — under Homebrew's own ==> header style. Brew's exit status passes through untouched; NO_COLOR respected; silent in pipes.
  • before-upgrade trigger: read-only verdict above brew's plan, where the y/n decision actually happens.
  • brew-change refresh: headless, lock-coalescing background re-run that keeps the export fresh (dead-PID/expiry takeover, 30-min failure backoff, degraded-run guard that never clobbers a healthy export with forced-unknown records, size-capped refresh.log). Also the future entry point for a LaunchAgent.
  • brew-change init: emits the wrapper with the emitting binary's absolute path pinned (__bc_bin), so PATH drift cannot resolve an older brew-change that predates the subcommands.
  • Homebrew has no hook/plugin mechanism for core commands (verified against installed source, 2026-10) — the wrapper is the only route to inline highlights; this feature is strictly opt-in and leaves brew itself untouched.

Design record: docs/dev/prd-004-badge-integration.md (with live-testing addendum), plan in docs/dev/tasks-006-badge-integration.md, user guide in docs/badge-integration.md.

Test Plan

  • 4 new deterministic suites (118 assertions): badge output contract (fresh/stale/missing/malformed/future-schema, colors, duplicate suppression), refresh lock (adopt/block/takeover), init emission (syntax, binary pin, exit-code passthrough under a pty), refresh flow (empty-outdated, degraded-keep end-to-end)
  • Full deterministic suite green: 36/36 (ubuntu + macos via CI on this PR)
  • Live end-to-end on macOS: badge after real brew update, spawn → adopt → refresh rc=0, settle-back, decline path, wrapper exit-code passthrough

🤖 Generated by Claude Code - glm-5.3-flash

shrwnsan and others added 19 commits October 5, 2026 03:29
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
…ract

Pipe-separated counts from badge_counts so empty fields survive read -a.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
EXIT trap owns lock release + size-capped refresh.log on every path;
empty-outdated runs still write a fresh empty export; subcommand tokens
never parse as package names; state dir auto-created before locking.

Co-Authored-By: Claude <noreply@anthropic.com>
REFRESH_MODE initialized with the other mode flags and the refresh
handler moved below full argument parsing so the parse default cannot
clobber it.

Co-Authored-By: Claude <noreply@anthropic.com>
The wrapper's failed >/dev/tty open printed shell noise when no
controlling terminal exists; a brace group with its own 2>/dev/null
swallows exactly that message. The init test's pty harness now picks
BSD vs util-linux script(1) syntax — CI runs ubuntu-latest too.

Co-Authored-By: Claude <noreply@anthropic.com>
Failed refreshes start a 30-min backoff window (configurable) during
which the badge neither spawns nor prints a refresh suffix — the line
never promises a refresh that is not happening. A network-dead refresh
(zero fresh|cached-fresh retrievals) keeps a healthy export instead of
clobbering it with forced-unknown records and exits 2 (backoff + log).
Setup hint now points at -u, the flagship workflow.

Co-Authored-By: Claude <noreply@anthropic.com>
One outdated package with every curl probe failing instantly
(BREW_CHANGE_MAX_RETRIES=1 removes retry sleeps; a single package is a
single batch, so no rate-limit sleep) runs the doomed evidence pass in
under a second and exercises the full guard: healthy export kept,
exit 2, rc=2 logged, backoff started, lock released.

Co-Authored-By: Claude <noreply@anthropic.com>
The fixture kept a hardcoded generated_at while BREW_CHANGE_TEST_NOW
tracked capture time, so the test only passed within ~1h of that UTC
instant. Fixture and injected clock now derive from the same capture.

Co-Authored-By: Claude <noreply@anthropic.com>
Users who added the eval line while PATH still resolved v1.20.1 got a
shell-startup stall: the old pre-parse treats unknown words as package
names, so 'init zsh' ran the changelog pipeline (and could block on a
selection prompt) inside rc evaluation. init now emits
__bc_bin=<absolute path of the emitting binary> and the wrapper invokes
badge through it — immune to PATH drift, and checkout installs work
before any release. Requirements note added to docs.

Co-Authored-By: Claude <noreply@anthropic.com>
The badge pre-acquires the refresh lock and writes the spawned child's
pid into it; the child then saw a lock 'held by a live process' —
itself — and exited instantly. Every badge spawn produced a no-op
child and a lingering lock: refresh never ran. Found live: badge said
'refreshing…' forever while the lock held a dead pid and refresh.log
stayed empty. refresh_lock_acquire now adopts a lock whose pid equals
its own; foreign live pids still block, dead/expired still take over.

Co-Authored-By: Claude <noreply@anthropic.com>
Harness suites faked brew/curl but ran with the real HOME, so piped -u
tests overwrote the user's ~/.brew-change/last-assessment.json with
3-package all-unknown fixture data — pre-existing since tasks-005, and
user-visible once the badge started reading that file. setup_command_
harness now points HOME at a temp dir inside the harness root and
teardown restores it.

Co-Authored-By: Claude <noreply@anthropic.com>
The post-upgrade badge lands after the decision point — useless at the
y/n. before-upgrade prints the cached verdict above brew's plan where
the choice happens: no refresh, no suffix, decision support only. The
post-upgrade badge keeps the settle-state and self-healing refresh.

Co-Authored-By: Claude <noreply@anthropic.com>
Breaking red, no-signal green, unknown yellow, prefix and age dimmed —
presentation only, the words carry the classification, NO_COLOR
respected. The verdict is marked with Homebrew's own bold-green '==>'
header style. An after-* verdict identical to the last printed one
(same export, <10 min) repeats nothing: only the honest spawn tail or
silence. before-upgrade always prints — decision support is never
suppressed. Color and dedupe require a real TTY so FORCE runs stay
deterministic.

Co-Authored-By: Claude <noreply@anthropic.com>
Three CI-ubuntu failures, none reproducible on macOS:
- badge_export_stamp tried BSD stat -f first; GNU stat -f is a
  filesystem query that succeeds with drifting multi-line output, so
  stamps never matched and duplicate suppression never fired. GNU -c
  first now (macOS -c errors into the -f fallback).
- the export-subcommand test needs brew on PATH for
  verify_dependencies; CI ubuntu runners have no Homebrew. Minimal
  brew shim added.
- the order test used BSD script syntax only; util-linux needs -c.
Same case-split as the wrapper pty harness.
@shrwnsan
shrwnsan merged commit fd27a82 into main Oct 6, 2026
2 checks passed
@shrwnsan
shrwnsan deleted the feat/badge-integration branch October 6, 2026 16:31
@shrwnsan

shrwnsan commented Oct 6, 2026

Copy link
Copy Markdown
Owner Author

✅ PR Merged

Merged to: main (fd27a82)

Summary

Opt-in update badge for brew-change: a one-line risk verdict around brew update / brew upgrade, plus the headless refresh engine that keeps it honest.

Changes

  • badge / refresh / init subcommands; opt-in wrapper with the emitting binary pinned (__bc_bin)
  • before-upgrade read-only verdict above brew's plan (decision support at the y/n)
  • Risk coloring (breaking red / no-signal green / unknown yellow, NO_COLOR respected), ==> header marker, duplicate suppression on TTYs
  • Background refresh: lock with own-PID adoption, 30-min failure backoff, degraded-run guard, size-capped refresh.log
  • 4 new deterministic suites (118 assertions); Linux portability fixes found by this PR's own CI gate
  • Docs: docs/badge-integration.md guide, configuration entries, prd-004 decision record with live-testing addendum

Issues Resolved

None (no linked issues)

🤖 Generated by Claude Code - GLM 5.3 Flash

@shrwnsan shrwnsan mentioned this pull request Oct 6, 2026
2 tasks done
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.

1 participant