Skip to content
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
35 changes: 13 additions & 22 deletions webhook_server/libs/github_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
from webhook_server.utils.context import WebhookContext, get_context
from webhook_server.utils.github_repository_settings import (
DEFAULT_BRANCH_PROTECTION,
get_github_app_slug,
get_repository_github_app_api,
)
from webhook_server.utils.github_retry import github_api_call
Expand Down Expand Up @@ -542,28 +543,18 @@ async def process(self) -> Any:

# Initialize app bot login for bot-PR identification (async)
if not self.app_bot_login:
_github_app_api = await github_api_call(
get_repository_github_app_api,
config_=self.config,
repository_name=self.repository_full_name,
logger=self.logger,
log_prefix=self.log_prefix,
)
if _github_app_api:
try:
self.app_bot_login = await github_api_call(
lambda: _github_app_api.get_user().login,
logger=self.logger,
log_prefix=self.log_prefix,
)
except asyncio.CancelledError:
raise
except Exception:
self.logger.exception(
f"{self.log_prefix} Failed to get app bot login — bot-PR detection may not work"
)
else:
self.logger.debug(f"{self.log_prefix} No GitHub App API available — app_bot_login not set")
try:
_app_slug = await github_api_call(
get_github_app_slug,
self.config,
logger=self.logger,
log_prefix=self.log_prefix,
)
self.app_bot_login = f"{_app_slug}[bot]"
except asyncio.CancelledError:
raise
except Exception:
self.logger.exception(f"{self.log_prefix} Failed to get app bot login — bot-PR detection may not work")

event_log: str = f"Event type: {self.github_event}. event ID: {self.x_github_delivery}"

Expand Down
51 changes: 41 additions & 10 deletions webhook_server/utils/github_repository_settings.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import copy
import os
import threading
from collections.abc import Callable
from concurrent.futures import Future, ThreadPoolExecutor, as_completed
from copy import deepcopy
Expand Down Expand Up @@ -43,6 +44,8 @@
}

LOGGER = get_logger_with_params()
_github_app_slug_cache: str | None = None
Comment thread
rnetser marked this conversation as resolved.
_github_app_slug_lock = threading.Lock()


def _get_github_repo_api(github_api: github.Github, repository: int | str) -> Repository | None:
Expand Down Expand Up @@ -407,15 +410,23 @@ def _set_checkrun_queued(_api: Repository, _pull_request: PullRequest) -> None:
return True, f"[API user {api_user}] - {repository}: Set check run status to {QUEUED_STR} is done", LOGGER.debug


def get_repository_github_app_api(config_: Config, repository_name: str) -> Github | None:
LOGGER.debug("Getting repositories GitHub app API")
def _create_github_integration(config_: Config) -> GithubIntegration:
"""Create a GithubIntegration instance with App JWT authentication.

Reads the private key and app ID from config to create an authenticated
GithubIntegration. This is the shared setup for all GitHub App API operations.
"""
with open(os.path.join(config_.data_dir, "webhook-server.private-key.pem")) as fd:
private_key = fd.read()

github_app_id: int = config_.root_data["github-app-id"]
auth: AppAuth = Auth.AppAuth(app_id=github_app_id, private_key=private_key)
app_instance: GithubIntegration = GithubIntegration(auth=auth)
return GithubIntegration(auth=auth)


def get_repository_github_app_api(config_: Config, repository_name: str) -> Github | None:
LOGGER.debug("Getting repositories GitHub app API")
app_instance = _create_github_integration(config_)
owner: str
repo: str
owner, repo = repository_name.split("/")
Expand All @@ -432,19 +443,39 @@ def get_repository_github_app_api(config_: Config, repository_name: str) -> Gith
return None


Comment thread
rnetser marked this conversation as resolved.
def get_github_app_slug(config_: Config) -> str:
"""Get the GitHub App slug using App JWT authentication.

Returns the app slug (e.g., 'manage-repositories-app').
Caches the result at module level since the slug is immutable.
Raises on failure so github_api_call() can apply retry/backoff.
"""
global _github_app_slug_cache

if _github_app_slug_cache is not None:
return _github_app_slug_cache
Comment thread
rnetser marked this conversation as resolved.

with _github_app_slug_lock:
# Double-checked locking
if _github_app_slug_cache is not None:
return _github_app_slug_cache

LOGGER.debug("Getting GitHub App slug")
app_instance = _create_github_integration(config_)
slug = app_instance.get_app().slug
if not slug:
raise ValueError("GitHub App returned empty slug")
_github_app_slug_cache = slug
return _github_app_slug_cache


def get_repository_github_app_token(config_: Config, repository_name: str) -> str | None:
"""Get a raw GitHub App installation token string for use with CLI tools.

Returns the token string or None if the app is not configured/installed.
"""
LOGGER.debug(f"Getting GitHub App installation token for {repository_name}")

with open(os.path.join(config_.data_dir, "webhook-server.private-key.pem")) as fd:
private_key = fd.read()

github_app_id: int = config_.root_data["github-app-id"]
auth: AppAuth = Auth.AppAuth(app_id=github_app_id, private_key=private_key)
app_instance: GithubIntegration = GithubIntegration(auth=auth)
app_instance = _create_github_integration(config_)
owner, repo = repository_name.split("/", maxsplit=1)

try:
Expand Down