Add ssr update subcommand for self-update

- bin/ssr-update: fast-forward `git pull` on the clone with safety guards
  (dirty tree, detached HEAD, divergent history all abort).
- Re-renders & reloads the launchd plist when launchd/ or bin/ssr* changed
  AND the schedule is currently active, so the cron picks up any plist
  template updates automatically.
- Reports VERSION delta, commit log, and new config keys introduced in
  config/ssr.conf.example so users can opt-in via `ssr config edit`.
- `--dry-run` shows would-be changes without pulling or touching launchd.
- Wired into bin/ssr dispatcher + README updated (Updating section, layout
  entry, command help line).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-11 15:38:11 +02:00
parent 539a8fc28f
commit 9f745178b3
3 changed files with 145 additions and 1 deletions
+26
View File
@@ -49,6 +49,7 @@ export PATH="$HOME/.local/bin:$PATH"
ssr install First-time install: link binary, seed config, schedule sync ssr install First-time install: link binary, seed config, schedule sync
ssr sync One-shot backup → cloud ssr sync One-shot backup → cloud
ssr restore [Brewfile] Restore the machine; Brewfile defaults to the cloud copy ssr restore [Brewfile] Restore the machine; Brewfile defaults to the cloud copy
ssr update [--dry-run] Pull latest ssr code, refresh launchd if needed
ssr schedule on|off Enable/disable the daily launchd job ssr schedule on|off Enable/disable the daily launchd job
ssr config show|edit Show or edit the active config ssr config show|edit Show or edit the active config
ssr status Show config, cloud target, last sync, schedule state ssr status Show config, cloud target, last sync, schedule state
@@ -146,6 +147,30 @@ log show --predicate 'process == "ssr"' --last 1h
--- ---
## Updating
Updates happen on three independent layers:
**Tool code (this repo).** Self-update via `ssr update`:
```bash
ssr update # git pull --ff-only on the clone, then refresh side-effects
ssr update --dry-run # show what would change, don't pull
```
`ssr update` will:
- Refuse if the working tree is dirty or HEAD is detached.
- Fast-forward only — never rewrites history.
- Re-render and reload the launchd plist if `launchd/` or `bin/ssr*` changed *and* the schedule is currently active.
- Print added config keys when `config/ssr.conf.example` gained new options.
**Backup data (cloud drive).** Refreshed automatically by the scheduled `ssr sync`. The previous `Brewfile` is preserved as `Brewfile.bak`; deeper history is provided by the cloud provider's built-in version retention (iCloud Drive, Dropbox, …). Trigger an on-demand sync with `ssr sync`.
**Dependencies (Homebrew, mackup).** `ssr sync` runs `brew upgrade` and `brew cleanup` when `SSR_BREW_UPGRADE=true` / `SSR_BREW_CLEANUP=true` (both default `true`). Set them to `false` in `ssr.conf` to keep snapshots without auto-upgrading installed packages.
---
## Project layout ## Project layout
``` ```
@@ -154,6 +179,7 @@ log show --predicate 'process == "ssr"' --last 1h
├── bin/ ├── bin/
│ ├── ssr # CLI dispatcher │ ├── ssr # CLI dispatcher
│ ├── ssr-install # Local install + scheduling │ ├── ssr-install # Local install + scheduling
│ ├── ssr-update # Self-update (git pull + launchd refresh)
│ ├── ssr-sync # Backup to cloud │ ├── ssr-sync # Backup to cloud
│ ├── ssr-restore # Rebuild a Mac from the backup │ ├── ssr-restore # Rebuild a Mac from the backup
│ ├── ssr-schedule # launchd on/off/status │ ├── ssr-schedule # launchd on/off/status
+2 -1
View File
@@ -29,6 +29,7 @@ Commands:
install First-time install: links binary, seeds config, schedules sync. install First-time install: links binary, seeds config, schedules sync.
sync Run a backup: brew bundle dump + mackup backup → cloud. sync Run a backup: brew bundle dump + mackup backup → cloud.
restore [Brewfile] Restore a machine: brew bundle + mackup restore + macOS defaults. restore [Brewfile] Restore a machine: brew bundle + mackup restore + macOS defaults.
update [--dry-run] Pull latest ssr code, refresh launchd if needed.
schedule on|off Enable/disable the scheduled launchd sync job. schedule on|off Enable/disable the scheduled launchd sync job.
config show|edit Print or edit the active config file. config show|edit Print or edit the active config file.
status Show last sync time, cloud target, and scheduled state. status Show last sync time, cloud target, and scheduled state.
@@ -60,7 +61,7 @@ done
cmd="$1"; shift cmd="$1"; shift
case "$cmd" in case "$cmd" in
install|sync|restore|schedule|config|status) install|sync|restore|update|schedule|config|status)
target="$SSR_BIN_DIR/ssr-$cmd" target="$SSR_BIN_DIR/ssr-$cmd"
[[ -x "$target" ]] || ssr::die "Subcommand not executable: $target" [[ -x "$target" ]] || ssr::die "Subcommand not executable: $target"
exec "$target" "$@" exec "$target" "$@"
Executable
+117
View File
@@ -0,0 +1,117 @@
#!/usr/bin/env bash
# ssr-update — pull the latest ssr code from origin and refresh side-effects.
#
# Behavior:
# 1. Verify the repo is a clean git checkout on a real branch.
# 2. git fetch + fast-forward pull. Bail if FF would be impossible.
# 3. Report VERSION delta.
# 4. If the launchd template or any bin/ file changed and the schedule is
# currently active, re-render and reload it.
# 5. If config/ssr.conf.example gained new keys, warn the user.
set -euo pipefail
SSR_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck source=../lib/common.sh
source "$SSR_ROOT/lib/common.sh"
DRY_RUN=false
while [[ $# -gt 0 ]]; do
case "$1" in
-n|--dry-run) DRY_RUN=true; shift ;;
-h|--help)
cat <<EOF
Usage: ssr update [--dry-run]
Pulls the latest commit on the current branch and re-applies side effects:
- Re-renders & reloads the launchd plist if it changed (when schedule is active).
- Reports added config keys in config/ssr.conf.example.
EOF
exit 0 ;;
*) ssr::die "Unknown flag: $1" ;;
esac
done
ssr::require_cmd git
# --- 1. preflight ---------------------------------------------------------
[[ -d "$SSR_ROOT/.git" ]] \
|| ssr::die "Not a git checkout: $SSR_ROOT (re-clone the repo to enable updates)"
cd "$SSR_ROOT"
branch="$(git symbolic-ref --quiet --short HEAD 2>/dev/null || true)"
[[ -n "$branch" ]] || ssr::die "Detached HEAD — checkout a branch first"
if [[ -n "$(git status --porcelain)" ]]; then
ssr::die "Working tree has local changes — commit, stash, or revert before updating"
fi
origin_url="$(git remote get-url origin 2>/dev/null || true)"
[[ -n "$origin_url" ]] || ssr::die "No 'origin' remote configured"
ssr::log "Updating from $origin_url ($branch)"
# --- 2. fetch + ff pull ---------------------------------------------------
old_sha="$(git rev-parse HEAD)"
old_version="$(cat VERSION 2>/dev/null || echo unknown)"
if $DRY_RUN; then
git fetch --quiet origin "$branch"
new_sha="$(git rev-parse "origin/$branch")"
else
git fetch --quiet origin "$branch"
new_sha="$(git rev-parse "origin/$branch")"
if [[ "$old_sha" == "$new_sha" ]]; then
ssr::ok "Already up to date ($(git rev-parse --short HEAD))"
exit 0
fi
if ! git merge-base --is-ancestor "$old_sha" "$new_sha"; then
ssr::die "Cannot fast-forward (history diverged). Resolve manually: git pull --rebase"
fi
git merge --ff-only --quiet "origin/$branch"
fi
new_version="$(cat VERSION 2>/dev/null || echo unknown)"
if [[ "$old_sha" == "$new_sha" ]]; then
ssr::ok "No changes available."
exit 0
fi
# --- 3. summarize -----------------------------------------------------
ssr::ok "Updated $(git rev-parse --short "$old_sha") → $(git rev-parse --short "$new_sha")"
if [[ "$old_version" != "$new_version" ]]; then
ssr::log "Version: $old_version → $new_version"
fi
ssr::log "Changes:"
git --no-pager log --oneline "$old_sha..$new_sha" | sed 's/^/ /'
$DRY_RUN && { ssr::log "(dry-run: no side effects executed)"; exit 0; }
# --- 4. launchd refresh -----------------------------------------------
changed_files="$(git --no-pager diff --name-only "$old_sha" "$new_sha")"
plist_changed=false
case "$changed_files" in
*launchd/*|*bin/ssr*) plist_changed=true ;;
esac
label="${SSR_LAUNCHD_LABEL:-de.surke.ssr.sync}"
if $plist_changed && launchctl list 2>/dev/null | grep -q "$label"; then
ssr::log "launchd-relevant files changed → reloading schedule"
"$SSR_ROOT/bin/ssr-schedule" on
fi
# --- 5. config drift hint -------------------------------------------------
example="$SSR_ROOT/config/ssr.conf.example"
user_cfg="${SSR_CONFIG:-$HOME/.config/ssr/ssr.conf}"
if [[ -f "$user_cfg" && -f "$example" ]]; then
new_keys="$(comm -23 \
<(grep -E '^[A-Z_][A-Z0-9_]*=' "$example" | cut -d= -f1 | sort -u) \
<(grep -E '^[A-Z_][A-Z0-9_]*=' "$user_cfg" | cut -d= -f1 | sort -u))"
if [[ -n "$new_keys" ]]; then
ssr::warn "New config keys available in $example:"
echo "$new_keys" | sed 's/^/ /'
echo "Review with: ssr config edit"
fi
fi
ssr::ok "Update complete."