Skip to content

Instantly share code, notes, and snippets.

@zudsniper
Last active May 13, 2025 07:56
Show Gist options
  • Save zudsniper/23956e948dd0666050da19417309aec9 to your computer and use it in GitHub Desktop.
Save zudsniper/23956e948dd0666050da19417309aec9 to your computer and use it in GitHub Desktop.
start/wait for time machine backup, then update mac
#!/usr/bin/env bash
#
# backup-and-update.sh
# 1) Prevent system sleep (via caffeinate)
# 2) Run (or detect) a Time Machine backup
# 3) Wait for the backup to finish
# 4) If the target macOS installer is present:
# • launch startosinstall (major upgrade → reboot)
# Else if the target macOS update is downloaded (from Software Update):
# • install it using softwareupdate command
# Else:
# • list & install all minor/security updates
# 5) Reboot (shutdown -r now)
#
# Options:
# --test Test mode - only log information without performing actions
#
# Environment Variables:
# TARGET_MACOS_NAME Name of the macOS version to update to (default: "Sequoia")
# TARGET_MACOS_VERSION Version number to update to (default: "15")
#
# Setup:
# • In ~/.env:
# DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/…"
# • chmod 600 ~/.env
# • chmod +x ~/bin/backup-and-update.sh
#
# Examples:
# ./backup-and-update.sh # Update to macOS Sequoia
# TARGET_MACOS_NAME=Sonoma ./backup-and-update.sh # Update to macOS Sonoma
# TARGET_MACOS_NAME="Ventura" TARGET_MACOS_VERSION="13" ./backup-and-update.sh # Update to macOS Ventura
set -euo pipefail
echo -ne "WARNING: make sure you've given the terminal you're executing this script with the \"Full Disk Access\" permission in the \"Security & Privacy\" Settings on your Mac. Otherwise, this will not work. Cheers.\n\n\n"
# Default target macOS version if not specified
TARGET_MACOS_NAME="${TARGET_MACOS_NAME:-Sequoia}"
TARGET_MACOS_VERSION="${TARGET_MACOS_VERSION:-15}"
# Parse arguments
TEST_MODE=false
for arg in "$@"; do
case $arg in
--test)
TEST_MODE=true
echo "🧪 TEST MODE ACTIVE - No installations or reboots will be performed"
echo "📋 Target macOS: $TARGET_MACOS_NAME (version $TARGET_MACOS_VERSION)"
;;
esac
done
#–– load discord webhook
if [ -f "${HOME}/.env" ]; then
# shellcheck source=/dev/null
source "${HOME}/.env"
fi
: "${DISCORD_WEBHOOK_URL:?Set DISCORD_WEBHOOK_URL in ~/.env}"
WEBHOOK_URL="$DISCORD_WEBHOOK_URL"
DISCORD_SCRIPT="/Users/jason/scripts/discord.sh/discord.sh"
#–– helper to send an embed
send_embed() {
local title="$1" desc="$2" color="${3:-3447003}"
# In test mode, only log the embed info
if [[ "$TEST_MODE" == true ]]; then
echo "📤 DISCORD EMBED (TEST MODE)"
echo " Title: $title"
echo " Description: $desc"
echo " Color: $color"
return 0
fi
# Escape special characters in the description
local escaped_desc
escaped_desc=$(echo -n "$desc" | perl -pe 's/\n/\\n/g; s/\r/\\r/g; s/\t/\\t/g')
# Use discord.sh correctly with its expected parameters
"$DISCORD_SCRIPT" \
--webhook-url="$WEBHOOK_URL" \
--title "$title" \
--description "$escaped_desc" \
--color "$color"
}
#–– prevent sleep (skipped in test mode)
if [[ "$TEST_MODE" == false ]]; then
caffeinate -dimsu &
CAFFE_PID=$!
trap 'kill "$CAFFE_PID"' EXIT
else
echo "🔋 TEST MODE: Skipping caffeinate process"
fi
#–– announce start
host_info="Host: $(hostname)
Time: $(date +'%Y-%m-%d %H:%M:%S')
macOS: $(sw_vers -productName) $(sw_vers -productVersion) ($(sw_vers -buildVersion))
Architecture: $(uname -m)
Target: macOS $TARGET_MACOS_NAME ($TARGET_MACOS_VERSION)"
send_embed "Backup & Update Started" "$host_info" 3447003
#–– 1) Time Machine
if [[ "$TEST_MODE" == true ]]; then
echo "💾 TEST MODE: Skipping actual Time Machine backup"
echo "Current Time Machine status:"
tmutil status
echo "Recent Time Machine backups:"
tmutil listbackups | tail -5
else
phase=$(tmutil currentphase 2>/dev/null || echo "NotRunning")
t0=$(date +%s)
if [[ "$phase" == "NotRunning" ]]; then
send_embed "Time Machine Backup" "Starting backup…" 15844367
tmutil startbackup --auto
else
send_embed "Time Machine Backup" "Already in progress (phase: $phase). Monitoring…" 15844367
fi
echo -n "Waiting for backup"
while true; do
phase=$(tmutil currentphase 2>/dev/null || echo "NotRunning")
if [[ "$phase" == "NotRunning" ]]; then
echo " ✔"
break
fi
echo -n "."
sleep 60
done
t1=$(date +%s)
send_embed "Backup Complete" \
"Duration: $((t1-t0))s
Finished at: $(date +'%Y-%m-%d %H:%M:%S')" \
3066993
fi
#–– 2) Check for macOS major upgrade methods
# First, check if we have the full installer app
installer_app=$(find /Applications -maxdepth 1 -type d -iname "Install macOS $TARGET_MACOS_NAME*.app" | head -n1)
# Print debug info in test mode
if [[ "$TEST_MODE" == true ]]; then
echo "🔍 CHECKING FOR $TARGET_MACOS_NAME INSTALLER APP"
if [[ -n "$installer_app" ]]; then
echo "✅ Found installer app: $installer_app"
echo " Last modified: $(stat -f '%Sm' "$installer_app")"
echo " Size: $(du -sh "$installer_app" | cut -f1)"
echo " Version info:"
plutil -p "$installer_app/Contents/Info.plist" | grep -E "CFBundleShortVersionString|CFBundleVersion"
echo " startosinstall path exists: $([[ -x "$installer_app/Contents/Resources/startosinstall" ]] && echo "Yes" || echo "No")"
else
echo "❌ No macOS $TARGET_MACOS_NAME installer app found in /Applications"
echo " Checking for other macOS installers:"
find /Applications -maxdepth 1 -type d -name "Install macOS*.app" | while read -r app; do
echo " • $(basename "$app")"
done
fi
fi
# Second, check if target update is available via softwareupdate
echo "🔍 CHECKING SOFTWARE UPDATE FOR $TARGET_MACOS_NAME"
softwareupdate_output=$(softwareupdate -l 2>&1)
if [[ "$TEST_MODE" == true ]]; then
echo "Software Update output:"
echo "$softwareupdate_output"
echo ""
fi
# Look for target macOS in Software Update output
target_update=$(echo "$softwareupdate_output" | grep -i "macOS $TARGET_MACOS_NAME" | awk '{$1=$1};1')
if [[ "$TEST_MODE" == true ]]; then
if [[ -n "$target_update" ]]; then
echo "✅ Found $TARGET_MACOS_NAME update in Software Update:"
echo "$target_update"
# Try to find more details about the downloaded update
echo "Checking /Library/Updates for downloaded packages:"
if [[ -d "/Library/Updates" ]]; then
find /Library/Updates -type d -name "*macOS*" -o -name "*$TARGET_MACOS_NAME*" | while read -r update_dir; do
echo " • $update_dir"
find "$update_dir" -type f -name "*.pkg" | while read -r pkg; do
echo " - $(basename "$pkg") ($(du -sh "$pkg" | cut -f1))"
done
done
fi
else
echo "❌ No macOS $TARGET_MACOS_NAME update found in Software Update"
fi
fi
# Get the exact update label if target update is found
update_label=""
if [[ -n "$target_update" ]]; then
# Extract the update label correctly by removing the "Label: " prefix
update_label=$(echo "$softwareupdate_output" | grep -i "macOS $TARGET_MACOS_NAME" | grep "Label:" | sed 's/^* Label: //g' | awk '{$1=$1};1')
if [[ "$TEST_MODE" == true ]]; then
echo "📝 Update label: '$update_label'"
if [[ -n "$update_label" ]]; then
echo "Update command that would be executed:"
echo "sudo softwareupdate --install \"$update_label\" --restart --no-scan --agree-to-license"
else
echo "⚠️ Could not determine the exact update label from Software Update output"
fi
fi
fi
# Now proceed with the actual logic
if [[ -n "$installer_app" ]]; then
# Method 1: Use the full installer app if present
send_embed "Major Upgrade" \
"Found installer $(basename "$installer_app")—launching upgrade to macOS $TARGET_MACOS_NAME now (this will reboot)…" \
10181046
if [[ "$TEST_MODE" == true ]]; then
echo "🔄 TEST MODE: Would execute the following command:"
echo "sudo \"$installer_app/Contents/Resources/startosinstall\" --agreetolicense --nointeraction"
else
sudo "$installer_app/Contents/Resources/startosinstall" \
--agreetolicense --nointeraction
fi
if [[ "$TEST_MODE" == true ]]; then
echo "👋 TEST MODE: Script would exit here after launching the installer"
else
exit 0
fi
elif [[ -n "$target_update" && -n "$update_label" ]]; then
# Method 2: Use the already downloaded target update from Software Update
send_embed "Major Upgrade" \
"Found downloaded macOS $TARGET_MACOS_NAME update—installing now (this will reboot)…" \
10181046
send_embed "Installing macOS $TARGET_MACOS_NAME" "Installing update: $update_label" 10181046
u0=$(date +%s)
if [[ "$TEST_MODE" == true ]]; then
echo "🔄 TEST MODE: Would execute the following command:"
echo "sudo softwareupdate --install \"$update_label\" --restart --no-scan --agree-to-license"
else
# For both Apple Silicon and Intel Macs
sudo softwareupdate --install "$update_label" --restart --no-scan --agree-to-license
# Note: This might not execute if the update immediately restarts the Mac
u1=$(date +%s)
send_embed "macOS $TARGET_MACOS_NAME Installing" \
"Installation started. Duration: $((u1-u0))s
Your Mac will restart automatically to complete installation." \
3066993
fi
if [[ "$TEST_MODE" == true ]]; then
echo "👋 TEST MODE: Script would exit here after starting the update"
else
exit 0
fi
elif [[ -n "$target_update" && -z "$update_label" ]]; then
send_embed "Update Error" "Could not determine the exact update label for macOS $TARGET_MACOS_NAME. Falling back to normal updates." 15158332
if [[ "$TEST_MODE" == true ]]; then
echo "⚠️ TEST MODE: Could not determine the exact update label. Would fall back to normal updates."
fi
fi
#–– 3) Minor/security updates if no major update was found or installed
send_embed "Listing Updates" "Running softwareupdate --list…" 15844367
if [[ "$TEST_MODE" == true ]]; then
echo "🔍 TEST MODE: Available updates:"
echo "$softwareupdate_output"
else
updates=$(softwareupdate --list 2>&1)
send_embed "Available Updates" \
"$(echo -n "```"; echo; echo -n "$updates"; echo; echo -n "```")" \
15105570
send_embed "Installing Updates" "Installing all available updates…" 10181046
u0=$(date +%s)
sudo softwareupdate --install --all --restart
u1=$(date +%s)
# This might not execute if the update immediately restarts the Mac
send_embed "Updates Installed" \
"Duration: $((u1-u0))s
Rebooting now…" \
3066993
fi
#–– 4) reboot if not already triggered by the update
if [[ "$TEST_MODE" == true ]]; then
echo "🔄 TEST MODE: Would restart the Mac here"
echo "✅ Test completed successfully. Run without --test to perform the actual update."
else
sudo shutdown -r now
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment