diff --git a/config.py b/config.py index d466ab00..656aec40 100644 --- a/config.py +++ b/config.py @@ -19,10 +19,15 @@ def get_env_var(name: str) -> str: # GitHub Credentials from environment variables GITHUB_API_URL: str = "https://api.github.com" GITHUB_API_VERSION: str = "2022-11-28" -GH_APP_ID: str = get_env_var(name="GH_APP_ID") -GH_PRIVATE_KEY_ENCODED: str = get_env_var(name="GH_PRIVATE_KEY") -GH_PRIVATE_KEY: bytes = base64.b64decode(s=GH_PRIVATE_KEY_ENCODED) -GH_WEBHOOK_SECRET: str = get_env_var(name="GH_WEBHOOK_SECRET") +GITHUB_APP_ID = int(get_env_var(name="GH_APP_ID")) +GITHUB_APP_IDS: list[int] = list(set([ + GITHUB_APP_ID, # Production or your local development + 844909, # Production + 901480 # Staging +])) +GITHUB_PRIVATE_KEY_ENCODED: str = get_env_var(name="GH_PRIVATE_KEY") +GITHUB_PRIVATE_KEY: bytes = base64.b64decode(s=GITHUB_PRIVATE_KEY_ENCODED) +GITHUB_WEBHOOK_SECRET: str = get_env_var(name="GH_WEBHOOK_SECRET") # OpenAI Credentials from environment variables OPENAI_API_KEY: str = get_env_var(name="OPENAI_API_KEY") diff --git a/main.py b/main.py index c363fa63..3db9481e 100644 --- a/main.py +++ b/main.py @@ -10,7 +10,7 @@ from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration # Local imports -from config import GH_WEBHOOK_SECRET, ENV, PRODUCT_NAME +from config import GITHUB_WEBHOOK_SECRET, ENV, PRODUCT_NAME, UTF8 from services.github.github_manager import verify_webhook_signature from services.webhook_handler import handle_webhook_event @@ -30,14 +30,17 @@ @app.post(path="/webhook") async def handle_webhook(request: Request) -> dict[str, str]: - content_type: str = request.headers.get("Content-Type", "Content-Type not specified") + content_type: str = request.headers.get( + "Content-Type", "Content-Type not specified" + ) event_name: str = request.headers.get("X-GitHub-Event", "Event not specified") + print("\n" * 3 + "-" * 70) print(f"Received event: {event_name} with content type: {content_type}") # Validate if the webhook signature comes from GitHub try: print("Webhook received") - await verify_webhook_signature(request=request, secret=GH_WEBHOOK_SECRET) + await verify_webhook_signature(request=request, secret=GITHUB_WEBHOOK_SECRET) print("Webhook signature verified") except Exception as e: print(f"Error: {e}") @@ -48,17 +51,19 @@ async def handle_webhook(request: Request) -> dict[str, str]: request_body: bytes = await request.body() except Exception as e: print(f"Error in reading request body: {e}") - request_body = b'' + request_body = b"" payload: Any = {} try: # First try to parse the body as JSON - payload = json.loads(s=request_body.decode(encoding='utf-8')) + payload = json.loads(s=request_body.decode(encoding=UTF8)) except json.JSONDecodeError: # If JSON parsing fails, treat the body as URL-encoded - decoded_body: dict[str, list[str]] = urllib.parse.parse_qs(qs=request_body.decode(encoding='utf-8')) - if 'payload' in decoded_body: - payload = json.loads(s=decoded_body['payload'][0]) + decoded_body: dict[str, list[str]] = urllib.parse.parse_qs( + qs=request_body.decode(encoding=UTF8) + ) + if "payload" in decoded_body: + payload = json.loads(s=decoded_body["payload"][0]) except Exception as e: print(f"Error in parsing JSON payload: {e}") diff --git a/requirements.txt b/requirements.txt index c21a9995..77f8f50d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,46 +1,46 @@ -annotated-types==0.6.0 -anyio==4.3.0 -black==24.2.0 -certifi==2024.2.2 +annotated-types==0.7.0 +anyio==4.4.0 +black==24.4.2 +certifi==2024.6.2 cffi==1.16.0 charset-normalizer==3.3.2 click==8.1.7 colorama==0.4.6 -cryptography==42.0.5 +cryptography==42.0.8 deprecation==2.1.0 distro==1.9.0 -exceptiongroup==1.2.0 -fastapi==0.110.0 -gotrue==2.4.1 +exceptiongroup==1.2.1 +fastapi==0.111.0 +gotrue==2.5.2 h11==0.14.0 -httpcore==1.0.4 -httpx==0.25.2 -idna==3.6 +httpcore==1.0.5 +httpx==0.27.0 +idna==3.7 mangum==0.17.0 -openai==1.30.1 -packaging==23.2 -postgrest==0.16.1 -pre-commit==3.6.2 -pycparser==2.21 -pydantic==2.6.3 -pydantic_core==2.16.3 +openai==1.35.3 +packaging==24.1 +postgrest==0.16.8 +pre-commit==3.7.1 +pycparser==2.22 +pydantic==2.7.4 +pydantic_core==2.18.4 PyJWT==2.8.0 -pylint==3.1.0 +pylint==3.2.3 python-dateutil==2.9.0.post0 python-dotenv==1.0.1 -realtime==1.0.2 -requests==2.31.0 -sentry-sdk==1.41.0 +realtime==1.0.6 +requests==2.32.3 +sentry-sdk==2.6.0 six==1.16.0 sniffio==1.3.1 -starlette==0.36.3 -storage3==0.7.0 +starlette==0.37.2 +storage3==0.7.6 StrEnum==0.4.15 -stripe==8.8.0 -supabase==2.4.0 -supafunc==0.3.3 -tqdm==4.66.2 -typing_extensions==4.10.0 -urllib3==2.2.1 -uvicorn==0.27.1 -websockets==11.0.3 +stripe==9.12.0 +supabase==2.5.1 +supafunc==0.4.6 +tqdm==4.66.4 +typing_extensions==4.12.2 +urllib3==2.2.2 +uvicorn==0.30.1 +websockets==12.0 diff --git a/services/github/github_manager.py b/services/github/github_manager.py index 91418331..2a3e9f3d 100644 --- a/services/github/github_manager.py +++ b/services/github/github_manager.py @@ -3,6 +3,7 @@ import datetime import hashlib # For HMAC (Hash-based Message Authentication Code) signatures import hmac # For HMAC (Hash-based Message Authentication Code) signatures +import json import logging import os import time @@ -17,14 +18,16 @@ from config import ( GITHUB_API_URL, GITHUB_API_VERSION, - GH_APP_ID, - GH_PRIVATE_KEY, + GITHUB_APP_ID, + GITHUB_APP_IDS, + GITHUB_PRIVATE_KEY, PRODUCT_NAME, PRODUCT_URL, TIMEOUT_IN_SECONDS, PRODUCT_ID, SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, + UTF8, ) from services.github.github_types import GitHubContentInfo, GitHubLabeledPayload from services.supabase import SupabaseManager @@ -113,7 +116,7 @@ def commit_changes_to_remote_branch( content: str = file_info.get("content") # content is base64 encoded by default in GitHub API original_text: str = base64.b64decode(s=content).decode( - encoding="utf-8", errors="replace" + encoding=UTF8, errors="replace" ) sha: str = file_info["sha"] elif response.status_code != 404: # Error other than 'file not found' @@ -127,9 +130,9 @@ def commit_changes_to_remote_branch( return data: dict[str, str | None] = { "message": commit_message, - "content": base64.b64encode( - s=modified_text.encode(encoding="utf-8") - ).decode(encoding="utf-8"), + "content": base64.b64encode(s=modified_text.encode(encoding=UTF8)).decode( + encoding=UTF8 + ), "branch": branch, } if sha != "": @@ -268,10 +271,10 @@ def create_jwt() -> str: payload: dict[str, int | str] = { "iat": now, # Issued at time "exp": now + 600, # JWT expires in 10 minutes - "iss": GH_APP_ID, # Issuer + "iss": GITHUB_APP_ID, # Issuer } # The reason we use RS256 is that GitHub requires it for JWTs - return jwt.encode(payload=payload, key=GH_PRIVATE_KEY, algorithm="RS256") + return jwt.encode(payload=payload, key=GITHUB_PRIVATE_KEY, algorithm="RS256") @handle_exceptions(default_return_value=None, raise_on_error=False) @@ -326,9 +329,7 @@ def initialize_repo(repo_path: str, remote_url: str) -> None: os.makedirs(name=repo_path) run_command(command="git init", cwd=repo_path) - with open( - file=os.path.join(repo_path, "README.md"), mode="w", encoding="utf-8" - ) as f: + with open(file=os.path.join(repo_path, "README.md"), mode="w", encoding=UTF8) as f: f.write(f"# Initial commit by [{PRODUCT_NAME}]({PRODUCT_URL})\n") run_command(command="git add README.md", cwd=repo_path) run_command(command='git commit -m "Initial commit"', cwd=repo_path) @@ -360,8 +361,15 @@ def get_issue_comments( timeout=TIMEOUT_IN_SECONDS, ) response.raise_for_status() - comments = response.json() - comment_texts: list[str] = [comment["body"] for comment in comments] + comments: list[Any] = response.json() + print(f"GITHUB_APP_IDS: {GITHUB_APP_IDS}") + filtered_comments: list[Any] = [ + comment + for comment in comments + if comment.get("performed_via_github_app", {}).get("id") not in GITHUB_APP_IDS + ] + print(f"\nIssue comments: {json.dumps(filtered_comments, indent=2)}\n") + comment_texts: list[str] = [comment["body"] for comment in filtered_comments] return comment_texts @@ -432,7 +440,7 @@ def get_remote_file_content( ) response.raise_for_status() encoded_content: str = response.json()["content"] - decoded_content: str = base64.b64decode(s=encoded_content).decode(encoding="utf-8") + decoded_content: str = base64.b64decode(s=encoded_content).decode(encoding=UTF8) return decoded_content