feat: event-driven parity, Windows shell detection, dirty build timestamps, and gatekeeper integration

This commit is contained in:
Riz Ashraf committed 2026-10-08 10:52:19 +01:00
1 parent 64857f9d5e
commit f4dcf3acbf
33 files changed
+3501 -143

No files matched your search

+204
View File
@@ -0,0 +1,204 @@
#!/usr/bin/env python3
"""
Pre-Push Gatekeeper for Git and MCP Memory.
Enforces workspace quality gates:
1. Allows tag pushes and branch deletions without verification.
2. Bypasses tests for docs/config-only modifications (.md, .txt, .png, justfile, etc.).
3. Queries mcp-memory gate API (/gate/verify?action=push&target=<branch>).
4. If no pre-authorized gate is found, runs project tests locally to verify 100% pass before allowing push.
"""
import os
import sys
import json
import subprocess
import urllib.request
import urllib.error
DOC_EXTENSIONS = {
".md", ".txt", ".png", ".jpg", ".jpeg", ".pdf", ".svg",
".drawio", ".gif", ".ico", ".csv"
}
DOC_FILENAMES = {
"justfile", "jenkinsfile", "dockerfile", "license",
"changelog", ".gitignore", ".gitattributes"
}
def is_doc_or_config(filepath: str) -> bool:
base = os.path.basename(filepath).lower()
if base in DOC_FILENAMES:
return True
ext = os.path.splitext(base)[1].lower()
return ext in DOC_EXTENSIONS
def run_git(cmd):
try:
res = subprocess.run(["git"] + cmd, capture_output=True, text=True, check=True)
return res.stdout.strip()
except Exception:
return ""
def check_mcp_gate(port: str, branch: str):
"""
Returns (status_code, reason)
status_code: 200 (Authorized), 403 (Blocked), 404 (Not found), 0 (Server unreachable)
"""
url = f"http://127.0.0.1:{port}/gate/verify?action=push&target={branch}"
try:
req = urllib.request.Request(url, method="GET")
with urllib.request.urlopen(req, timeout=1.5) as resp:
return resp.status, "Authorized"
except urllib.error.HTTPError as e:
body = ""
try:
body = e.read().decode("utf-8")
except Exception:
pass
return e.code, body
except Exception:
return 0, "Server unreachable"
def set_mcp_gate(port: str, branch: str, authorize: bool, reason: str):
url = f"http://127.0.0.1:{port}/gate/set"
payload = {
"action": "push",
"target": branch,
"authorize": authorize,
"reason": reason
}
try:
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"}, method="POST")
with urllib.request.urlopen(req, timeout=1.5):
pass
except Exception:
pass
def post_mcp_event(port: str, topic: str, payload: dict):
url = f"http://127.0.0.1:{port}/api/events/post"
try:
data = json.dumps({"topic": topic, "payload": payload}).encode("utf-8")
req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"}, method="POST")
with urllib.request.urlopen(req, timeout=1.0):
pass
except Exception:
pass
def wait_for_mcp_decision(port: str, topic: str, timeout_secs: int = 2):
"""Waits for an operator or auditor decision on the event bus."""
url = f"http://127.0.0.1:{port}/events/wait?topic={topic}&timeout={timeout_secs}"
try:
req = urllib.request.Request(url, method="GET")
with urllib.request.urlopen(req, timeout=timeout_secs + 0.5) as resp:
data = json.loads(resp.read().decode("utf-8"))
payload = data.get("payload", {})
status = str(payload.get("status", "")).lower()
reason = payload.get("reason", "Decision received via event bus")
if status in ("authorized", "approved", "ok", "true"):
return True, reason
elif status in ("blocked", "denied", "rejected", "false"):
return False, reason
except Exception:
pass
return None, ""
def run_local_tests() -> bool:
# 1. Rust workspace
if os.path.exists("Cargo.toml"):
print("\033[36m[Pre-Push Gatekeeper]\033[0m Running cargo test...")
cmd = ["cargo", "test", "--workspace"]
res = subprocess.run(cmd)
return res.returncode == 0
# 2. Python workspace (uv / pytest)
elif os.path.exists("pyproject.toml") or os.path.exists("setup.py"):
print("\033[36m[Pre-Push Gatekeeper]\033[0m Running python tests...")
cmd = ["uv", "run", "pytest"] if os.system("uv --version >nul 2>&1") == 0 else ["pytest"]
res = subprocess.run(cmd)
return res.returncode == 0
return True
def main():
# Read stdin passed by git pre-push: <local ref> <local sha> <remote ref> <remote sha>
lines = sys.stdin.read().splitlines()
if not lines:
sys.exit(0)
port = os.environ.get("MCP_PORT", "3000")
for line in lines:
parts = line.strip().split()
if len(parts) < 4:
continue
local_ref, local_sha, remote_ref, remote_sha = parts[:4]
# 1. Skip deletions
if local_sha == "0000000000000000000000000000000000000000" or local_ref == "(delete)":
continue
# 2. Skip tags
if local_ref.startswith("refs/tags/"):
print(f"\033[33m[Pre-Push Gatekeeper]\033[0m Tag push detected ({local_ref}). Quality gate bypassed.")
continue
# Extract branch name
branch = local_ref.replace("refs/heads/", "")
# 3. Check for documentation / config-only changes
zero_sha = "0000000000000000000000000000000000000000"
diff_range = local_sha if remote_sha == zero_sha else f"{remote_sha}..{local_sha}"
changed_files_raw = run_git(["diff", "--name-only", diff_range])
changed_files = [f.strip() for f in changed_files_raw.splitlines() if f.strip()]
if changed_files and all(is_doc_or_config(f) for f in changed_files):
print(f"\033[33m[Pre-Push Gatekeeper]\033[0m All modified files are docs/config-only. Quality gate bypassed.")
continue
# 4. Query MCP Memory gate status
status, reason = check_mcp_gate(port, branch)
if status == 200:
print(f"\033[32m[Pre-Push Gatekeeper]\033[0m \u2714 Push AUTHORIZED by MCP Memory gate for branch '{branch}'.")
continue
elif status == 403:
print(f"\033[31m[Pre-Push Gatekeeper]\033[0m \u274c Push BLOCKED by MCP Memory gate for branch '{branch}'!")
if reason:
print(f" Reason: {reason}")
sys.exit(1)
# 5. No gate found (404 or server offline) -> Request interactive approval or verify unit tests
if status != 0:
post_mcp_event(port, "gate:approval_requested", {
"branch": branch,
"local_sha": local_sha,
"remote_sha": remote_sha,
"changed_files_count": len(changed_files)
})
# Fast check: Did dashboard / operator / auditor approve or reject via event bus?
decision, dec_reason = wait_for_mcp_decision(port, f"gate:decision:{branch}", timeout_secs=1)
if decision is True:
print(f"\033[32m[Pre-Push Gatekeeper]\033[0m \u2714 Push INTERACTIVELY AUTHORIZED via event bus: {dec_reason}")
set_mcp_gate(port, branch, True, dec_reason)
post_mcp_event(port, "gate:event", {"status": "authorized", "active_gate": "authorized", "branch": branch})
continue
elif decision is False:
print(f"\033[31m[Pre-Push Gatekeeper]\033[0m \u274c Push BLOCKED by operator: {dec_reason}")
set_mcp_gate(port, branch, False, dec_reason)
post_mcp_event(port, "gate:event", {"status": "blocked", "active_gate": "blocked", "branch": branch})
sys.exit(1)
print(f"\033[33m[Pre-Push Gatekeeper]\033[0m No pre-authorized gate record for '{branch}'. Verifying unit tests...")
if run_local_tests():
print(f"\033[32m[Pre-Push Gatekeeper]\033[0m \u2714 Unit tests passed 100%. Push authorized.")
if status != 0:
set_mcp_gate(port, branch, True, "Unit tests passed locally via pre-push gatekeeper")
post_mcp_event(port, "gate:event", {"status": "authorized", "active_gate": "authorized", "branch": branch})
else:
print(f"\033[31m[Pre-Push Gatekeeper]\033[0m \u274c Push BLOCKED! Unit tests failed locally. Fix failing tests before pushing.")
if status != 0:
set_mcp_gate(port, branch, False, "Unit tests failed during pre-push gatekeeper check")
post_mcp_event(port, "gate:event", {"status": "blocked", "active_gate": "blocked", "branch": branch})
sys.exit(1)
sys.exit(0)
if __name__ == "__main__":
main()