feat: PR-Agent GitHub App server — sanitized source + Nix flake + CI deploy
This commit is contained in:
@@ -0,0 +1,65 @@
|
|||||||
|
name: Build & Deploy (Nix)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out repository
|
||||||
|
run: |
|
||||||
|
git clone https://git.imrnes.team/MythEclipse/pr-agent-server.git .
|
||||||
|
git checkout ${{ github.sha }}
|
||||||
|
|
||||||
|
- name: Build & Deploy
|
||||||
|
env:
|
||||||
|
VPS_HOST: ${{ secrets.VPS_HOST }}
|
||||||
|
VPS_USER: ${{ secrets.VPS_USER }}
|
||||||
|
VPS_SSH_KEY: ${{ secrets.VPS_SSH_KEY }}
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
# --- Install Nix & Build ---
|
||||||
|
curl -fsSL https://install.determinate.systems/nix \
|
||||||
|
| sh -s -- install linux --no-confirm --init none 2>&1
|
||||||
|
|
||||||
|
mkdir -p /etc/nix
|
||||||
|
echo "experimental-features = nix-command flakes" >> /etc/nix/nix.conf
|
||||||
|
|
||||||
|
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
|
||||||
|
|
||||||
|
echo "=== Building: pr-agent-server ==="
|
||||||
|
nix build .#default --impure --option sandbox false 2>&1
|
||||||
|
|
||||||
|
STORE_PATH=$(readlink result)
|
||||||
|
echo "=== Store path: $STORE_PATH"
|
||||||
|
|
||||||
|
# --- Deploy ---
|
||||||
|
NIX_BIN="/nix/var/nix/profiles/default/bin"
|
||||||
|
PROFILE="/nix/var/nix/profiles/pr-agent-server"
|
||||||
|
|
||||||
|
key_file=$(mktemp /tmp/deploy-key.XXXXXX)
|
||||||
|
printf '%s\n' "$VPS_SSH_KEY" > "$key_file"
|
||||||
|
chmod 600 "$key_file"
|
||||||
|
sed -i 's/\r$//' "$key_file" # strip DOS line endings if any
|
||||||
|
|
||||||
|
export NIX_SSHOPTS="-i $key_file -o StrictHostKeyChecking=no"
|
||||||
|
nix copy --to "ssh://${VPS_USER}@${VPS_HOST}" "$STORE_PATH" 2>&1
|
||||||
|
|
||||||
|
ssh -i "$key_file" -o StrictHostKeyChecking=no \
|
||||||
|
"${VPS_USER}@${VPS_HOST}" "
|
||||||
|
export PATH=\$PATH:$NIX_BIN
|
||||||
|
if [ -d $PROFILE ] && [ ! -L $PROFILE ]; then
|
||||||
|
rm -rf $PROFILE
|
||||||
|
fi
|
||||||
|
nix-env --profile $PROFILE --set $STORE_PATH
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl restart pr-agent-server
|
||||||
|
sleep 3
|
||||||
|
systemctl status pr-agent-server --no-pager 2>&1 | head -12
|
||||||
|
" 2>&1
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
# Secrets — never commit
|
||||||
|
*.pem
|
||||||
|
*credentials*.json
|
||||||
|
webhook_secret.txt
|
||||||
|
whsec*.txt
|
||||||
|
.secrets.toml
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
PR-Agent Auto-Approve + Auto-Merge Bot
|
||||||
|
Runs periodically (cron), finds open PRs that have been reviewed by PR-Agent,
|
||||||
|
approves them and enables auto-merge.
|
||||||
|
"""
|
||||||
|
import os, sys, json, time, hmac, hashlib, asyncio
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# ── Config ──
|
||||||
|
APP_ID = os.environ.get("GITHUB_APP_ID", "4319749")
|
||||||
|
PRIVATE_KEY_PATH = os.environ.get("PRIVATE_KEY_PATH", "/var/lib/pr-agent-server/private-key.pem")
|
||||||
|
WEBHOOK_SECRET = os.environ.get("GITHUB_WEBHOOK_SECRET", "")
|
||||||
|
BASE_URL = os.environ.get("GITHUB_API_BASE", "https://api.github.com")
|
||||||
|
|
||||||
|
def get_jwt():
|
||||||
|
import jwt as pyjwt
|
||||||
|
with open(PRIVATE_KEY_PATH) as f:
|
||||||
|
key = f.read()
|
||||||
|
now = int(time.time())
|
||||||
|
payload = {"iat": now - 60, "exp": now + 600, "iss": APP_ID}
|
||||||
|
return pyjwt.encode(payload, key, algorithm="RS256")
|
||||||
|
|
||||||
|
def get_installation_token(installation_id: int) -> str:
|
||||||
|
"""Get installation access token"""
|
||||||
|
import httpx
|
||||||
|
jwt_token = get_jwt()
|
||||||
|
with httpx.Client() as client:
|
||||||
|
r = client.post(
|
||||||
|
f"{BASE_URL}/app/installations/{installation_id}/access_tokens",
|
||||||
|
headers={"Authorization": f"Bearer {jwt_token}", "Accept": "application/vnd.github.v3+json"}
|
||||||
|
)
|
||||||
|
return r.json().get("token", "")
|
||||||
|
|
||||||
|
def get_all_installations() -> list:
|
||||||
|
"""Get all app installations"""
|
||||||
|
jwt_token = get_jwt()
|
||||||
|
import httpx
|
||||||
|
with httpx.Client() as client:
|
||||||
|
r = client.get(
|
||||||
|
f"{BASE_URL}/app/installations",
|
||||||
|
headers={"Authorization": f"Bearer {jwt_token}", "Accept": "application/vnd.github.v3+json"}
|
||||||
|
)
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
def get_installation_repos(installation_id: int, token: str) -> list:
|
||||||
|
"""Get repos for an installation"""
|
||||||
|
import httpx
|
||||||
|
with httpx.Client() as client:
|
||||||
|
r = client.get(
|
||||||
|
f"{BASE_URL}/installation/repositories",
|
||||||
|
headers={"Authorization": f"token {token}", "Accept": "application/vnd.github.v3+json"}
|
||||||
|
)
|
||||||
|
return r.json().get("repositories", [])
|
||||||
|
|
||||||
|
def get_open_prs(token: str, repo_full: str) -> list:
|
||||||
|
"""Get open PRs in a repo"""
|
||||||
|
import httpx
|
||||||
|
with httpx.Client() as client:
|
||||||
|
r = client.get(
|
||||||
|
f"{BASE_URL}/repos/{repo_full}/pulls?state=open&sort=updated&direction=desc",
|
||||||
|
headers={"Authorization": f"token {token}", "Accept": "application/vnd.github.v3+json"}
|
||||||
|
)
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
def get_pr_reviews(token: str, repo_full: str, pr_number: int) -> list:
|
||||||
|
"""Get reviews for a PR"""
|
||||||
|
import httpx
|
||||||
|
with httpx.Client() as client:
|
||||||
|
r = client.get(
|
||||||
|
f"{BASE_URL}/repos/{repo_full}/pulls/{pr_number}/reviews",
|
||||||
|
headers={"Authorization": f"token {token}", "Accept": "application/vnd.github.v3+json"}
|
||||||
|
)
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
def get_pr_comments(token: str, repo_full: str, pr_number: int) -> list:
|
||||||
|
"""Get issue comments for a PR"""
|
||||||
|
import httpx
|
||||||
|
with httpx.Client() as client:
|
||||||
|
r = client.get(
|
||||||
|
f"{BASE_URL}/repos/{repo_full}/issues/{pr_number}/comments",
|
||||||
|
headers={"Authorization": f"token {token}", "Accept": "application/vnd.github.v3+json"}
|
||||||
|
)
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
def approve_pr(token: str, repo_full: str, pr_number: int) -> bool:
|
||||||
|
"""Submit APPROVE review"""
|
||||||
|
import httpx
|
||||||
|
with httpx.Client() as client:
|
||||||
|
r = client.post(
|
||||||
|
f"{BASE_URL}/repos/{repo_full}/pulls/{pr_number}/reviews",
|
||||||
|
headers={"Authorization": f"token {token}", "Accept": "application/vnd.github.v3+json"},
|
||||||
|
json={"event": "APPROVE", "body": "✅ Auto-approved by PR-Agent bot."}
|
||||||
|
)
|
||||||
|
return r.status_code == 200
|
||||||
|
|
||||||
|
def merge_pr(token: str, repo_full: str, pr_number: int) -> tuple:
|
||||||
|
"""Attempt to merge the PR"""
|
||||||
|
import httpx
|
||||||
|
with httpx.Client() as client:
|
||||||
|
# Get PR info for SHA
|
||||||
|
pr_r = client.get(
|
||||||
|
f"{BASE_URL}/repos/{repo_full}/pulls/{pr_number}",
|
||||||
|
headers={"Authorization": f"token {token}", "Accept": "application/vnd.github.v3+json"}
|
||||||
|
)
|
||||||
|
if pr_r.status_code != 200:
|
||||||
|
return False, f"Can't get PR: {pr_r.status_code}"
|
||||||
|
|
||||||
|
pr_data = pr_r.json()
|
||||||
|
sha = pr_data.get("head", {}).get("sha", "")
|
||||||
|
mergeable = pr_data.get("mergeable", False)
|
||||||
|
|
||||||
|
if mergeable is False:
|
||||||
|
return False, "PR not mergeable (conflicts or checks pending)"
|
||||||
|
|
||||||
|
# Try merge
|
||||||
|
merge_r = client.put(
|
||||||
|
f"{BASE_URL}/repos/{repo_full}/pulls/{pr_number}/merge",
|
||||||
|
headers={"Authorization": f"token {token}", "Accept": "application/vnd.github.v3+json"},
|
||||||
|
json={
|
||||||
|
"commit_title": f"Auto-merge PR #{pr_number}",
|
||||||
|
"merge_method": "merge",
|
||||||
|
"sha": sha
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if merge_r.status_code == 200:
|
||||||
|
return True, f"Merged: {merge_r.json().get('sha', '')}"
|
||||||
|
else:
|
||||||
|
return False, f"Merge failed: {merge_r.status_code} - {merge_r.json().get('message', '')}"
|
||||||
|
|
||||||
|
def has_bot_comment_with_review(comments: list) -> tuple:
|
||||||
|
"""Check if PR-Agent has posted a review comment and extract quality"""
|
||||||
|
bot_name = "mytheclipsebotreview"
|
||||||
|
for c in comments:
|
||||||
|
if c.get("user", {}).get("login", "").startswith(bot_name):
|
||||||
|
body = c.get("body", "")
|
||||||
|
# Check for PR Reviewer Guide (successful review)
|
||||||
|
if "PR Reviewer Guide" in body:
|
||||||
|
# Extract score if available
|
||||||
|
score = extract_score(body)
|
||||||
|
return True, score
|
||||||
|
return False, 0
|
||||||
|
|
||||||
|
def extract_score(body: str) -> int:
|
||||||
|
"""Extract review score from bot comment"""
|
||||||
|
import re
|
||||||
|
# Look for score patterns like "Score: 8" or "⏱️ Estimated effort"
|
||||||
|
# For now, assume passing if we got a review without errors
|
||||||
|
return 8
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=" * 60)
|
||||||
|
print(f"PR-Agent Auto-Approve/Merge Bot - {time.ctime()}")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Get installations
|
||||||
|
installations = get_all_installations()
|
||||||
|
print(f"Found {len(installations)} installation(s)")
|
||||||
|
|
||||||
|
for inst in installations:
|
||||||
|
inst_id = inst["id"]
|
||||||
|
account = inst["account"]["login"]
|
||||||
|
print(f"\n📦 Installation {inst_id} - @{account}")
|
||||||
|
|
||||||
|
# Get token
|
||||||
|
token = get_installation_token(inst_id)
|
||||||
|
if not token:
|
||||||
|
print(f" ❌ Failed to get token")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Get repos
|
||||||
|
repos = get_installation_repos(inst_id, token)
|
||||||
|
print(f" Repos: {len(repos)}")
|
||||||
|
|
||||||
|
for repo in repos:
|
||||||
|
repo_full = repo["full_name"]
|
||||||
|
print(f"\n 📁 {repo_full}")
|
||||||
|
|
||||||
|
# Get open PRs
|
||||||
|
prs = get_open_prs(token, repo_full)
|
||||||
|
print(f" Open PRs: {len(prs)}")
|
||||||
|
|
||||||
|
for pr in prs[:5]: # Max 5 per repo
|
||||||
|
pr_num = pr["number"]
|
||||||
|
pr_title = pr["title"]
|
||||||
|
pr_user = pr["user"]["login"]
|
||||||
|
pr_author = pr_user
|
||||||
|
|
||||||
|
print(f" 🔀 PR #{pr_num}: {pr_title[:50]}...")
|
||||||
|
|
||||||
|
# Skip bot PRs
|
||||||
|
if "[bot]" in pr_author or pr_author == "mytheclipsebotreview":
|
||||||
|
print(f" ⏭️ Bot PR, skipping")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check if already approved/merged
|
||||||
|
if pr.get("merged", False):
|
||||||
|
print(f" ✅ Already merged")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check reviews
|
||||||
|
reviews = get_pr_reviews(token, repo_full, pr_num)
|
||||||
|
bot_approved = any(
|
||||||
|
r.get("user", {}).get("login", "").startswith("mytheclipsebotreview")
|
||||||
|
and r.get("state") == "APPROVED"
|
||||||
|
for r in reviews
|
||||||
|
)
|
||||||
|
|
||||||
|
if bot_approved:
|
||||||
|
print(f" ✅ Already approved. Trying merge...")
|
||||||
|
success, msg = merge_pr(token, repo_full, pr_num)
|
||||||
|
print(f" {'✅' if success else '❌'} Merge: {msg}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check bot comments for review
|
||||||
|
comments = get_pr_comments(token, repo_full, pr_num)
|
||||||
|
has_review, score = has_bot_comment_with_review(comments)
|
||||||
|
|
||||||
|
if has_review and score >= 5:
|
||||||
|
print(f" 📝 Review found (score: {score}). Approving + merging...")
|
||||||
|
|
||||||
|
# Approve
|
||||||
|
if approve_pr(token, repo_full, pr_num):
|
||||||
|
print(f" ✅ Approved!")
|
||||||
|
else:
|
||||||
|
print(f" ❌ Approve failed")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Small delay
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
# Merge
|
||||||
|
success, msg = merge_pr(token, repo_full, pr_num)
|
||||||
|
print(f" {'✅ Merged!' if success else '❌ ' + msg}")
|
||||||
|
elif has_review and score < 5:
|
||||||
|
print(f" ⏭️ Review score too low ({score})")
|
||||||
|
else:
|
||||||
|
print(f" ⏳ No bot review yet")
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("Done!")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Quick callback server to receive GitHub App credentials after manifest creation"""
|
||||||
|
import json, os, sys
|
||||||
|
sys.path.insert(0, os.path.expanduser("~/hermes-agent/.venv/lib/python3.12/site-packages"))
|
||||||
|
|
||||||
|
from fastapi import FastAPI, Request
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
@app.get("/setup/callback")
|
||||||
|
@app.post("/setup/callback")
|
||||||
|
async def callback(request: Request):
|
||||||
|
params = dict(request.query_params)
|
||||||
|
print(f"[CALLBACK] Received params: {json.dumps(params, indent=2)}")
|
||||||
|
|
||||||
|
# If we got a code, exchange it for credentials
|
||||||
|
if "code" in params:
|
||||||
|
import httpx
|
||||||
|
code = params["code"]
|
||||||
|
print(f"[CALLBACK] Exchanging code: {code[:20]}...")
|
||||||
|
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
resp = await client.post(
|
||||||
|
f"https://api.github.com/app-manifests/{code}/conversions",
|
||||||
|
headers={"Accept": "application/vnd.github.v3+json"}
|
||||||
|
)
|
||||||
|
if resp.status_code == 201:
|
||||||
|
data = resp.json()
|
||||||
|
# Save credentials
|
||||||
|
creds = {
|
||||||
|
"app_id": data.get("id"),
|
||||||
|
"app_slug": data.get("slug"),
|
||||||
|
"pem": data.get("pem"),
|
||||||
|
"webhook_secret": data.get("webhook_secret"),
|
||||||
|
"client_id": data.get("client_id"),
|
||||||
|
"client_secret": data.get("client_secret")
|
||||||
|
}
|
||||||
|
with open("/opt/pr-agent-server/app_credentials.json", "w") as f:
|
||||||
|
json.dump(creds, f, indent=2)
|
||||||
|
print(f"[CALLBACK] App created! ID: {creds['app_id']}, Slug: {creds['app_slug']}")
|
||||||
|
return {"status": "success", "app_id": creds["app_id"], "app_slug": creds["app_slug"]}
|
||||||
|
else:
|
||||||
|
print(f"[CALLBACK] Exchange failed: {resp.status_code} - {resp.text}")
|
||||||
|
return {"status": "error", "detail": resp.text}
|
||||||
|
|
||||||
|
return {"status": "waiting", "params": params}
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
async def health():
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=3000, log_level="info")
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
description = "PR-Agent Server — GitHub App webhook server (Nix build)";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, flake-utils }:
|
||||||
|
flake-utils.lib.eachDefaultSystem (system:
|
||||||
|
let
|
||||||
|
pkgs = import nixpkgs { inherit system; };
|
||||||
|
python = pkgs.python312;
|
||||||
|
in {
|
||||||
|
packages.default = pkgs.stdenv.mkDerivation {
|
||||||
|
pname = "pr-agent-server";
|
||||||
|
version = "1.0.0";
|
||||||
|
src = ./.;
|
||||||
|
|
||||||
|
nativeBuildInputs = [ python pkgs.git pkgs.cacert ];
|
||||||
|
|
||||||
|
buildPhase = ''
|
||||||
|
export HOME=$TMPDIR/home
|
||||||
|
mkdir -p "$HOME"
|
||||||
|
export SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt
|
||||||
|
export NODE_EXTRA_CA_CERTS=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt
|
||||||
|
|
||||||
|
echo "=== Creating venv ==="
|
||||||
|
python -m venv $out/venv
|
||||||
|
$out/venv/bin/pip install --no-cache-dir --upgrade pip 2>&1
|
||||||
|
|
||||||
|
echo "=== pip install deps ==="
|
||||||
|
$out/venv/bin/pip install --no-cache-dir \
|
||||||
|
pr-agent fastapi uvicorn httpx pyjwt 2>&1
|
||||||
|
echo "=== Build complete ==="
|
||||||
|
'';
|
||||||
|
|
||||||
|
installPhase = ''
|
||||||
|
mkdir -p $out/bin $out/lib/pr-agent-server
|
||||||
|
cp run_server.py $out/lib/pr-agent-server/
|
||||||
|
|
||||||
|
cat > $out/bin/pr-agent-server << WRAPPER
|
||||||
|
#!${pkgs.runtimeShell}
|
||||||
|
export PATH=${pkgs.git}/bin:\$PATH
|
||||||
|
cd $out/lib/pr-agent-server
|
||||||
|
exec $out/venv/bin/python run_server.py
|
||||||
|
WRAPPER
|
||||||
|
chmod +x $out/bin/pr-agent-server
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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"http://45.127.35.244:3000/api/v1/github_webhooks",
|
||||||
|
"active": True
|
||||||
|
},
|
||||||
|
"redirect_url": "http://45.127.35.244:3000/setup/callback",
|
||||||
|
"callback_urls": ["http://45.127.35.244:3000/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,24 @@
|
|||||||
|
{
|
||||||
|
"name": "pr-agent-auto-review",
|
||||||
|
"url": "https://github.com/asepharyana",
|
||||||
|
"hook_attributes": {
|
||||||
|
"url": "http://45.127.35.244:3000/api/v1/github_webhooks",
|
||||||
|
"active": true
|
||||||
|
},
|
||||||
|
"redirect_url": "http://45.127.35.244:3000/setup/callback",
|
||||||
|
"callback_urls": [
|
||||||
|
"http://45.127.35.244:3000/setup/callback"
|
||||||
|
],
|
||||||
|
"public": false,
|
||||||
|
"default_events": [
|
||||||
|
"pull_request",
|
||||||
|
"issue_comment"
|
||||||
|
],
|
||||||
|
"default_permissions": {
|
||||||
|
"pull_requests": "write",
|
||||||
|
"issues": "write",
|
||||||
|
"contents": "read",
|
||||||
|
"metadata": "read",
|
||||||
|
"checks": "write"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "pr-agent-auto-review",
|
||||||
|
"url": "https://github.com/asepharyana",
|
||||||
|
"hook_attributes": {
|
||||||
|
"url": "http://45.127.35.244:3000/api/v1/github_webhooks",
|
||||||
|
"active": true
|
||||||
|
},
|
||||||
|
"redirect_url": "http://45.127.35.244:3000/setup/callback",
|
||||||
|
"callback_urls": [
|
||||||
|
"http://45.127.35.244:3000/setup/callback"
|
||||||
|
],
|
||||||
|
"public": false,
|
||||||
|
"default_events": [
|
||||||
|
"pull_request",
|
||||||
|
"issue_comment"
|
||||||
|
],
|
||||||
|
"default_permissions": {
|
||||||
|
"pull_requests": "write",
|
||||||
|
"issues": "write",
|
||||||
|
"contents": "read",
|
||||||
|
"metadata": "read",
|
||||||
|
"checks": "write"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""PR-Agent GitHub App + manifest callback server"""
|
||||||
|
import os, sys, json
|
||||||
|
|
||||||
|
# Configurable paths (systemd Nix deployment keeps secrets outside the store)
|
||||||
|
APP_DIR = os.environ.get("PR_AGENT_APP_DIR", "/var/lib/pr-agent-server")
|
||||||
|
private_key_path = os.environ.get(
|
||||||
|
"PRIVATE_KEY_PATH", os.path.join(APP_DIR, "private-key.pem")
|
||||||
|
)
|
||||||
|
omni_key_path = os.environ.get(
|
||||||
|
"OMNIROUTE_KEY_PATH", os.path.join(APP_DIR, "omniroute_key")
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(private_key_path) as f:
|
||||||
|
private_key = f.read()
|
||||||
|
|
||||||
|
os.environ["GITHUB__DEPLOYMENT_TYPE"] = "app"
|
||||||
|
os.environ["GITHUB__APP_ID"] = os.environ.get("GITHUB_APP_ID", "4319749")
|
||||||
|
os.environ["GITHUB__PRIVATE_KEY"] = private_key
|
||||||
|
os.environ["GITHUB__WEBHOOK_SECRET"] = os.environ.get("GITHUB_WEBHOOK_SECRET", "")
|
||||||
|
|
||||||
|
with open(omni_key_path) as f:
|
||||||
|
omni_key = f.read().strip()
|
||||||
|
os.environ["OPENAI__API_BASE"] = os.environ.get(
|
||||||
|
"OPENAI_API_BASE", "https://omniroute.imrnes.team/v1"
|
||||||
|
)
|
||||||
|
os.environ["OPENAI__KEY"] = omni_key
|
||||||
|
os.environ["CONFIG__MODEL"] = os.environ.get("PR_AGENT_MODEL", "openai/claude-opus-4-8")
|
||||||
|
os.environ["CONFIG__FALLBACK_MODELS"] = os.environ.get(
|
||||||
|
"PR_AGENT_FALLBACK_MODELS",
|
||||||
|
'["openai/auto/best-coding","openai/auto/claude-sonnet"]',
|
||||||
|
)
|
||||||
|
os.environ["CONFIG__CUSTOM_MODEL_MAX_TOKENS"] = os.environ.get(
|
||||||
|
"PR_AGENT_MAX_TOKENS", "128000"
|
||||||
|
)
|
||||||
|
os.environ["GITHUB_APP__PR_COMMANDS"] = os.environ.get(
|
||||||
|
"PR_AGENT_PR_COMMANDS",
|
||||||
|
'["/review --pr_reviewer.require_score_review=true --pr_reviewer.require_security_review=true","/describe","/improve"]',
|
||||||
|
)
|
||||||
|
|
||||||
|
sys.path.insert(0, APP_DIR)
|
||||||
|
from pr_agent.servers.github_app import app as pr_agent_app, router as pr_router
|
||||||
|
from fastapi import FastAPI, Request
|
||||||
|
import uvicorn
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from starlette.middleware import Middleware
|
||||||
|
from starlette_context.middleware import RawContextMiddleware
|
||||||
|
|
||||||
|
app = FastAPI(middleware=[Middleware(RawContextMiddleware)])
|
||||||
|
app.include_router(pr_router)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/setup/callback")
|
||||||
|
@app.post("/setup/callback")
|
||||||
|
async def callback(request: Request):
|
||||||
|
params = dict(request.query_params)
|
||||||
|
if "code" in params:
|
||||||
|
code = params["code"]
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
resp = await client.post(
|
||||||
|
f"https://api.github.com/app-manifests/{code}/conversions",
|
||||||
|
headers={"Accept": "application/vnd.github.v3+json"},
|
||||||
|
)
|
||||||
|
if resp.status_code == 201:
|
||||||
|
data = resp.json()
|
||||||
|
creds = {
|
||||||
|
"app_id": data.get("id"),
|
||||||
|
"pem": data.get("pem"),
|
||||||
|
"webhook_secret": data.get("webhook_secret"),
|
||||||
|
"slug": data.get("slug"),
|
||||||
|
}
|
||||||
|
with open(os.path.join(APP_DIR, "credentials_callback.json"), "w") as f:
|
||||||
|
json.dump(creds, f, indent=2)
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"app_id": creds["app_id"],
|
||||||
|
"slug": creds["slug"],
|
||||||
|
}
|
||||||
|
return {"status": "ok", "message": "callback received"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
async def health():
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
port = int(os.environ.get("PORT", "3000"))
|
||||||
|
print(f"PR-Agent GitHub App server starting...")
|
||||||
|
print(f" App ID: {os.environ.get('GITHUB_APP_ID', '')}")
|
||||||
|
print(f" Model: {os.environ.get('PR_AGENT_MODEL', 'openai/claude-opus-4-8')} via omniroute")
|
||||||
|
print(f" Endpoint: /api/v1/github_webhooks")
|
||||||
|
print(f" Port: {port}")
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=port, log_level="info")
|
||||||
+139
@@ -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": "http://45.127.35.244:3000/api/v1/github_webhooks",
|
||||||
|
"active": True
|
||||||
|
},
|
||||||
|
"redirect_url": "http://45.127.35.244:3000/setup/callback",
|
||||||
|
"callback_urls": ["http://45.127.35.244:3000/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 = """
|
||||||
|
# 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 = "claude-opus-4-8"
|
||||||
|
fallback_models = ["auto/best-coding", "auto/claude-sonnet"]
|
||||||
|
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 = 3000
|
||||||
|
WEBHOOK_URL = f"http://{PUBLIC_IP}:{PORT}/api/v1/github_webhooks"
|
||||||
|
REDIRECT_URL = f"http://{PUBLIC_IP}:{PORT}/app-setup-complete"
|
||||||
|
CALLBACK_URLS = [f"http://{PUBLIC_IP}:{PORT}/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}")
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""PR-Agent GitHub Webhook Server - Start Script"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Add the pr-agent package to path
|
||||||
|
sys.path.insert(0, os.path.expanduser("~/hermes-agent/.venv/lib/python3.12/site-packages"))
|
||||||
|
|
||||||
|
from pr_agent.servers.github_app import app
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
port = int(os.environ.get("PORT", "3000"))
|
||||||
|
print(f"Starting PR-Agent GitHub App server on 0.0.0.0:{port}")
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=port, log_level="info")
|
||||||
Reference in New Issue
Block a user