Skip to content

Commit 8dc2dd1

Browse files
chore(cron): add commit-freshness monitor + harden daily sync
Add scripts/monitor-commit-freshness.sh (LaunchAgent com.user.complexity-monitor, daily 13:00): checks the GitHub *remote* main tip age and alerts via terminal-notifier if older than MAX_AGE_HOURS (default 36). Watches the remote, not local, because the sync keeps committing locally even when its push is dead — which is exactly how a stranded-push regression hid for ~7 days. Harden scripts/launchd-sync.sh: - self-locate REPO_DIR from the script path instead of a hardcoded dir (a hardcoded path is how a stale ~/dev copy ended up committing into ~/Documents) - log to ~/Library/Logs (not TCC-protected ~/Documents, whose launchd append intermittently failed with EINTR) - push to the named 'origin' remote via a one-shot HTTP auth header so the origin/main tracking ref advances (the old ad-hoc token URL left git status perpetually "ahead N"); token injected transiently, never persisted Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ac8c3c6 commit 8dc2dd1

2 files changed

Lines changed: 102 additions & 6 deletions

File tree

scripts/launchd-sync.sh

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,14 @@ export TIKTOKEN_CACHE_DIR="/Users/ohadperry/.cache/tiktoken-persistent"
1212
export SSL_CERT_FILE="/Users/ohadperry/.ssl/combined_certs.pem"
1313
export REQUESTS_CA_BUNDLE="/Users/ohadperry/.ssl/combined_certs.pem"
1414

15-
REPO_DIR="/Users/ohadperry/Documents/Dev/complexity-analyzer"
16-
LOG_FILE="$REPO_DIR/logs/launchd-sync.log"
15+
# Self-locating: operate on the repo this script actually lives in, never a
16+
# hardcoded path. (A hardcoded REPO_DIR is how an old ~/dev copy ended up
17+
# committing into ~/Documents/Dev — see scripts/monitor-commit-freshness.sh.)
18+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
19+
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
20+
# Log to ~/Library/Logs (NOT ~/Documents — that path is TCC-protected and the
21+
# launchd append intermittently fails with EINTR / "Interrupted system call").
22+
LOG_FILE="$HOME/Library/Logs/complexity-sync.log"
1723
TOKEN_FILE="$HOME/.config/gh-token"
1824

1925
cd "$REPO_DIR"
@@ -55,12 +61,16 @@ if [ -n "$changed_files" ]; then
5561
/usr/bin/git add $changed_files
5662
/usr/bin/git commit -m "chore: daily sync — $labeled new PRs labeled, $jira_new jira features added (total PRs: $total)" >> "$LOG_FILE" 2>&1
5763

58-
# Push using token from file — osxkeychain credential helper is unreliable
59-
# from launchd. Fall back to origin remote if token file missing.
64+
# Push to the NAMED 'origin' remote so the local origin/main tracking ref
65+
# advances (an ad-hoc token URL pushes fine but leaves `git status` stuck at
66+
# "ahead N" forever). osxkeychain is unavailable under launchd, so the token
67+
# is injected as a one-shot HTTP Authorization header — never persisted to
68+
# .git/config and not embedded in the remote URL.
6069
if [ -r "$TOKEN_FILE" ]; then
6170
GH_TOKEN_VALUE=$(tr -d '\n\r' < "$TOKEN_FILE")
62-
PUSH_URL="https://x-access-token:${GH_TOKEN_VALUE}@github.com/RiveryIO/complexity-analyzer.git"
63-
if /usr/bin/git push "$PUSH_URL" main >> "$LOG_FILE" 2>&1; then
71+
AUTH_B64=$(printf 'x-access-token:%s' "$GH_TOKEN_VALUE" | base64 | tr -d '\n')
72+
if /usr/bin/git -c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${AUTH_B64}" \
73+
push origin main >> "$LOG_FILE" 2>&1; then
6474
push_status="pushed"
6575
else
6676
push_status="push-failed"
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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

Comments
 (0)