Last active
May 13, 2025 07:56
-
-
Save zudsniper/23956e948dd0666050da19417309aec9 to your computer and use it in GitHub Desktop.
start/wait for time machine backup, then update mac
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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