jarvis-cloud / backend /services /github_service.py
Jarvis2345's picture
Squash history — remove all prior commits (secret hygiene, S4)
a31f556
Raw
History Blame Contribute Delete
5.23 kB
import asyncio
import sqlite3
import logging
import re
import time
from backend.services.usb_vault import get_secret
from backend.services.usb_monitor import get_db_path
from backend.ws.agent_ws import ws_manager
# Ensure DB table exists
def init_github_db():
try:
with sqlite3.connect(get_db_path()) as conn:
conn.execute('PRAGMA journal_mode=WAL')
conn.execute("""
CREATE TABLE IF NOT EXISTS commit_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
commit_hash TEXT,
message TEXT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
)
""")
conn.commit()
except Exception as e:
logging.error(f"Failed to init github db: {e}")
init_github_db()
# Mutex to prevent concurrent commits
commit_lock = asyncio.Lock()
async def run_git(cmd: str, cwd: str = None) -> tuple[int, str, str]:
"""Runs a git command and returns (returncode, stdout, stderr)."""
proc = await asyncio.create_subprocess_shell(
cmd,
cwd=cwd or '.',
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await proc.communicate()
return proc.returncode, stdout.decode().strip(), stderr.decode().strip()
async def execute_commit(message: str) -> dict:
"""Executes a git commit and push with safety checks, queueing, and rate-limit backoff."""
async with commit_lock:
logging.info("GITHUB_ENGINE: Acquired commit lock.")
# 1. Fetch token from Vault
token = get_secret("GITHUB_TOKEN")
if not token:
return {"status": "error", "message": "GitHub Token not found in vault. Please inject it securely via EXE."}
# 2. Incremental Diff Compute
# Check if there are unstaged changes or untracked files
await run_git("git add -A")
code, stdout, stderr = await run_git("git diff --cached --name-only")
if not stdout:
return {"status": "skipped", "message": "No incremental changes detected. Aborting commit."}
logging.info(f"GITHUB_ENGINE: Staged files for commit:\n{stdout}")
# 3. Commit
code, c_out, c_err = await run_git(f'git commit -m "{message}"')
if code != 0:
return {"status": "error", "message": f"Commit failed: {c_err or c_out}"}
# Extract commit hash
hash_code, h_out, h_err = await run_git("git rev-parse HEAD")
commit_hash = h_out if hash_code == 0 else "UNKNOWN_HASH"
# 4. Push with Rate Limit Backoff
# Using a dummy remote URL structure that includes the PAT.
# Ensure your git remote is configured correctly if relying on system git.
# For simplicity, we just run git push.
max_retries = 3
attempt = 0
push_success = False
while attempt < max_retries:
attempt += 1
logging.info(f"GITHUB_ENGINE: Pushing to remote (Attempt {attempt})...")
p_code, p_out, p_err = await run_git("git push")
if p_code == 0:
push_success = True
break
# 5. Rate-Limit 403/429 Backoff Parsing
# GitHub usually responds with HTTP 403 or 429 and a "retry-after" header embedded in the stderr proxy response.
combined_err = (p_err + " " + p_out).lower()
if "403" in combined_err or "429" in combined_err or "rate limit" in combined_err:
retry_match = re.search(r"retry-after[:\s]+(\d+)", combined_err)
sleep_time = int(retry_match.group(1)) if retry_match else 60
logging.warning(f"GITHUB_ENGINE: Rate limit hit. Backing off for {sleep_time} seconds.")
await ws_manager.broadcast("github:rate_limit", {"wait_seconds": sleep_time})
await asyncio.sleep(sleep_time)
else:
return {"status": "error", "message": f"Push failed: {p_err}"}
if not push_success:
return {"status": "error", "message": "Push failed after max retries."}
# 6. SQLite History Logging
try:
with sqlite3.connect(get_db_path()) as conn:
conn.execute('PRAGMA journal_mode=WAL')
conn.execute("INSERT INTO commit_history (commit_hash, message) VALUES (?, ?)", (commit_hash, message))
conn.commit()
except Exception as e:
logging.error(f"Failed to log commit history: {e}")
# 7. WebSocket Event Emitted
await ws_manager.broadcast("github:commit_success", {
"hash": commit_hash,
"message": message,
"timestamp": time.time()
})
logging.info(f"GITHUB_ENGINE: Commit and push successful! Hash: {commit_hash}")
return {"status": "success", "hash": commit_hash, "message": "Successfully committed and pushed to remote."}