Skip to content

Add PR Event Logger workflow and authentication methods; remove old r… #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .bot
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ID=Iv23liGyTZJYhySo4cEM
SECRET=a0d16e1977f5dbd754649d9daa7d19d8ef32f38b
26 changes: 26 additions & 0 deletions .github/workflows/pr-reviewer.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: PR Event Logger

on:
pull_request:
types: [opened, reopened, ready_for_review, review_requested]
issue_comment:
types: [created, edited]

jobs:
log-event:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.9'

- name: log existing secrets
env:
API_TOKEN: ${{ secrets.API_TOKEN }}
WEBHOOK_SECRET: ${{ secrets.WEBHOOK_SECRET }}
ORG_TOKEN: ${{ secrets.ORG_TOKEN }}
run: echo $API_TOKEN $WEBHOOK_SECRET $ORG_TOKEN
43 changes: 0 additions & 43 deletions .github/workflows/reviewer.yml

This file was deleted.

19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# PR Reviewer bot
A bot that helps you to review the PRs in your repository.

## Get started

### Install the dependencies
```bash
pip3 install -r requirements.txt
```

### To run the bot
```bash
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
```

### Forward the port using ngrok
```bash
ngrok http 8000
```
38 changes: 38 additions & 0 deletions auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import base64
import hashlib
import hmac
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

def decrypt_token(encrypted_token, iv, WEBHOOK_SECRET):
"""Decrypt API token using WEBHOOK_SECRET as the key."""
# Generate the key from WEBHOOK_SECRET in the same way as the GitHub Action
key = hashlib.sha256(WEBHOOK_SECRET.encode()).hexdigest()
key_bytes = bytes.fromhex(key)
iv_bytes = bytes.fromhex(iv)

# Base64 decode the encrypted token
encrypted_data = base64.b64decode(encrypted_token)

# Create cipher and decrypt
cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes)
decrypted_bytes = cipher.decrypt(encrypted_data)

# Handle padding properly
try:
unpadded = unpad(decrypted_bytes, AES.block_size)
except ValueError:
# If unpadding fails, try to find the null termination
if b'\x00' in decrypted_bytes:
unpadded = decrypted_bytes[:decrypted_bytes.index(b'\x00')]
else:
unpadded = decrypted_bytes

return unpadded.decode('utf-8')


def verify_signature(secret, body, signature):
"""Verify GitHub webhook signature using HMAC-SHA256."""
mac = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
expected_signature = f"sha256={mac}"
return hmac.compare_digest(expected_signature, signature)
17 changes: 17 additions & 0 deletions github.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from fastapi import HTTPException, logger
import requests


def get_pr_commits(repo_full_name, pr_number, github_token):
"""Fetch the list of commits for a PR from GitHub API."""
url = f"https://api.github.com/repos/{repo_full_name}/pulls/{pr_number}/commits"
print(url)
headers = {"Authorization": f"{github_token}", "Accept": "application/vnd.github.v3+json"}

response = requests.get(url, headers=headers)

if response.status_code != 200:
logger.error(f"Failed to fetch commits: {response.text}")
raise HTTPException(status_code=500, detail="Error fetching PR commits")

return response.json()
123 changes: 1 addition & 122 deletions listener.py
Original file line number Diff line number Diff line change
@@ -1,122 +1 @@
import hmac
import hashlib
import json
import logging
import os
import base64
import requests
from fastapi import APIRouter, Request, Header, HTTPException
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

from dotenv import load_dotenv

load_dotenv()

router = APIRouter()
WEBHOOK_SECRET = os.getenv("WEBHOOK_SECRET")

logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)


def verify_signature(secret, body, signature):
"""Verify GitHub webhook signature using HMAC-SHA256."""
mac = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
expected_signature = f"sha256={mac}"
return hmac.compare_digest(expected_signature, signature)


def decrypt_token(encrypted_token, iv):
"""Decrypt API token using WEBHOOK_SECRET as the key."""
try:
# Generate the key from WEBHOOK_SECRET in the same way as the GitHub Action
key = hashlib.sha256(WEBHOOK_SECRET.encode()).hexdigest()
key_bytes = bytes.fromhex(key)
iv_bytes = bytes.fromhex(iv)

# Base64 decode the encrypted token
encrypted_data = base64.b64decode(encrypted_token)

# Create cipher and decrypt
cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes)
decrypted_bytes = cipher.decrypt(encrypted_data)

# Handle padding properly
try:
unpadded = unpad(decrypted_bytes, AES.block_size)
except ValueError:
# If unpadding fails, try to find the null termination
if b'\x00' in decrypted_bytes:
unpadded = decrypted_bytes[:decrypted_bytes.index(b'\x00')]
else:
unpadded = decrypted_bytes

return unpadded.decode('utf-8')
except Exception as e:
logger.error(f"Token decryption error: {str(e)}")
raise HTTPException(status_code=500, detail="Failed to decrypt token")


def get_pr_commits(repo_full_name, pr_number, github_token):
"""Fetch the list of commits for a PR from GitHub API."""
url = f"https://api.github.com/repos/{repo_full_name}/pulls/{pr_number}/commits"
print(url)
headers = {"Authorization": f"{github_token}", "Accept": "application/vnd.github.v3+json"}

response = requests.get(url, headers=headers)

if response.status_code != 200:
logger.error(f"Failed to fetch commits: {response.text}")
raise HTTPException(status_code=500, detail="Error fetching PR commits")

return response.json()


@router.post("/github-webhook")
async def github_webhook(
request: Request,
x_hub_signature_256: str = Header(None),
x_encrypted_token: str = Header(None, alias="X-Encrypted-Token"),
x_token_iv: str = Header(None, alias="X-Token-IV")
):
"""Receives GitHub webhook payload and fetches PR commits if applicable."""
body = await request.body()

# Verify webhook signature
if WEBHOOK_SECRET and x_hub_signature_256:
if not verify_signature(WEBHOOK_SECRET, body, x_hub_signature_256):
logger.error("Signature verification failed")
raise HTTPException(status_code=403, detail="Invalid signature")

# Validate encrypted token headers
if not x_encrypted_token or not x_token_iv:
logger.error("Missing encryption headers")
raise HTTPException(status_code=403, detail="Missing token encryption headers")

# Decrypt the token
try:
github_token = decrypt_token(x_encrypted_token, x_token_iv)
except Exception as e:
logger.error(f"Token decryption failed: {str(e)}")
raise HTTPException(status_code=403, detail="Token decryption failed")

payload = await request.json()
# save this locally
with open("samples/payload.json", "w") as f:
json.dump(payload, f)
event_type = payload.get("action", "")

logger.info(f"Received GitHub event: {event_type}")

if event_type == "synchronize":
action = payload.get("action", "")
if action in ["opened", "synchronize", "reopened"]:
repo_full_name = payload["repository"]["full_name"]
pr_number = payload["pull_request"]["number"]
commits = get_pr_commits(repo_full_name, pr_number, github_token)

logger.info(f"Fetched {len(commits)} commits for PR #{pr_number}")
return {"message": "PR processed", "pr_number": pr_number, "commits_count": len(commits)}

return {"message": "Webhook received", "event": event_type}
##