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
10 changes: 10 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,16 @@ nearest `.tool-versions` above the cwd, so a run typed inside a project would
update that project's toolchain instead of the global one pinned in
`~/.tool-versions`. `tests/update.bats` pins this.

**`update()` runs machine-local checks after the upgrades.** Every
`update.check` command in git config (repeatable, overlay-supplied — the
public repo names none) runs right after the no-sudo upgrades, while an
upgrade is still the obvious suspect for whatever it breaks: a service whose
networking a formula update changed, a cask a daemon reads secrets through.
A failing check never stops the update; each failure is repeated in a red
banner at the very end, past the softwareupdate and gem output that would
otherwise bury it. A value containing `;` must be quoted in the config file
(git reads a bare `;` as a comment) — `git config --add` does that for you.

**A half-installed cask is invisible to all of this.** A `.pkg` cask whose
installer never ran still leaves a Caskroom entry, so `brew bundle` counts it
as satisfied and never retries while the app is missing from `/Applications`.
Expand Down
17 changes: 16 additions & 1 deletion dot_functions
Original file line number Diff line number Diff line change
Expand Up @@ -2915,9 +2915,17 @@ function doctor() {
# Get macOS Software Updates, and update installed Ruby gems, Homebrew, npm, uv,
# and their installed packages. Runs from $HOME whatever directory it was typed
# in, and puts the shell back afterwards.
#
# After the no-sudo upgrades it runs every `update.check` command from git
# config (repeatable, machine-local, overlay-supplied — this repo names none):
# whatever on this machine an upgrade can silently break gets to say so while
# the upgrade is the obvious suspect. A failing check never stops the update;
# failures are repeated in a red banner at the very end, past the
# softwareupdate and gem output that would otherwise bury them.
function update() {
setopt localoptions nomonitor nonotify;
local here=${PWD:-} keepalive_pid=;
local here=${PWD:-} keepalive_pid= check;
local -a failed;

# Two reasons this never runs where it was typed. A directory that no
# longer exists (a removed worktree is the usual way) is inherited by
Expand Down Expand Up @@ -2964,6 +2972,10 @@ function update() {
command -v npm > /dev/null 2>&1 && npm install npm -g;
command -v npm > /dev/null 2>&1 && npm update -g;
command -v uv > /dev/null 2>&1 && uv tool upgrade --all;
for check in ${(f)"$(command git config --get-all update.check 2>/dev/null)"}; do
print -r -- "== update.check: $check";
sh -c "$check" || failed+=("$check");
done;

# One password prompt for the rest; keep the sudo timestamp alive
# until the updates finish (same pattern as ~/.macos). Disowned (&!)
Expand All @@ -2989,6 +3001,9 @@ function update() {
# and does not fire on an interrupt at all, so a ^C'd update left the
# keepalive polling sudo for the life of the shell.
[[ -n $keepalive_pid ]] && kill "$keepalive_pid" 2>/dev/null;
for check in $failed; do
print -Pru2 -- "%F{red}%B✘ update.check failed:%b%f ${check//\%/%%}";
done;
cd -q -- "$here" 2>/dev/null || print -ru2 -- "update: $here vanished during the update; shell left in $PWD";
};
}
22 changes: 22 additions & 0 deletions tests/update.bats
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,25 @@ run_update() {
! grep -qF "sudo -n true" calls.log || { echo "keepalive started after sudo -v failed"; false; }
! grep -qF "sudo gem" calls.log || { echo "gem ran after sudo -v failed"; false; }
}

@test "update.check commands run after the upgrades; a failure is repeated at the end and never stops the update" {
export GIT_CONFIG_GLOBAL="$BATS_TEST_TMPDIR/gitconfig" GIT_CONFIG_NOSYSTEM=1
git config --file "$GIT_CONFIG_GLOBAL" --add update.check 'echo first-check-ran'
git config --file "$GIT_CONFIG_GLOBAL" --add update.check 'echo 50% broken >&2; exit 3'
start="$BATS_TEST_TMPDIR/start"; mkdir -p "$start"
run_update "cd -q -- '$start'"
grep -q "^RC=0 PWD=$(cd "$start" && pwd -P)\$" out
grep -qF "first-check-ran" out
grep -qF "== update.check: echo 50% broken >&2; exit 3" out
# The banner comes last, after gem, and names the failing command verbatim.
[ "$(tail -n 1 err | sed $'s/\e\\[[0-9;]*m//g')" = "✘ update.check failed: echo 50% broken >&2; exit 3" ]
! grep -q "update.check failed: echo first" err
grep -qF "sudo gem cleanup" calls.log
}

@test "with no update.check configured, nothing extra is printed" {
export GIT_CONFIG_GLOBAL="$BATS_TEST_TMPDIR/gitconfig" GIT_CONFIG_NOSYSTEM=1
: > "$GIT_CONFIG_GLOBAL"
run_update ":"
! grep -q "update.check" out err
}
Loading