Files
the-nexus/scripts/ci_auto_revert.py
Bezalel a0ee7858ff
Some checks failed
Deploy Nexus / deploy (push) Has been cancelled
feat(bezalel): MemPalace ecosystem — validation, audit, sync, auto-revert, Evennia integration
2026-04-07 14:47:12 +00:00

136 lines
4.6 KiB
Python

#!/usr/bin/env python3
"""
CI Auto-Revert — Poka-yoke for broken merges.
Monitors the main branch post-merge and auto-reverts via local git if CI fails.
Usage:
python ci_auto_revert.py <repo_owner>/<repo_name>
python ci_auto_revert.py Timmy_Foundation/hermes-agent
Recommended cron: */10 * * * *
"""
import os
import sys
import json
import subprocess
import tempfile
from datetime import datetime, timedelta, timezone
from urllib import request, error
GITEA_TOKEN = os.environ.get("GITEA_TOKEN", "")
GITEA_URL = os.environ.get("GITEA_URL", "https://forge.alexanderwhitestone.com")
REVERT_WINDOW_MINUTES = 10
def api_call(method, path):
url = f"{GITEA_URL}/api/v1{path}"
headers = {"Authorization": f"token {GITEA_TOKEN}"}
req = request.Request(url, method=method, headers=headers)
try:
with request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read().decode())
except error.HTTPError as e:
return {"error": e.read().decode(), "status": e.code}
def get_recent_commits(owner, repo, since):
since_iso = since.strftime("%Y-%m-%dT%H:%M:%SZ")
return api_call("GET", f"/repos/{owner}/{repo}/commits?sha=main&since={since_iso}&limit=20")
def get_commit_status(owner, repo, sha):
return api_call("GET", f"/repos/{owner}/{repo}/commits/{sha}/status")
def revert_via_git(clone_url, sha, msg, owner, repo):
with tempfile.TemporaryDirectory() as tmpdir:
# Clone with token
auth_url = clone_url.replace("https://", f"https://bezalel:{GITEA_TOKEN}@")
subprocess.run(["git", "clone", "--depth", "10", auth_url, tmpdir], check=True, capture_output=True)
# Configure git
subprocess.run(["git", "-C", tmpdir, "config", "user.email", "bezalel@timmy.foundation"], check=True, capture_output=True)
subprocess.run(["git", "-C", tmpdir, "config", "user.name", "Bezalel"], check=True, capture_output=True)
# Revert the commit
revert_msg = f"[auto-revert] {msg}\n\nOriginal commit {sha} failed CI."
result = subprocess.run(
["git", "-C", tmpdir, "revert", "--no-edit", "-m", revert_msg, sha],
capture_output=True,
text=True,
)
if result.returncode != 0:
return {"error": f"git revert failed: {result.stderr}"}
# Push
push_result = subprocess.run(
["git", "-C", tmpdir, "push", "origin", "main"],
capture_output=True,
text=True,
)
if push_result.returncode != 0:
return {"error": f"git push failed: {push_result.stderr}"}
return {"ok": True, "reverted_sha": sha}
def main():
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <owner/repo>")
sys.exit(1)
repo_full = sys.argv[1]
owner, repo = repo_full.split("/", 1)
since = datetime.now(timezone.utc) - timedelta(minutes=REVERT_WINDOW_MINUTES + 5)
commits = get_recent_commits(owner, repo, since)
if not isinstance(commits, list):
print(f"ERROR fetching commits: {commits}")
sys.exit(1)
reverted = 0
for commit in commits:
sha = commit.get("sha", "")
msg = commit.get("commit", {}).get("message", "").split("\n")[0]
commit_time = commit.get("commit", {}).get("committer", {}).get("date", "")
if not commit_time:
continue
commit_dt = datetime.fromisoformat(commit_time.replace("Z", "+00:00"))
age_min = (datetime.now(timezone.utc) - commit_dt).total_seconds() / 60
if age_min > REVERT_WINDOW_MINUTES:
continue
status = get_commit_status(owner, repo, sha)
state = status.get("state", "")
if state == "failure":
print(f"ALERT: Commit {sha[:8]} '{msg}' failed CI ({age_min:.1f}m old). Initiating revert...")
repo_info = api_call("GET", f"/repos/{owner}/{repo}")
clone_url = repo_info.get("clone_url", "")
if not clone_url:
print(f" Cannot find clone URL")
continue
result = revert_via_git(clone_url, sha, msg, owner, repo)
if "error" in result:
print(f" Revert failed: {result['error']}")
else:
print(f" Reverted successfully.")
reverted += 1
elif state == "success":
print(f"OK: Commit {sha[:8]} '{msg}' passed CI.")
elif state == "pending":
print(f"PENDING: Commit {sha[:8]} '{msg}' still running CI.")
else:
print(f"UNKNOWN: Commit {sha[:8]} '{msg}' has CI state '{state}'.")
if reverted == 0:
print("No broken merges found in the last 10 minutes.")
if __name__ == "__main__":
main()