From 5c467066dbc71ea2d3b853765d75c26d3f9d4c0b Mon Sep 17 00:00:00 2001 From: Hillbillyer Date: Fri, 20 Feb 2026 17:40:44 +1100 Subject: [PATCH] migrated from github --- README.md | 3 + health-check/health-check.sh | 67 ++++++++++++++ mac-update.sh | 174 +++++++++++++++++++++++++++++++++++ ubuntu-update.sh | 133 ++++++++++++++++++++++++++ 4 files changed, 377 insertions(+) create mode 100644 health-check/health-check.sh create mode 100755 mac-update.sh create mode 100644 ubuntu-update.sh diff --git a/README.md b/README.md index e69de29..4237adf 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,3 @@ +# scripts + +zsh <(curl -fsSL https://raw.githubusercontent.com/hillbillyer/scripts/main/mac-update.sh) diff --git a/health-check/health-check.sh b/health-check/health-check.sh new file mode 100644 index 0000000..107085f --- /dev/null +++ b/health-check/health-check.sh @@ -0,0 +1,67 @@ +#!/bin/bash + +# ============================== +# Configuration +# ============================== + +NTFY_URL="https://ntfy.hillbillyer.dev/health-alert" +DISK_THRESHOLD=95 +RAM_THRESHOLD=90 +STATE_FILE="$HOME/hillbillyer/health-check/proxmox_health_state" + +HOSTNAME=$(hostname) + +# ============================== +# Disk Check (Ignore tmpfs, etc.) +# ============================== + +disk_alert=0 +disk_message="" + +while read -r source fstype size used avail pcent mount; do + usage_percent=$(echo "$pcent" | sed 's/%//') + + # Skip unwanted filesystem types + if [[ "$fstype" =~ ^(tmpfs|devtmpfs|overlay|squashfs)$ ]]; then + continue + fi + + if [ "$usage_percent" -ge "$DISK_THRESHOLD" ]; then + disk_alert=1 + disk_message+="Disk alert on $HOSTNAME: $mount ($fstype) is ${pcent} full\n" + fi +done < <(df -hT | tail -n +2) + +# ============================== +# RAM Check +# ============================== + +ram_used_percent=$(free | awk '/Mem:/ {printf("%.0f"), $3/$2 * 100}') + +ram_alert=0 +ram_message="" + +if [ "$ram_used_percent" -ge "$RAM_THRESHOLD" ]; then + ram_alert=1 + ram_message="RAM alert on $HOSTNAME: Memory usage is ${ram_used_percent}%\n" +fi + +# ============================== +# State Handling (Prevent Spam) +# ============================== + +previous_state="" +[ -f "$STATE_FILE" ] && previous_state=$(cat "$STATE_FILE") + +current_state="disk:$disk_alert ram:$ram_alert" + +if [ "$current_state" != "$previous_state" ]; then + if [ "$disk_alert" -eq 1 ] || [ "$ram_alert" -eq 1 ]; then + message="${disk_message}${ram_message}" + echo -e "$message" | curl -s \ + -H "Title: Usage Alert on $HOSTNAME" \ + -H "Priority: urgent" \ + -d @- "$NTFY_URL" + fi + echo "$current_state" > "$STATE_FILE" +fi diff --git a/mac-update.sh b/mac-update.sh new file mode 100755 index 0000000..0891dfc --- /dev/null +++ b/mac-update.sh @@ -0,0 +1,174 @@ +#!/bin/zsh +# Interactive arrow-key menu for choosing a macOS full installer version. +# Works on Apple Silicon & Intel, no extra deps. + +set -euo pipefail +export LC_ALL=C + +# --- normalize versions into X.Y.Z --- +normalize_ver() { + local v="${1//[^0-9.]/}" # strip non-numeric/dots + local -a p + p=(${(s/./)v}) + printf "%d.%d.%d" "${p[1]:-0}" "${p[2]:-0}" "${p[3]:-0}" +} + +# --- compare versions: returns 1 if v1>v2, 0 if equal, -1 if v1 ${B[i]} )) && { echo 1; return; } + (( ${A[i]} < ${B[i]} )) && { echo -1; return; } + done + echo 0 +} +version_ge() { [[ "$(version_cmp "$1" "$2")" != "-1" ]]; } +version_gt() { [[ "$(version_cmp "$1" "$2")" == "1" ]]; } + +# --- current version --- +current_version_raw=$(sw_vers -productVersion) +current_version=$(normalize_ver "$current_version_raw") + +autoload -Uz colors && colors + +hide_cursor() { printf '\e[?25l'; } +show_cursor() { printf '\e[?25h'; } +clear_screen() { printf '\e[2J\e[H'; } +cleanup() { stty "$_STTY_ORIG" 2>/dev/null || true; show_cursor; printf '\e[0m'; } +trap cleanup EXIT INT TERM + +if [[ -t 0 && -t 1 ]]; then + _STTY_ORIG=$(stty -g) + stty -echo -icanon min 1 time 0 +fi + +# --- fetch list --- +raw=$(/usr/sbin/softwareupdate --list-full-installers 2>/dev/null) +lines=("${(@f)$(printf "%s\n" "$raw" | grep -E '^\* Title: ')}") +(( ${#lines[@]} == 0 )) && { echo "No installers found."; exit 1; } + +parsed=$( + printf "%s\n" "${lines[@]}" | /usr/bin/awk ' + /^\* Title:/ { + version=""; title=""; build=""; size=""; + if (match($0, /^\* Title: [^,]*/)) { title = substr($0, RSTART+9, RLENGTH-9) } + if (match($0, /Version: [0-9][0-9.]*/)) { version = substr($0, RSTART+9, RLENGTH-9) } + if (match($0, /Build: [^,]*/)) { build = substr($0, RSTART+7, RLENGTH-7) } + if (match($0, /Size: [0-9]+KiB/)) { size = substr($0, RSTART+6, RLENGTH-6) } + printf("%s\t%s\t%s\t%s\n", version, title, build, size); + }' +) + +typeset -a versions versions_norm titles builds sizes +for row in "${(@f)parsed}"; do + IFS=$'\t' read -r v t b s <<< "$row" + [[ -n "$v" ]] || continue + if ! version_ge "$v" "$current_version"; then + continue + fi + versions+=("$v") + versions_norm+=("$(normalize_ver "$v")") + titles+=("$t") + builds+=("$b") + sizes+=("$s") +done +(( ${#versions[@]} == 0 )) && { echo "No newer/equal versions found."; exit 1; } + +human_gib() { + local in="$1" n + [[ "$in" == *KiB ]] && n="${in%KiB}" || n="$in" + [[ "$n" == <-> ]] || { printf "%s" "$in"; return; } + /usr/bin/awk -v kib="$n" 'BEGIN { printf("%.2f GiB", kib/1048576) }' +} + +# --- UI state --- +pos=1; count=${#versions[@]} +: ${LINES:=24}; : ${COLUMNS:=80} +min_visible=5; visible=$(( LINES - 6 )) +(( visible < min_visible )) && visible=$min_visible +(( visible > count )) && visible=$count +start=1 + +make_divider() { + # nice clean ASCII divider + printf '%*s' "$COLUMNS" '' | tr ' ' '-' +} + +draw() { + clear_screen + local divider="$(make_divider)" + + print -P "%F{yellow} Current macOS: ${current_version_raw} | Select macOS version | ↑/↓ move | Enter select | q quit %f" + print -r -- "$divider" + + (( pos < start )) && start=$pos + (( pos >= start + visible )) && start=$(( pos - visible + 1 )) + (( start < 1 )) && start=1 + + local end=$(( start + visible - 1 )) + (( end > count )) && end=$count + + for ((i=start; i<=end; i++)); do + if (( i == pos )); then + printf "\e[7m➤ %-*s\e[0m\n" $((COLUMNS-4)) "${versions[i]}" + else + printf " %-*s\n" $((COLUMNS-4)) "${versions[i]}" + fi + done + + print -r -- "$divider" + local sg=$(human_gib "${sizes[pos]}") + print -P "%F{yellow}Version:%f ${versions[pos]} %F{yellow}Title:%f ${titles[pos]} %F{yellow}Build:%f ${builds[pos]} %F{yellow}Size:%f ${sg}" +} + +hide_cursor +while true; do + draw + read -k 1 key || key="" + case "$key" in + $'\n'|$'\r') break ;; + q|Q) cleanup; echo "No selection."; exit 2 ;; + k) (( pos > 1 )) && (( pos-- )) ;; + j) (( pos < count )) && (( pos++ )) ;; + $'\e') + read -k 1 -t 0.05 key2 || key2="" + if [[ "$key2" == "[" ]]; then + read -k 1 -t 0.05 key3 || key3="" + case "$key3" in + A) (( pos > 1 )) && (( pos-- )) ;; + B) (( pos < count )) && (( pos++ )) ;; + H) pos=1 ;; + F) pos=$count ;; + esac + fi + ;; + esac +done + +show_cursor +selected_version="${versions[pos]}" +selected_version_norm="${versions_norm[pos]}" +echo "$selected_version" + +# Final sanity check +if ! version_ge "$selected_version" "$current_version"; then + print -P "%F{red}Selected version ($selected_version) is older than current ($current_version_raw). Aborting.%f" + exit 3 +fi + +# Major vs minor +selected_major="${${(s/./)selected_version_norm}[1]}" +current_major="${${(s/./)current_version}[1]}" + +if [[ "$selected_major" == "$current_major" ]]; then + print -P "%F{green}Minor update to $selected_version...%f" + sudo softwareupdate --install --all --force --restart +else + print -P "%F{cyan}Major upgrade to $selected_version...%f" + sudo softwareupdate --fetch-full-installer --full-installer-version "$selected_version" + current_user=$(stat -f%Su /dev/console) + sudo /Applications/Install\ macOS\ *.app/Contents/Resources/startosinstall --agreetolicense --nointeraction --rebootdelay 10 --forcequitapps --user "$current_user" --passprompt +fi \ No newline at end of file diff --git a/ubuntu-update.sh b/ubuntu-update.sh new file mode 100644 index 0000000..1cac282 --- /dev/null +++ b/ubuntu-update.sh @@ -0,0 +1,133 @@ +#!/bin/bash + +# Setup +mkdir -p "$HOME/hillbillyer/health-check" +LOGFILE="$HOME/hillbillyer/ubuntu-updates.log" +ERRFILE=$(mktemp) +UPGRADES_TMP=$(mktemp) +NTFY_TOPIC="https://ntfy.hillbillyer.dev/machine-updates" +HOSTNAME=$(hostname) +touch $HOME/hillbillyer/custom-commands.sh +CUSTOM_SCRIPT="$HOME/hillbillyer/custom-commands.sh" +UPDATE_PATH="$HOME/update.sh" +HEALTH_PATH="$HOME/hillbillyer/health-check/health-check.sh" + +# Logging start +{ + echo "===== $(date '+%Y-%m-%d %H:%M:%S') =====" + echo "Running apt update && full-upgrade on $HOSTNAME" +} >> "$LOGFILE" + +# Refresh package list +apt update >> "$LOGFILE" 2>>"$ERRFILE" + +# ============================== +# Update health-check folder from GitHub +# ============================== + +apt install unzip -y +REPO_ZIP="https://github.com/hillbillyer/scripts/archive/refs/heads/main.zip" +TARGET_DIR="$HOME/hillbillyer" +FOLDER_NAME="health-check" +TMP_DIR=$(mktemp -d) + +# Download the repo ZIP into a temporary folder +curl -L -o "$TMP_DIR/repo.zip" "$REPO_ZIP" + +# Extract only the health-check folder from the ZIP +unzip -q "$TMP_DIR/repo.zip" "scripts-main/$FOLDER_NAME/*" -d "$TMP_DIR" + +# Remove existing health-check folder on the server +rm -rf "$TARGET_DIR/$FOLDER_NAME" + +# Move the new health-check folder into place +mv "$TMP_DIR/scripts-main/$FOLDER_NAME" "$TARGET_DIR/" + +# Clean up temp folder +rm -rf "$TMP_DIR" + +echo "Updated $TARGET_DIR/$FOLDER_NAME from GitHub." + +# Capture list of upgradable packages BEFORE full-upgrade +UPGRADES=$(apt list --upgradable 2>/dev/null | awk -F/ 'NR>1 {print $1}' | paste -sd, -) +echo "Will upgrade: ${UPGRADES:-}" >> "$LOGFILE" + +# Run upgrades +{ + apt full-upgrade -y + apt autoremove -y + apt clean +} >> "$LOGFILE" 2>>"$ERRFILE" + +APT_SUCCESS=$? + +# Custom Commands Section +CUSTOM_SUCCESS=0 +CUSTOM_OUTPUT="" + +if [ -f "$CUSTOM_SCRIPT" ]; then + echo "Running custom update script: $CUSTOM_SCRIPT" >> "$LOGFILE" + CUSTOM_OUTPUT=$(bash "$CUSTOM_SCRIPT" 2>&1) + CUSTOM_SUCCESS=$? + { + echo "Custom script output:" + echo "$CUSTOM_OUTPUT" + } >> "$LOGFILE" +fi + +# Compose NTFY message +if [ $APT_SUCCESS -eq 0 ] && [ $CUSTOM_SUCCESS -eq 0 ]; then + if [ -n "$UPGRADES" ]; then + MESSAGE="✅ $HOSTNAME updated successfully. Updated: $UPGRADES" + else + MESSAGE="✅ $HOSTNAME updated successfully. No packages were updated." + fi + if [ -f "$CUSTOM_SCRIPT" ]; then + MESSAGE="$MESSAGE (Custom script ran successfully.)" + fi +elif [ $APT_SUCCESS -ne 0 ]; then + ERROR_MSG=$(<"$ERRFILE") + MESSAGE="❌ $HOSTNAME apt update/upgrade failed: $ERROR_MSG" +elif [ $CUSTOM_SUCCESS -ne 0 ]; then + MESSAGE="❌ $HOSTNAME custom update script failed." +fi + +# Send notification +curl -s -X POST -H "Title: Server Update" -d "$MESSAGE" "$NTFY_TOPIC" >/dev/null + +# Run Health Check +bash $HOME/hillbillyer/health-check/health-check.sh + +# ============================== +# Update Cron Jobs (Tagged Only) +# ============================== + +UPDATE_JOB="0 3 * * * /usr/bin/bash $UPDATE_PATH # HILLBILLYER_UPDATE" +HEALTH_JOB="*/5 * * * * /usr/bin/bash $HEALTH_PATH # HILLBILLYER_HEALTH" + +TMP_FILE=$(mktemp) + +# Remove only our tagged jobs (leave everything else untouched) +crontab -l 2>/dev/null | \ +grep -v "# HILLBILLYER_UPDATE" | \ +grep -v "# HILLBILLYER_HEALTH" > "$TMP_FILE" + +# Add fresh tagged entries +echo "$UPDATE_JOB" >> "$TMP_FILE" +echo "$HEALTH_JOB" >> "$TMP_FILE" + +# Install updated crontab +crontab "$TMP_FILE" + +rm "$TMP_FILE" + +echo "Cron jobs replaced successfully." >> "$LOGFILE" + +# Append result to log +{ + echo "$MESSAGE" + echo "" +} >> "$LOGFILE" + +# Clean up +rm -f "$ERRFILE" "$UPGRADES_TMP"