refactor: restructure into proper project layout + improve docs
Project layout: - src/: application modules (run_server, auto_merge_bot, health-check, sync-key, trivial_merge, callback_server, start_server) - scripts/: setup/deployment helpers (setup_all, setup_app, generate_manifest) - templates/: manifest.json (GitHub App manifest template) - docs/ + CONTRIBUTING.md: documentation Improvements: - flake.nix: added pr-agent-auto-merge wrapper binary, updated installPhase paths - deploy.yml: syntax check covers all modules including health-check.py and sync-key.py - README.md: comprehensive with architecture, layout, dev, ops, deployment - CONTRIBUTING.md: standards and testing checklist - .gitignore: added *.log, *.pid, .env.* - Cleanup: removed duplicate manifest_current.json / manifest_final.json - Fix: health-check.py docstring updated to claude-opus-5 Verification: - ✅ python3 -m py_compile: all 11 modules pass - ✅ nix flake check: passes
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate GitHub App manifest URL for PR-Agent"""
|
||||
import json
|
||||
import base64
|
||||
import secrets
|
||||
|
||||
# Generate webhook secret
|
||||
webhook_secret = secrets.token_hex(20)
|
||||
print(f"Webhook Secret: {webhook_secret}")
|
||||
|
||||
manifest = {
|
||||
"name": "pr-agent-auto-review",
|
||||
"url": "https://github.com/asepharyana",
|
||||
"hook_attributes": {
|
||||
"url": f"https://pr-agent.asepharyana.my.id/api/v1/github_webhooks",
|
||||
"active": True
|
||||
},
|
||||
"redirect_url": "https://pr-agent.asepharyana.my.id/setup/callback",
|
||||
"callback_urls": ["https://pr-agent.asepharyana.my.id/setup/callback"],
|
||||
"public": False,
|
||||
"default_events": [
|
||||
"pull_request",
|
||||
"issue_comment"
|
||||
],
|
||||
"default_permissions": {
|
||||
"pull_requests": "write",
|
||||
"issues": "write",
|
||||
"contents": "read",
|
||||
"metadata": "read",
|
||||
"checks": "write"
|
||||
}
|
||||
}
|
||||
|
||||
# Encode manifest to base64 URL-safe
|
||||
manifest_json = json.dumps(manifest)
|
||||
manifest_b64 = base64.urlsafe_b64encode(manifest_json.encode()).decode()
|
||||
|
||||
url = f"https://github.com/settings/apps/new?manifest={manifest_b64}"
|
||||
print(f"\n{'='*60}")
|
||||
print("MANIFEST URL (klik di browser GitHub asepharyana):")
|
||||
print(f"{'='*60}")
|
||||
print(url)
|
||||
print(f"{'='*60}")
|
||||
|
||||
# Save for later
|
||||
with open("/opt/pr-agent-server/manifest.json", "w") as f:
|
||||
json.dump(manifest, f, indent=2)
|
||||
|
||||
with open("/opt/pr-agent-server/webhook_secret.txt", "w") as f:
|
||||
f.write(webhook_secret)
|
||||
|
||||
print(f"\nManifest saved to: /opt/pr-agent-server/manifest.json")
|
||||
print(f"Webhook secret saved to: /opt/pr-agent-server/webhook_secret.txt")
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate GitHub App manifest with domain URL"""
|
||||
import json
|
||||
import base64
|
||||
import secrets
|
||||
|
||||
webhook_secret = secrets.token_hex(20)
|
||||
print(f"Webhook Secret: {webhook_secret}")
|
||||
|
||||
manifest = {
|
||||
"name": "pr-agent-auto-review",
|
||||
"url": "https://github.com/asepharyana",
|
||||
"hook_attributes": {
|
||||
"url": "https://pr-agent.asepharyana.my.id/api/v1/github_webhooks",
|
||||
"active": True
|
||||
},
|
||||
"redirect_url": "https://pr-agent.asepharyana.my.id/setup/callback",
|
||||
"callback_urls": ["https://pr-agent.asepharyana.my.id/setup/callback"],
|
||||
"public": False,
|
||||
"default_events": [
|
||||
"pull_request",
|
||||
"issue_comment"
|
||||
],
|
||||
"default_permissions": {
|
||||
"pull_requests": "write",
|
||||
"issues": "write",
|
||||
"contents": "read",
|
||||
"metadata": "read",
|
||||
"checks": "write"
|
||||
}
|
||||
}
|
||||
|
||||
manifest_json = json.dumps(manifest)
|
||||
manifest_b64 = base64.urlsafe_b64encode(manifest_json.encode()).decode()
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print("BUAT APP BARU - Klik link ini di browser GitHub:")
|
||||
print(f"{'='*60}")
|
||||
print(f"https://github.com/settings/apps/new?manifest={manifest_b64}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
# Save
|
||||
with open("/opt/pr-agent-server/manifest_domain.json", "w") as f:
|
||||
json.dump(manifest, f, indent=2)
|
||||
with open("/opt/pr-agent-server/webhook_secret.txt", "w") as f:
|
||||
f.write(webhook_secret)
|
||||
|
||||
print(f"\nWebhook secret: {webhook_secret}")
|
||||
print(f"Webhook URL: https://pr-agent.asepharyana.my.id/api/v1/github_webhooks")
|
||||
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PR-Agent GitHub App - Complete Setup & Server
|
||||
Generates manifest URL, starts webhook server, handles callback
|
||||
"""
|
||||
import json, base64, secrets, os, sys, threading
|
||||
from pathlib import Path
|
||||
|
||||
WEBHOOK_SECRET = secrets.token_hex(20)
|
||||
BASE_DIR = Path("/opt/pr-agent-server")
|
||||
BASE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# ── 1. Generate Manifest ──
|
||||
manifest = {
|
||||
"name": "pr-agent-auto",
|
||||
"url": "https://github.com/asepharyana",
|
||||
"hook_attributes": {
|
||||
"url": "https://pr-agent.asepharyana.my.id/api/v1/github_webhooks",
|
||||
"active": True
|
||||
},
|
||||
"redirect_url": "https://pr-agent.asepharyana.my.id/setup/callback",
|
||||
"callback_urls": ["https://pr-agent.asepharyana.my.id/setup/callback"],
|
||||
"public": False,
|
||||
"default_events": ["pull_request", "issue_comment"],
|
||||
"default_permissions": {
|
||||
"pull_requests": "write",
|
||||
"issues": "write",
|
||||
"contents": "read",
|
||||
"metadata": "read",
|
||||
"checks": "write"
|
||||
}
|
||||
}
|
||||
|
||||
manifest_b64 = base64.urlsafe_b64encode(json.dumps(manifest).encode()).decode()
|
||||
manifest_url = f"https://github.com/settings/apps/new?manifest={manifest_b64}"
|
||||
|
||||
# ── 2. Save configs ──
|
||||
with open(BASE_DIR / "manifest.json", "w") as f:
|
||||
json.dump(manifest, f, indent=2)
|
||||
with open(BASE_DIR / "webhook_secret.txt", "w") as f:
|
||||
f.write(WEBHOOK_SECRET)
|
||||
with open(BASE_DIR / "manifest_url.txt", "w") as f:
|
||||
f.write(manifest_url)
|
||||
|
||||
print(f"""
|
||||
╔══════════════════════════════════════════════════╗
|
||||
║ PR-Agent GitHub App Setup ║
|
||||
╠══════════════════════════════════════════════════╣
|
||||
║ ║
|
||||
║ Webhook Secret: {WEBHOOK_SECRET[:20]}... ║
|
||||
║ ║
|
||||
║ MANIFEST URL: ║
|
||||
║ {manifest_url[:60]}... ║
|
||||
║ ║
|
||||
║ Buka URL di atas di browser GitHub ║
|
||||
║ asepharyana, klik Create, lalu kirim ║
|
||||
║ App ID + Private Key ke sini. ║
|
||||
║ ║
|
||||
╚══════════════════════════════════════════════════╝
|
||||
""")
|
||||
|
||||
# ── 3. Create .secrets.toml for PR-Agent ──
|
||||
KEY = os.environ.get("OMNIROUTE_API_KEY", "")
|
||||
secrets_toml = f"""[openai]
|
||||
key = "{KEY}"
|
||||
api_base = "https://omniroute.imrnes.team/v1"
|
||||
|
||||
[github]
|
||||
deployment_type = "app"
|
||||
# Will be filled after app creation:
|
||||
# app_id = 123456
|
||||
# private_key = "<paste PEM here>"
|
||||
# webhook_secret = "{WEBHOOK_SECRET}"
|
||||
"""
|
||||
|
||||
with open(BASE_DIR / ".secrets.toml", "w") as f:
|
||||
f.write(secrets_toml)
|
||||
|
||||
# ── 4. Create PR-Agent config ──
|
||||
config_toml = """[config]
|
||||
model = "openai/claude-opus-5"
|
||||
fallback_models = ["openai/claude-sonnet-5", "openai/claude-haiku-4-5-20251001", "openai/ATLAS", "openai/gemini", "openai/text", "openai/deepseek-v4-flash-free"]
|
||||
custom_model_max_tokens = 128000
|
||||
git_provider = "github"
|
||||
publish_output = true
|
||||
verbosity_level = 0
|
||||
|
||||
[github_app]
|
||||
pr_commands = ["/describe", "/review", "/improve"]
|
||||
handle_push_trigger = true
|
||||
push_commands = ["/describe", "/review"]
|
||||
|
||||
[pr_reviewer]
|
||||
num_max_findings = 5
|
||||
require_tests_review = true
|
||||
require_security_review = true
|
||||
|
||||
[pr_description]
|
||||
enable_pr_diagram = true
|
||||
use_bullet_points = true
|
||||
|
||||
[pr_code_suggestions]
|
||||
num_code_suggestions_per_chunk = 4
|
||||
"""
|
||||
|
||||
with open(BASE_DIR / "configuration.toml", "w") as f:
|
||||
f.write(config_toml)
|
||||
|
||||
# ── 5. Create systemd service file ──
|
||||
app_dir = os.path.expanduser("~/hermes-agent/.venv/lib/python3.12/site-packages")
|
||||
service = f"""[Unit]
|
||||
Description=PR-Agent GitHub App Webhook Server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory={BASE_DIR}
|
||||
Environment="PYTHONPATH={app_dir}"
|
||||
Environment="OMNIROUTE_API_KEY={KEY}"
|
||||
Environment="OPENAI_KEY={KEY}"
|
||||
Environment="OPENAI_API_BASE=https://omniroute.imrnes.team/v1"
|
||||
Environment="ANTHROPIC_API_KEY={KEY}"
|
||||
Environment="ANTHROPIC_API_BASE=https://omniroute.imrnes.team/v1"
|
||||
Environment="PORT=3000"
|
||||
ExecStart={sys.executable} -c "from pr_agent.servers.github_app import app; import uvicorn; uvicorn.run(app, host='0.0.0.0', port=3000, log_level='info')"
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
"""
|
||||
|
||||
with open(BASE_DIR / "pr-agent.service", "w") as f:
|
||||
f.write(service)
|
||||
|
||||
print(f" Config files created in {BASE_DIR}")
|
||||
print(f" Run: cp {BASE_DIR}/pr-agent.service /etc/systemd/system/")
|
||||
print(f" Then: systemctl daemon-reload && systemctl enable --now pr-agent")
|
||||
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PR-Agent GitHub App Setup Helper
|
||||
Creates the GitHub App manifest and prepares the server configuration.
|
||||
"""
|
||||
import json
|
||||
import base64
|
||||
import os
|
||||
import secrets
|
||||
|
||||
# ============================================================
|
||||
# CONFIGURATION
|
||||
# ============================================================
|
||||
APP_NAME = "pr-agent-auto"
|
||||
APP_SLUG = "pr-agent-auto"
|
||||
DESCRIPTION = "Automated PR review and merge bot powered by AI"
|
||||
HOME_URL = "https://github.com/asepharyana"
|
||||
PUBLIC_IP = "45.127.35.244"
|
||||
PORT = 4002
|
||||
WEBHOOK_URL = "https://pr-agent.asepharyana.my.id/api/v1/github_webhooks"
|
||||
REDIRECT_URL = "https://pr-agent.asepharyana.my.id/app-setup-complete"
|
||||
CALLBACK_URLS = ["https://pr-agent.asepharyana.my.id/callback"]
|
||||
|
||||
# Generate webhook secret
|
||||
WEBHOOK_SECRET = secrets.token_hex(20)
|
||||
|
||||
# ============================================================
|
||||
# CREATE MANIFEST
|
||||
# ============================================================
|
||||
manifest = {
|
||||
"name": APP_NAME,
|
||||
"slug": APP_SLUG,
|
||||
"description": DESCRIPTION,
|
||||
"url": HOME_URL,
|
||||
"hook_attributes": {
|
||||
"url": WEBHOOK_URL,
|
||||
"active": True
|
||||
},
|
||||
"redirect_url": REDIRECT_URL,
|
||||
"callback_urls": CALLBACK_URLS,
|
||||
"public": False,
|
||||
"default_events": [
|
||||
"pull_request",
|
||||
"issue_comment",
|
||||
"push"
|
||||
],
|
||||
"default_permissions": {
|
||||
"pull_requests": "write",
|
||||
"issues": "write",
|
||||
"metadata": "read",
|
||||
"contents": "read",
|
||||
"checks": "write",
|
||||
"emails": "read"
|
||||
}
|
||||
}
|
||||
|
||||
# Save manifest
|
||||
os.makedirs("/opt/pr-agent-server", exist_ok=True)
|
||||
manifest_path = "/opt/pr-agent-server/manifest.json"
|
||||
|
||||
with open(manifest_path, "w") as f:
|
||||
json.dump(manifest, f, indent=2)
|
||||
|
||||
# Create the URL
|
||||
manifest_b64 = base64.b64encode(json.dumps(manifest).encode()).decode()
|
||||
manifest_url = f"https://github.com/settings/apps/new?manifest={manifest_b64}"
|
||||
|
||||
print("=" * 60)
|
||||
print("PR-Agent GITHUB APP SETUP")
|
||||
print("=" * 60)
|
||||
print(f"\n📋 App Name: {APP_NAME}")
|
||||
print(f"🌐 Webhook URL: {WEBHOOK_URL}")
|
||||
print(f"🔑 Webhook Secret: {WEBHOOK_SECRET}")
|
||||
print(f"\n{'=' * 60}")
|
||||
print("STEP 1: Click this URL to create the GitHub App:")
|
||||
print(f"{'=' * 60}")
|
||||
print(f"\n{manifest_url}\n")
|
||||
print(f"{'=' * 60}")
|
||||
print("STEP 2: After clicking 'Create GitHub App', you'll be redirected.")
|
||||
print(" Save the App ID, Private Key, and Webhook Secret shown.")
|
||||
print(f"{'=' * 60}")
|
||||
|
||||
# Save vars for later use
|
||||
env_file = "/opt/pr-agent-server/.env"
|
||||
with open(env_file, "w") as f:
|
||||
f.write(f"WEBHOOK_SECRET={WEBHOOK_SECRET}\n")
|
||||
f.write(f"PORT={PORT}\n")
|
||||
f.write("# After GitHub App creation, add:\n")
|
||||
f.write("# APP_ID=<your-app-id>\n")
|
||||
f.write("# PRIVATE_KEY_PATH=/opt/pr-agent-server/private-key.pem\n")
|
||||
f.write(f"# GITHUB_APP_NAME={APP_NAME}\n")
|
||||
|
||||
print(f"\n📁 Config saved to: {manifest_path}")
|
||||
print(f"📁 Env file: {env_file}")
|
||||
print(f"\nWebhook Secret (save this!): {WEBHOOK_SECRET}")
|
||||
Reference in New Issue
Block a user