Skip to content

Commit

Permalink
refactor(install): Refactor GitHub App Installation Logic (#581)
Browse files Browse the repository at this point in the history
* 重构GitHub APP 安装逻辑
* 增加 repositoryDAO 批量删除和添加的方法,替代for循环入库
* 增加 InstallationEditEventHandler 响应 Repository access 的编辑逻辑
close #537 
close #566 

TODO: 部署上线验证无误后可以删除 github_app_authorization 表及其类型定义和引用
  • Loading branch information
RaoHai authored Dec 10, 2024
2 parents d1465b9 + 52646b4 commit 613ee11
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 97 deletions.
154 changes: 104 additions & 50 deletions server/core/dao/repositoryConfigDAO.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
from typing import List
from typing import Counter, List
from core.dao.BaseDAO import BaseDAO
from core.models.bot import RepoBindBotConfigVO
from core.models.repository import RepositoryConfig
from supabase.client import Client

from petercat_utils.db.client.supabase import get_client


Expand All @@ -26,65 +25,120 @@ def create(self, data: RepositoryConfig):
else:
return False, {"message": "GithubRepoConfig creation failed"}
except Exception as e:
print("Error: ", e)
return False, {"message": "GithubRepoConfig creation failed"}
print(f"Error: {e}")
return False, {"message": f"GithubRepoConfig creation failed: {e}"}

def create_batch(self, data_list: List[RepositoryConfig]):
try:
records_data = [data.model_dump(exclude=["id"]) for data in data_list]
repo_ids = [data.repo_id for data in data_list]

# 查询现有的 repo_id
response = (
self.client.table("github_repo_config")
.select("repo_id")
.in_("repo_id", repo_ids)
.execute()
)
existing_repo_ids = (
{record["repo_id"] for record in response.data}
if response.data
else set()
)

def query_by_owners(self, orgs: list[str]):
response = (
self.client.table("github_repo_config")
.select("*")
.filter("owner_id", "in", f"({','.join(map(str, orgs))})")
.execute()
)
# 筛选出未存在的记录
new_records_data = [
data
for data in records_data
if data["repo_id"] not in existing_repo_ids
]

return response.data
if not new_records_data:
return False, {
"message": "No new GithubRepoConfig records to insert, all repo_ids already exist"
}

def update_bot_to_repos(
self,
repos: List[RepoBindBotConfigVO],
) -> bool:
for repo in repos:
res = (
# 执行插入操作
repo_config_result = (
self.client.table("github_repo_config")
.update({"robot_id": repo.robot_id})
.match({"repo_id": repo.repo_id})
.insert(new_records_data)
.execute()
)
if not res:
raise ValueError("Failed to bind the bot.")

def get_by_repo_name(self, repo_name: str):
response = (
self.client.table("github_repo_config")
.select("*")
.eq("repo_name", repo_name)
.execute()
)
return repo_config_result
except Exception as e:
print(f"Error: {e}")
return False, {"message": f"GithubRepoConfig batch creation failed: {e}"}

if not response.data or not response.data[0]:
def query_by_owners(self, orgs: List[str]):
try:
response = (
self.client.table("github_repo_config")
.select("*")
.filter("owner_id", "in", f"({','.join(map(str, orgs))})")
.execute()
)
return response.data
except Exception as e:
print(f"Error: {e}")
return None
repo_config = response.data[0]

return RepositoryConfig(**repo_config)
def update_bot_to_repos(self, repos: List[RepoBindBotConfigVO]) -> bool:
try:
for repo in repos:
res = (
self.client.table("github_repo_config")
.update({"robot_id": repo.robot_id})
.match({"repo_id": repo.repo_id})
.execute()
)
if not res:
raise ValueError("Failed to bind the bot.")
return True
except Exception as e:
print(f"Error: {e}")
return False

def get_by_bot_id(self, bot_id: str):
response = (
self.client.table("github_repo_config")
.select("*")
.eq("robot_id", bot_id)
.execute()
)
if not response.data or not response.data[0]:
def get_by_repo_name(self, repo_name: str):
try:
response = (
self.client.table("github_repo_config")
.select("*")
.eq("repo_name", repo_name)
.execute()
)
if not response.data or not response.data[0]:
return None
repo_config = response.data[0]
return RepositoryConfig(**repo_config)
except Exception as e:
print(f"Error: {e}")
return None
repo_configs = [RepositoryConfig(**repo) for repo in response.data]

return repo_configs
def get_by_bot_id(self, bot_id: str):
try:
response = (
self.client.table("github_repo_config")
.select("*")
.eq("robot_id", bot_id)
.execute()
)
if not response.data or not response.data[0]:
return None
return [RepositoryConfig(**repo) for repo in response.data]
except Exception as e:
print(f"Error: {e}")
return None

def delete_by_repo_ids(self, repo_ids: list):
response = (
self.client.table("github_repo_config")
.delete()
.in_("repo_id", repo_ids)
.execute()
)
return response
def delete_by_repo_ids(self, repo_ids: List[str]):
try:
response = (
self.client.table("github_repo_config")
.delete()
.in_("repo_id", repo_ids)
.execute()
)
return response
except Exception as e:
print(f"Error: {e}")
return None
71 changes: 69 additions & 2 deletions server/event_handler/intsall.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from typing import Any
from typing import Any, List
from github import Github, Auth
from github import GithubException
from core.dao.repositoryConfigDAO import RepositoryConfigDAO
from core.models.repository import RepositoryConfig
import time


class InstallEventHandler:
class InstallationEventHandler:
event: Any
auth: Auth.AppAuth
g: Github
Expand All @@ -20,12 +22,77 @@ def delete_config(self):
repository_config = RepositoryConfigDAO()
repository_config.delete_by_repo_ids(repo_ids)

def add_config(self):
repositories = self.event["repositories"]
owner_id = self.event["installation"]["account"]["id"]
repository_config_dao = RepositoryConfigDAO()
repository_configs: List[RepositoryConfig] = []
for repo in repositories:
repository_config = RepositoryConfig(
owner_id=str(owner_id),
repo_name=repo["full_name"],
repo_id=str(repo["id"]),
robot_id="",
created_at=int(time.time()),
)
repository_configs.append(repository_config)
repository_config_dao.create_batch(repository_configs)

async def execute(self):
try:
action = self.event["action"]
if action == "deleted":
self.delete_config()
return {"success": True}
if action == "created":
self.add_config()
return {"success": True}
except GithubException as e:
print(f"处理 GitHub 请求时出错:{e}")
return {"success": False, "error": str(e)}


class InstallationEditEventHandler:
event: Any
auth: Auth.AppAuth
g: Github

def __init__(self, payload: Any, auth: Auth.AppAuth, installation_id: int) -> None:
self.event: Any = payload
self.auth: Auth.AppAuth = auth
self.g: Github = Github(auth=auth)

def delete_config(self):
repositories = self.event["repositories_removed"]
repo_ids = [str(repo["id"]) for repo in repositories]
repository_config = RepositoryConfigDAO()
repository_config.delete_by_repo_ids(repo_ids)

def add_config(self):
repositories = self.event["repositories_added"]
owner_id = self.event["installation"]["account"]["id"]
repository_config_dao = RepositoryConfigDAO()
repository_configs: List[RepositoryConfig] = []
for repo in repositories:
repository_config = RepositoryConfig(
owner_id=str(owner_id),
repo_name=repo["full_name"],
repo_id=str(repo["id"]),
robot_id="",
created_at=int(time.time()),
)
repository_configs.append(repository_config)
repository_config_dao.create_batch(repository_configs)

async def execute(self):
try:
action = self.event["action"]
if action == "removed":
self.delete_config()
return {"success": True}
if action == "added":
self.add_config()
return {"success": True}
except GithubException as e:
print(f"处理 GitHub 请求时出错:{e}")
return {"success": False, "error": str(e)}
8 changes: 5 additions & 3 deletions server/github_app/handlers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Union

from event_handler.intsall import InstallEventHandler
from event_handler.intsall import InstallationEventHandler, InstallationEditEventHandler
from petercat_utils import get_env_variable
from github import Auth

Expand All @@ -26,7 +26,8 @@ def get_handler(
DiscussionEventHandler,
DiscussionCommentEventHandler,
PullRequestReviewCommentEventHandler,
InstallEventHandler,
InstallationEventHandler,
InstallationEditEventHandler,
None,
]:
handlers = {
Expand All @@ -37,7 +38,8 @@ def get_handler(
"discussion_comment": DiscussionCommentEventHandler,
"pull_request_review_comment": PullRequestReviewCommentEventHandler,
"pull_request_review": PullRequestReviewCommentEventHandler,
"installation": InstallEventHandler,
"installation": InstallationEventHandler,
"installation_repositories": InstallationEditEventHandler,
}
return (
handlers.get(event)(payload=payload, auth=auth, installation_id=installation_id)
Expand Down
46 changes: 4 additions & 42 deletions server/github_app/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,48 +50,10 @@
# https://github.com/login/oauth/authorize?client_id=Iv1.c2e88b429e541264
@router.get("/app/installation/callback")
def github_app_callback(code: str, installation_id: str, setup_action: str):
authorization_dao = AuthorizationDAO()
repository_config_dao = RepositoryConfigDAO()
if setup_action == "install":
if authorization_dao.exists(installation_id=installation_id):
message = (f"Installation_id {installation_id} Exists",)
return RedirectResponse(
url=f"{WEB_URL}/github/installed/{message}", status_code=302
)
else:
jwt = get_jwt()
access_token = get_app_installations_access_token(
installation_id=installation_id, jwt=jwt
)
print(f"get_app_installations_access_token: {access_token}")
authorization = Authorization(
**access_token,
code=code,
installation_id=installation_id,
created_at=int(time.time()),
)
success, message = authorization_dao.create(authorization)
installed_repositories = get_installation_repositories(
access_token=access_token["token"]
)
for repo in installed_repositories["repositories"]:
repository_config = RepositoryConfig(
owner_id=str(repo["owner"]["id"]),
repo_name=repo["full_name"],
repo_id=str(repo["id"]),
robot_id="",
created_at=int(time.time()),
)
repository_config_dao.create(repository_config)

return RedirectResponse(
url=f"{WEB_URL}/github/installed?message={message}", status_code=302
)
# ignore others setup_action,such as deleted our app
return {
"success": False,
"message": f"Invalid setup_action value {setup_action},please delete the app first then re-install the app.",
}
return RedirectResponse(
url=f"{WEB_URL}/github/installed?installation_id={installation_id}&setup_action={setup_action}&code={code}",
status_code=302,
)


@router.post("/app/webhook")
Expand Down

0 comments on commit 613ee11

Please sign in to comment.