From 8566fcd0276294b88397f23862bae68af69ef49b Mon Sep 17 00:00:00 2001 From: Tomas David Date: Fri, 29 May 2026 15:30:14 +0200 Subject: [PATCH 1/3] OCPERT-368: Replace GitHub token with Github app for Jira Notificator rh-pre-commit.version: 2.4.0 rh-pre-commit.check-secrets: ENABLED --- oar/core/const.py | 4 +++ oar/core/github_app.py | 41 +++++++++++++++++++++ oar/notificator/jira_notificator.py | 55 +++++++++++++++++++++++++---- tests/test_jira_notificator.py | 8 ++--- 4 files changed, 98 insertions(+), 10 deletions(-) create mode 100644 oar/core/github_app.py diff --git a/oar/core/const.py b/oar/core/const.py index 9c2c4d2e3a43..8ba80c986859 100644 --- a/oar/core/const.py +++ b/oar/core/const.py @@ -135,6 +135,10 @@ ENV_APP_PASSWD = "GOOGLE_APP_PASSWD" ENV_JENKINS_USER = "JENKINS_USER" ENV_JENKINS_TOKEN = "JENKINS_TOKEN" +ENV_VAR_GITHUB_APP_WRITER_ID = "GITHUB_APP_WRITER_ID" +ENV_VAR_GITHUB_APP_WRITER_PRIVATE_KEY = "GITHUB_APP_WRITER_PRIVATE_KEY" +ENV_VAR_GITHUB_APP_READER_ID = "GITHUB_APP_READER_ID" +ENV_VAR_GITHUB_APP_READER_PRIVATE_KEY = "GITHUB_APP_READER_PRIVATE_KEY" # jira status JIRA_STATUS_CLOSED = "Closed" JIRA_STATUS_IN_PROGRESS = "In Progress" diff --git a/oar/core/github_app.py b/oar/core/github_app.py new file mode 100644 index 000000000000..fffdb5bbf457 --- /dev/null +++ b/oar/core/github_app.py @@ -0,0 +1,41 @@ +"""GitHub App auth for ERT (Writer: release-tests, Reader: openshift/*).""" + +from pathlib import Path + +from github import Auth, Github, GithubIntegration + + +class GitHubApp: + """PyGithub client via GitHub App installation token.""" + + def __init__(self, app_id: str, private_key: str): + """ + Initialize GitHub App authentication. + + Args: + app_id: Application ID (not Client ID). + private_key: PEM contents or path to a .pem file. + """ + key = private_key + key_path = Path(private_key).expanduser() + if key_path.is_file(): + key = key_path.read_text() + auth = Auth.AppAuth(app_id, key) + self._integration = GithubIntegration(auth=auth) + + def client_for_repo(self, owner: str, repo: str) -> Github: + """ + Return a Github client for ``owner/repo``. + + Args: + owner: GitHub org or user (e.g. ``openshift``). + repo: Repository name (e.g. ``release-tests``). + + Returns: + Installation-scoped ``Github`` client. + + Raises: + GithubException: App not installed on the repo or invalid credentials. + """ + installation = self._integration.get_repo_installation(owner, repo) + return self._integration.get_github_for_installation(installation.id) diff --git a/oar/notificator/jira_notificator.py b/oar/notificator/jira_notificator.py index 464b2cb5d04a..800f61af8ec0 100644 --- a/oar/notificator/jira_notificator.py +++ b/oar/notificator/jira_notificator.py @@ -9,7 +9,8 @@ from jira.resources import User from datetime import datetime, timedelta, timezone from dateutil import parser -from github import Auth, Github +from oar.core.const import ENV_VAR_GITHUB_APP_READER_ID, ENV_VAR_GITHUB_APP_READER_PRIVATE_KEY +from oar.core.github_app import GitHubApp from oar.core.jira import JiraIssue from oar.core.ldap import LdapHelper @@ -74,8 +75,44 @@ def __init__(self, jira, dry_run=False): self.jira = jira self.dry_run = dry_run self.ldap = LdapHelper() - github_token = os.environ.get("GITHUB_TOKEN", "") - self.github = Github(auth=Auth.Token(github_token)) if github_token else None + self._github_app = self._init_github_app() + + def _init_github_app(self) -> GitHubApp | None: + """ + Create GitHub App Reader client from environment variables. + + Returns: + GitHubApp instance, or None if credentials are missing or initialization fails. + """ + app_id = os.environ.get(ENV_VAR_GITHUB_APP_READER_ID) + private_key = os.environ.get(ENV_VAR_GITHUB_APP_READER_PRIVATE_KEY) + if not app_id or not private_key: + return None + try: + return GitHubApp(app_id, private_key) + except Exception: + logger.exception("Failed to initialize GitHub App Reader") + return None + + def _github_client_for_repo(self, org: str, repo: str): + """ + Return a PyGithub client for a linked PR repository. + + Args: + org: GitHub org (e.g. ``openshift``). + repo: Repository name from the PR URL. + + Returns: + Installation-scoped ``Github`` client, or None if the app is unavailable + or not installed on the repository. + """ + if not self._github_app: + return None + try: + return self._github_app.client_for_repo(org, repo) + except Exception as e: + logger.warning(f"Failed to get GitHub client for {org}/{repo}: {e}") + return None def get_user_email(self, user: User) -> Optional[str]: """ @@ -688,8 +725,11 @@ def is_pre_merge_verified(self, issue: Issue) -> bool: Returns: bool: True if all valid PRs are pre-merge verified, False otherwise. """ - if not self.github: - logger.warning("GITHUB_TOKEN not set, skipping pre-merge verification check.") + if not self._github_app: + logger.warning( + f"{ENV_VAR_GITHUB_APP_READER_ID} or {ENV_VAR_GITHUB_APP_READER_PRIVATE_KEY} " + "not set, skipping pre-merge verification check." + ) return False try: @@ -710,7 +750,10 @@ def is_pre_merge_verified(self, issue: Issue) -> bool: for url, org, repo, pr_number in valid_prs: try: - pr = self.github.get_repo(f"{org}/{repo}").get_pull(pr_number) + github = self._github_client_for_repo(org, repo) + if not github: + return False + pr = github.get_repo(f"{org}/{repo}").get_pull(pr_number) labels = {label.name for label in pr.get_labels()} if "verified-later" in labels: logger.info(f"Issue {issue.key} PR {url} has 'verified-later' label, post-merge verification needed.") diff --git a/tests/test_jira_notificator.py b/tests/test_jira_notificator.py index 40f8fd58ccfa..50de302737e2 100644 --- a/tests/test_jira_notificator.py +++ b/tests/test_jira_notificator.py @@ -485,10 +485,10 @@ def test_is_pre_merge_verified(self): issue_no_links = self.jira.issue("OCPBUGS-59288") self.assertFalse(self.ns.is_pre_merge_verified(issue_no_links)) - # No GITHUB_TOKEN -> skip check, return False - ns_no_token = NotificationService(self.jira, True) - ns_no_token.github = None - self.assertFalse(ns_no_token.is_pre_merge_verified(issue_pre_merge_verified)) + # No GitHub App Reader credentials -> skip check, return False + ns_no_github = NotificationService(self.jira, True) + ns_no_github._github_app = None + self.assertFalse(ns_no_github.is_pre_merge_verified(issue_pre_merge_verified)) def test_process_on_qa_issues(self): day_ago = datetime.now() - timedelta(hours=24) From 6b5f8644995b9527c4c1ed13bf8183f4791f4ec8 Mon Sep 17 00:00:00 2001 From: Tomas David Date: Fri, 29 May 2026 15:45:30 +0200 Subject: [PATCH 2/3] Fix coderabbit suggestions rh-pre-commit.version: 2.4.0 rh-pre-commit.check-secrets: ENABLED --- oar/notificator/jira_notificator.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/oar/notificator/jira_notificator.py b/oar/notificator/jira_notificator.py index 800f61af8ec0..cad8fe68330d 100644 --- a/oar/notificator/jira_notificator.py +++ b/oar/notificator/jira_notificator.py @@ -31,6 +31,7 @@ class NotificationType(Enum): REPORTER = (24, "Reporter Action Request") def __init__(self, hours: int, label: str): + """Set escalation threshold in weekday hours and notification label.""" self.hours = hours self.label = label @@ -58,6 +59,12 @@ class Notification: text: str def __init__(self, issue, type, text): + """ + Args: + issue: Jira issue for the notification. + type: Notification type. + text: Comment body text. + """ self.issue = issue self.type = type self.text = text @@ -72,6 +79,11 @@ class NotificationService: """ def __init__(self, jira, dry_run=False): + """ + Args: + jira: JIRA API client instance. + dry_run: If True, log notifications without posting Jira comments. + """ self.jira = jira self.dry_run = dry_run self.ldap = LdapHelper() @@ -90,8 +102,11 @@ def _init_github_app(self) -> GitHubApp | None: return None try: return GitHubApp(app_id, private_key) - except Exception: - logger.exception("Failed to initialize GitHub App Reader") + except Exception as e: + logger.error( + "Failed to initialize GitHub App Reader (%s)", + type(e).__name__, + ) return None def _github_client_for_repo(self, org: str, repo: str): @@ -111,7 +126,12 @@ def _github_client_for_repo(self, org: str, repo: str): try: return self._github_app.client_for_repo(org, repo) except Exception as e: - logger.warning(f"Failed to get GitHub client for {org}/{repo}: {e}") + logger.warning( + "Failed to get GitHub client for %s/%s (%s)", + org, + repo, + type(e).__name__, + ) return None def get_user_email(self, user: User) -> Optional[str]: From f6cf95e923d9dc555745790b0ff5455ea786fefb Mon Sep 17 00:00:00 2001 From: Tomas David Date: Fri, 29 May 2026 15:55:08 +0200 Subject: [PATCH 3/3] Fix coderabbit suggestions, part 2 rh-pre-commit.version: 2.4.0 rh-pre-commit.check-secrets: ENABLED --- oar/core/github_app.py | 16 ++++++++++------ oar/notificator/jira_notificator.py | 6 +++--- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/oar/core/github_app.py b/oar/core/github_app.py index fffdb5bbf457..9df9391467a8 100644 --- a/oar/core/github_app.py +++ b/oar/core/github_app.py @@ -8,18 +8,22 @@ class GitHubApp: """PyGithub client via GitHub App installation token.""" - def __init__(self, app_id: str, private_key: str): + def __init__(self, app_id: str, private_key_path: str): """ Initialize GitHub App authentication. Args: app_id: Application ID (not Client ID). - private_key: PEM contents or path to a .pem file. + private_key_path: Path to the App private key ``.pem`` file. """ - key = private_key - key_path = Path(private_key).expanduser() - if key_path.is_file(): - key = key_path.read_text() + if "\n" in private_key_path or "-----BEGIN" in private_key_path: + raise ValueError( + "private_key_path must be a path to a .pem file, not inline key content" + ) + key_file = Path(private_key_path).expanduser() + if not key_file.is_file(): + raise FileNotFoundError("GitHub App private key file not found") + key = key_file.read_text() auth = Auth.AppAuth(app_id, key) self._integration = GithubIntegration(auth=auth) diff --git a/oar/notificator/jira_notificator.py b/oar/notificator/jira_notificator.py index cad8fe68330d..9f623ac6959f 100644 --- a/oar/notificator/jira_notificator.py +++ b/oar/notificator/jira_notificator.py @@ -97,11 +97,11 @@ def _init_github_app(self) -> GitHubApp | None: GitHubApp instance, or None if credentials are missing or initialization fails. """ app_id = os.environ.get(ENV_VAR_GITHUB_APP_READER_ID) - private_key = os.environ.get(ENV_VAR_GITHUB_APP_READER_PRIVATE_KEY) - if not app_id or not private_key: + private_key_path = os.environ.get(ENV_VAR_GITHUB_APP_READER_PRIVATE_KEY) + if not app_id or not private_key_path: return None try: - return GitHubApp(app_id, private_key) + return GitHubApp(app_id, private_key_path) except Exception as e: logger.error( "Failed to initialize GitHub App Reader (%s)",