Files
timmy-config/bin/nexus-merge-bot.sh
Alexander Whitestone d4c79d47a6 feat: add operational scripts and deploy.sh
- Moved all agent loop scripts into source control (bin/)
- claude-loop.sh, gemini-loop.sh, timmy-orchestrator.sh
- workforce-manager.py, agent-dispatch.sh, nexus-merge-bot.sh
- ops dashboard scripts (ops-panel, ops-helpers, ops-gitea)
- monitoring scripts (timmy-status, timmy-loopstat)
- deploy.sh: one-command overlay onto ~/.hermes/
- Updated README with sidecar architecture docs
- All loops now target the-nexus + autolora only
2026-03-25 10:05:55 -04:00

217 lines
6.4 KiB
Bash
Executable File

#!/usr/bin/env bash
# nexus-merge-bot.sh — Auto-review and auto-merge for the-nexus
# Polls open PRs. For each: clone, validate (HTML/JS/JSON/size), merge if clean.
# Runs as a loop. Squash-only. Linear history.
#
# Pattern: matches Timmy-time-dashboard merge policy.
# Pre-commit hooks + this bot are the gates. If gates pass, auto-merge.
set -uo pipefail
LOG_DIR="$HOME/.hermes/logs"
LOG="$LOG_DIR/nexus-merge-bot.log"
PIDFILE="$LOG_DIR/nexus-merge-bot.pid"
GITEA_URL="http://143.198.27.163:3000"
GITEA_TOKEN=$(cat "$HOME/.hermes/gitea_token_vps" 2>/dev/null)
REPO="Timmy_Foundation/the-nexus"
CHECK_INTERVAL=60 # 2 minutes
mkdir -p "$LOG_DIR"
# Single instance guard
if [ -f "$PIDFILE" ]; then
old_pid=$(cat "$PIDFILE")
if kill -0 "$old_pid" 2>/dev/null; then
echo "Merge bot already running (PID $old_pid)" >&2
exit 0
fi
fi
echo $$ > "$PIDFILE"
trap 'rm -f "$PIDFILE"' EXIT
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] MERGE-BOT: $*" >> "$LOG"
}
validate_pr() {
local pr_num="$1"
local work_dir="/tmp/nexus-validate-$$"
rm -rf "$work_dir"
# Get PR head branch
local pr_info
pr_info=$(curl -s --max-time 10 -H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_URL}/api/v1/repos/${REPO}/pulls/${pr_num}")
local head_ref
head_ref=$(echo "$pr_info" | python3 -c "import sys,json; print(json.loads(sys.stdin.read())['head']['ref'])" 2>/dev/null)
local mergeable
mergeable=$(echo "$pr_info" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('mergeable', False))" 2>/dev/null)
if [ "$mergeable" != "True" ]; then
log "PR #${pr_num}: not mergeable (conflicts), skipping"
echo "CONFLICT"
return 1
fi
# Clone and checkout the PR branch
git clone -q --depth 20 \
"http://Timmy:${GITEA_TOKEN}@143.198.27.163:3000/${REPO}.git" "$work_dir" 2>&1 | tail -5 >> "$LOG"
if [ ! -d "$work_dir/.git" ]; then
log "PR #${pr_num}: clone failed"
echo "CLONE_FAIL"
return 1
fi
cd "$work_dir" || return 1
# Fetch and checkout the PR branch
git fetch origin "$head_ref" 2>/dev/null && git checkout "$head_ref" 2>/dev/null
if [ $? -ne 0 ]; then
# Try fetching the PR ref directly
git fetch origin "pull/${pr_num}/head:pr-${pr_num}" 2>/dev/null && git checkout "pr-${pr_num}" 2>/dev/null
fi
local FAIL=0
# 1. HTML validation
if [ -f index.html ]; then
python3 -c "
import html.parser
class V(html.parser.HTMLParser):
pass
v = V()
v.feed(open('index.html').read())
" 2>/dev/null || { log "PR #${pr_num}: HTML validation failed"; FAIL=1; }
fi
# 2. JS syntax check (node --check)
for f in $(find . -name '*.js' -not -path './node_modules/*' 2>/dev/null); do
if command -v node >/dev/null 2>&1; then
if ! node --check "$f" 2>/dev/null; then
log "PR #${pr_num}: JS syntax error in $f"
FAIL=1
fi
fi
done
# 3. JSON validation
for f in $(find . -name '*.json' -not -path './node_modules/*' 2>/dev/null); do
if ! python3 -c "import json; json.load(open('$f'))" 2>/dev/null; then
log "PR #${pr_num}: invalid JSON in $f"
FAIL=1
fi
done
# 4. File size budget (500KB per JS file)
for f in $(find . -name '*.js' -not -path './node_modules/*' 2>/dev/null); do
local size
size=$(wc -c < "$f")
if [ "$size" -gt 512000 ]; then
log "PR #${pr_num}: $f exceeds 500KB budget (${size} bytes)"
FAIL=1
fi
done
# Cleanup
rm -rf "$work_dir"
if [ $FAIL -eq 0 ]; then
echo "PASS"
return 0
else
echo "FAIL"
return 1
fi
}
merge_pr() {
local pr_num="$1"
local result
result=$(curl -s --max-time 30 -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"Do":"squash","delete_branch_after_merge":true}' \
"${GITEA_URL}/api/v1/repos/${REPO}/pulls/${pr_num}/merge")
if echo "$result" | grep -q '"sha"'; then
log "PR #${pr_num}: MERGED (squash)"
return 0
elif echo "$result" | grep -q '"message"'; then
local msg
msg=$(echo "$result" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('message','unknown'))" 2>/dev/null)
log "PR #${pr_num}: merge failed: $msg"
return 1
fi
}
comment_pr() {
local pr_num="$1"
local body="$2"
curl -s --max-time 10 -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"body\": \"$body\"}" \
"${GITEA_URL}/api/v1/repos/${REPO}/issues/${pr_num}/comments" >/dev/null
}
log "Starting nexus merge bot (PID $$)"
while true; do
# Get open PRs
prs=$(curl -s --max-time 15 -H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_URL}/api/v1/repos/${REPO}/pulls?state=open&sort=newest&limit=20")
pr_count=$(echo "$prs" | python3 -c "import sys,json; print(len(json.loads(sys.stdin.buffer.read())))" 2>/dev/null || echo "0")
if [ "$pr_count" = "0" ] || [ -z "$pr_count" ]; then
log "No open PRs. Sleeping ${CHECK_INTERVAL}s"
sleep "$CHECK_INTERVAL"
continue
fi
log "Found ${pr_count} open PRs, validating..."
# Process PRs one at a time, oldest first (sequential merge)
pr_nums=$(echo "$prs" | python3 -c "
import sys, json
prs = json.loads(sys.stdin.buffer.read())
for p in prs:
print(p['number'])
" 2>/dev/null)
for pr_num in $pr_nums; do
log "Validating PR #${pr_num}..."
result=$(validate_pr "$pr_num")
case "$result" in
PASS)
log "PR #${pr_num}: validation passed, merging..."
comment_pr "$pr_num" "🤖 **Merge Bot**: CI validation passed (HTML, JS syntax, JSON, size budget). Auto-merging."
merge_pr "$pr_num"
# Wait a beat for Gitea to process
sleep 5
;;
CONFLICT)
# Auto-close stale conflicting PRs — don't let them pile up
log "PR #${pr_num}: conflicts, closing"
comment_pr "$pr_num" "🤖 **Merge Bot**: Merge conflicts with main. Closing. The issue remains open — next agent cycle will pick it up fresh."
curl -s --max-time 5 -X PATCH \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"state":"closed"}' \
"${GITEA_URL}/api/v1/repos/${REPO}/pulls/${pr_num}" >/dev/null 2>&1
;;
FAIL)
comment_pr "$pr_num" "🤖 **Merge Bot**: CI validation failed. Check the merge-bot log for details."
;;
*)
log "PR #${pr_num}: unknown result: $result"
;;
esac
done
log "Cycle complete. Sleeping ${CHECK_INTERVAL}s"
sleep "$CHECK_INTERVAL"
done