add SREBOT, SHARED, TSSBOT contents (fixup for #1223)

PR #1223 only staged the deletions of the old paths because the new
top-level directories were still untracked when the commit was authored.
This commit adds the actual restructured tree: SREBOT/ (existing bot),
SHARED/ (vromfs, data_parser, ICONS/MAPS/FONTS, DAGOR_FILES,
update_game_files), and TSSBOT/ (skeleton).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
FURRO404
2026-05-13 23:17:02 -07:00
commit 2b399fdb81
186 changed files with 96596 additions and 0 deletions
+262
View File
@@ -0,0 +1,262 @@
#!/usr/bin/env python3
"""
GitHub Webhook Handler for auto-deploying SREBOT on main branch pushes.
Listens for push events, pulls changes, and restarts pm2.
"""
import hmac
import hashlib
import logging
import subprocess
import os
from flask import Flask, request, jsonify
from dotenv import load_dotenv
load_dotenv()
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
logger = logging.getLogger(__name__)
app = Flask(__name__)
WEBHOOK_SECRET = os.environ.get('GITHUB_WEBHOOK_SECRET', '')
REPO_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
WEBHOOK_FILENAME = 'SREBOT/github_webhook_updater.py'
def verify_signature(payload: bytes, signature: str) -> bool:
"""Verify GitHub webhook signature."""
if not WEBHOOK_SECRET:
logger.warning("No webhook secret configured")
return False
if not signature:
logger.warning("No signature provided in request")
return False
expected = 'sha256=' + hmac.new(
WEBHOOK_SECRET.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
def pull_and_restart(changed_files: list[str]) -> tuple[bool, str]:
"""Pull latest changes from main and restart pm2 processes."""
try:
# Change to repo directory
os.chdir(REPO_PATH)
logger.info(f"Changed to repo directory: {REPO_PATH}")
# Fetch and pull main branch
logger.info("Fetching from origin/main...")
subprocess.run(
['git', 'fetch', 'origin', 'main'],
capture_output=True, text=True, timeout=30
)
logger.info("Pulling from origin/main...")
pull_result = subprocess.run(
['git', 'pull', 'origin', 'main'],
capture_output=True, text=True, timeout=60
)
if pull_result.returncode != 0:
logger.error(f"Git pull failed: {pull_result.stderr}")
return False, f"Git pull failed: {pull_result.stderr}"
# Check if there were actual changes
if 'Already up to date' in pull_result.stdout:
logger.info("Already up to date, no restart needed")
return True, "Already up to date, no restart needed"
logger.info(f"Pull successful: {pull_result.stdout.strip()}")
# Determine which processes to restart based on changed files
processes_to_restart = []
# Bot: restart if SREBOT/BOT/, SHARED/, SREBOT/start_bot.py, ecosystem.config.js,
# or SREBOT root .py files changed
bot_changed = any(
f.startswith('SREBOT/BOT/')
or f.startswith('SHARED/')
or f == 'SREBOT/start_bot.py'
or f == 'SREBOT/ecosystem.config.js'
or (f.startswith('SREBOT/') and f.endswith('.py') and f.count('/') == 1 and not f.endswith('/' + WEBHOOK_FILENAME))
for f in changed_files
)
if bot_changed:
processes_to_restart.append('srebot')
logger.info("Bot files changed, will restart srebot")
# API server: restart if SREBOT/server.js changed
api_changed = any(f == 'SREBOT/server.js' for f in changed_files)
if api_changed:
processes_to_restart.append('srebot-api')
logger.info("API server.js changed, will restart srebot-api")
# Web frontend: restart if SREBOT/web/ files changed
web_changed = any(f.startswith('SREBOT/web/') for f in changed_files)
if web_changed:
# Rebuild CSS if tailwind source or config changed
css_changed = any(
f in ('SREBOT/web/public/css/tailwind.css', 'SREBOT/web/tailwind.config.js')
for f in changed_files
)
if css_changed:
logger.info("Tailwind source changed, rebuilding CSS...")
build_result = subprocess.run(
['npm', 'run', 'build:css'],
capture_output=True, text=True, timeout=60,
cwd=os.path.join(REPO_PATH, 'SREBOT', 'web')
)
if build_result.returncode != 0:
logger.error(f"CSS build failed: {build_result.stderr}")
return False, f"CSS build failed: {build_result.stderr}"
logger.info("CSS build successful")
# Rebuild obfuscated JS if any JS source files changed
js_changed = any(
f.startswith('SREBOT/web/public/js/') and f.endswith('.js') and '/dist/' not in f
for f in changed_files
)
if js_changed:
logger.info("JS source files changed, rebuilding obfuscated JS...")
build_result = subprocess.run(
['node', 'build.js'],
capture_output=True, text=True, timeout=120,
cwd=os.path.join(REPO_PATH, 'SREBOT', 'web')
)
if build_result.returncode != 0:
logger.warning(f"JS build failed (non-fatal): {build_result.stderr}")
logger.warning("Continuing deploy — unobfuscated JS will be served as fallback")
else:
logger.info("JS build successful")
processes_to_restart.append('srebot-web')
logger.info("Web files changed, will restart srebot-web")
# Webhook: restart if this file changed
webhook_changed = any(WEBHOOK_FILENAME in f for f in changed_files)
if webhook_changed:
processes_to_restart.append('srebot-webhook')
logger.info(f"Webhook file changed, will restart srebot-webhook")
if not processes_to_restart:
logger.info(f"No relevant process files changed, skipping restart. Files: {changed_files}")
return True, "Pulled but no restart needed"
# Flush pm2 logs before restarting to free disk space
logger.info("Flushing PM2 logs...")
subprocess.run(
['pm2', 'flush'],
capture_output=True, text=True, timeout=30
)
# Restart pm2 processes
restarted = []
for process in processes_to_restart:
logger.info(f"Restarting PM2 process: {process}")
restart_result = subprocess.run(
['pm2', 'restart', process],
capture_output=True, text=True, timeout=30
)
if restart_result.returncode != 0:
logger.error(f"PM2 restart failed for {process}: {restart_result.stderr}")
return False, f"PM2 restart failed for {process}: {restart_result.stderr}"
restarted.append(process)
logger.info(f"PM2 restart successful: {restarted}")
return True, f"Pulled and restarted {restarted}"
except subprocess.TimeoutExpired:
logger.error("Command timed out")
return False, "Command timed out"
except Exception as e:
logger.exception(f"Unexpected error during pull_and_restart: {e}")
return False, f"Error: {str(e)}"
@app.route('/webhook', methods=['POST'])
def webhook():
"""Handle GitHub webhook POST requests."""
logger.info(f"Received webhook request from {request.remote_addr}")
# Verify signature
signature = request.headers.get('X-Hub-Signature-256', '')
if not verify_signature(request.data, signature):
logger.warning(f"Invalid signature from {request.remote_addr}")
return jsonify({'error': 'Invalid signature'}), 403
# Parse event type
event = request.headers.get('X-GitHub-Event', '')
logger.info(f"GitHub event type: {event}")
if event == 'ping':
logger.info("Received ping event, responding with pong")
return jsonify({'message': 'pong'}), 200
if event != 'push':
logger.info(f"Ignoring non-push event: {event}")
return jsonify({'message': f'Ignoring event: {event}'}), 200
# Parse JSON payload with error handling
try:
payload = request.get_json()
if payload is None:
logger.error("Failed to parse JSON payload (returned None)")
return jsonify({'error': 'Invalid JSON payload'}), 400
except Exception as e:
logger.error(f"Failed to parse JSON payload: {e}")
return jsonify({'error': 'Invalid JSON payload'}), 400
# Check if it's the main branch
ref = payload.get('ref', '')
logger.info(f"Push to ref: {ref}")
if ref != 'refs/heads/main':
logger.info(f"Ignoring push to non-main branch: {ref}")
return jsonify({'message': f'Ignoring branch: {ref}'}), 200
# Extract changed files from commits
commits = payload.get('commits', [])
pusher = payload.get('pusher', {}).get('name', 'unknown')
logger.info(f"Push from {pusher} with {len(commits)} commit(s)")
changed_files = []
for commit in commits:
changed_files.extend(commit.get('added', []))
changed_files.extend(commit.get('modified', []))
changed_files.extend(commit.get('removed', []))
logger.info(f"Changed files: {changed_files}")
# Pull and restart
success, message = pull_and_restart(changed_files)
if success:
logger.info(f"Webhook update successful: {message}")
return jsonify({'success': True, 'message': message}), 200
else:
logger.error(f"Webhook update failed: {message}")
return jsonify({'success': False, 'error': message}), 500
@app.route('/health', methods=['GET'])
def health():
"""Health check endpoint."""
return jsonify({'status': 'ok'}), 200
if __name__ == '__main__':
port = int(os.environ.get('WEBHOOK_PORT', 9000))
logger.info(f"Starting webhook server on port {port}")
logger.info(f"Repo path: {REPO_PATH}")
app.run(host='0.0.0.0', port=port)