|
| 1 | +#!/bin/bash |
| 2 | +# monitor-commit-freshness.sh — alert if RiveryIO/complexity-analyzer's GitHub |
| 3 | +# `main` branch has not received a commit within MAX_AGE_HOURS. |
| 4 | +# |
| 5 | +# WHY THE REMOTE, NOT LOCAL: the daily sync (com.user.complexity-sync) keeps |
| 6 | +# committing locally even when its `git push` is broken, so the local repo |
| 7 | +# always looks "fresh". Only the *remote* tip reveals a dead push. This is |
| 8 | +# exactly the failure that hid for 7 days in June 2026 (the gh token in |
| 9 | +# ~/.config/gh-token expired and every scheduled push silently 403'd). |
| 10 | +# |
| 11 | +# Exit codes: 0 = fresh, 1 = stale (alerted), 2 = could not verify (alerted). |
| 12 | +set -u |
| 13 | + |
| 14 | +export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin" |
| 15 | + |
| 16 | +# Same CA bundle the sync job uses (corporate TLS interception). git needs |
| 17 | +# GIT_SSL_CAINFO; without it https to github fails under launchd. |
| 18 | +CA_BUNDLE="/Users/ohadperry/.ssl/combined_certs.pem" |
| 19 | +[ -r "$CA_BUNDLE" ] && export GIT_SSL_CAINFO="$CA_BUNDLE" |
| 20 | + |
| 21 | +REPO_DIR="/Users/ohadperry/Documents/Dev/complexity-analyzer" |
| 22 | +REMOTE_HOST_PATH="github.com/RiveryIO/complexity-analyzer.git" |
| 23 | +BRANCH="main" |
| 24 | +TOKEN_FILE="$HOME/.config/gh-token" |
| 25 | +# Library/Logs is NOT TCC-protected like ~/Documents, so launchd appends here |
| 26 | +# reliably (the sync job's ~/Documents log intermittently hits EINTR). |
| 27 | +LOG_FILE="$HOME/Library/Logs/complexity-monitor.log" |
| 28 | +MAX_AGE_HOURS="${MAX_AGE_HOURS:-36}" |
| 29 | +NOTIFIER="/opt/homebrew/bin/terminal-notifier" |
| 30 | + |
| 31 | +log() { echo "$(date '+%Y-%m-%d %H:%M:%S'): $*" >> "$LOG_FILE"; } |
| 32 | + |
| 33 | +notify() { # $1 = subtitle, $2 = message |
| 34 | + [ -x "$NOTIFIER" ] && "$NOTIFIER" \ |
| 35 | + -title "⚠️ complexity-analyzer not pushing" \ |
| 36 | + -subtitle "$1" \ |
| 37 | + -message "$2" \ |
| 38 | + -group "complexity-monitor" \ |
| 39 | + -sound default >/dev/null 2>&1 |
| 40 | +} |
| 41 | + |
| 42 | +cd "$REPO_DIR" 2>/dev/null || { log "FATAL: cannot cd to $REPO_DIR"; notify "Monitor error" "Cannot access $REPO_DIR"; exit 2; } |
| 43 | + |
| 44 | +# Authenticated URL — the osxkeychain helper is unavailable under launchd, so we |
| 45 | +# use the same token file the sync job pushes with. |
| 46 | +if [ -r "$TOKEN_FILE" ]; then |
| 47 | + TOKEN=$(tr -d '\n\r' < "$TOKEN_FILE") |
| 48 | + REMOTE_URL="https://x-access-token:${TOKEN}@${REMOTE_HOST_PATH}" |
| 49 | +else |
| 50 | + REMOTE_URL="https://${REMOTE_HOST_PATH}" |
| 51 | +fi |
| 52 | + |
| 53 | +# Remote tip SHA, without mutating any local ref. |
| 54 | +REMOTE_SHA=$(/usr/bin/git ls-remote "$REMOTE_URL" "refs/heads/$BRANCH" 2>>"$LOG_FILE" | cut -f1) |
| 55 | +if [ -z "$REMOTE_SHA" ]; then |
| 56 | + log "UNVERIFIED: ls-remote returned nothing (expired token or no network?)" |
| 57 | + notify "Cannot verify remote" "ls-remote failed — token expired or offline?" |
| 58 | + exit 2 |
| 59 | +fi |
| 60 | + |
| 61 | +# The remote tip is normally already an ancestor of local main, so read its date |
| 62 | +# locally without fetching; otherwise fetch just that branch into FETCH_HEAD. |
| 63 | +COMMIT_EPOCH=$(/usr/bin/git log -1 --format=%ct "$REMOTE_SHA" 2>/dev/null) |
| 64 | +if [ -z "$COMMIT_EPOCH" ]; then |
| 65 | + /usr/bin/git fetch --quiet "$REMOTE_URL" "$BRANCH" 2>>"$LOG_FILE" \ |
| 66 | + && COMMIT_EPOCH=$(/usr/bin/git log -1 --format=%ct FETCH_HEAD 2>/dev/null) |
| 67 | +fi |
| 68 | +if [ -z "$COMMIT_EPOCH" ]; then |
| 69 | + log "UNVERIFIED: resolved remote tip $REMOTE_SHA but could not read its commit date" |
| 70 | + notify "Cannot verify remote" "Got SHA ${REMOTE_SHA:0:7} but no commit date" |
| 71 | + exit 2 |
| 72 | +fi |
| 73 | + |
| 74 | +NOW=$(date +%s) |
| 75 | +AGE_HOURS=$(( (NOW - COMMIT_EPOCH) / 3600 )) |
| 76 | +SHORT_SHA=${REMOTE_SHA:0:7} |
| 77 | +COMMIT_WHEN=$(date -r "$COMMIT_EPOCH" '+%Y-%m-%d %H:%M') |
| 78 | + |
| 79 | +if [ "$AGE_HOURS" -gt "$MAX_AGE_HOURS" ]; then |
| 80 | + log "STALE: GitHub $BRANCH tip $SHORT_SHA is ${AGE_HOURS}h old (> ${MAX_AGE_HOURS}h), last commit $COMMIT_WHEN" |
| 81 | + notify "No push for ${AGE_HOURS}h" "GitHub $BRANCH @ $SHORT_SHA, last commit $COMMIT_WHEN" |
| 82 | + exit 1 |
| 83 | +fi |
| 84 | + |
| 85 | +log "OK: GitHub $BRANCH tip $SHORT_SHA is ${AGE_HOURS}h old (<= ${MAX_AGE_HOURS}h), last commit $COMMIT_WHEN" |
| 86 | +exit 0 |
0 commit comments