-
Notifications
You must be signed in to change notification settings - Fork 43
OCPERT-368: Replace GitHub token with Github apps #1060
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
base: main
Are you sure you want to change the base?
Changes from all commits
d6f30bd
0851166
33ea0f2
7d910fb
7812304
0c4de60
4b86867
ad70a35
eafc3f6
d079ad3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| """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_path: str): | ||
| """ | ||
| Initialize GitHub App authentication. | ||
|
|
||
| Args: | ||
| app_id: Application ID (not Client ID). | ||
| private_key_path: Path to the App private key ``.pem`` file. | ||
| """ | ||
| 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) | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| def installation_token(self, owner: str, repo: str) -> str: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is never called. |
||
| """ | ||
| Return a short-lived installation access token for ``owner/repo``. | ||
|
|
||
| Uses ``GithubIntegration.get_access_token`` (standard GitHub App API). | ||
|
|
||
| Args: | ||
| owner: GitHub org or user (e.g. ``openshift``). | ||
| repo: Repository name (e.g. ``release-tests``). | ||
|
|
||
| Returns: | ||
| Installation access token string for REST/GraphQL ``Authorization: Bearer``. | ||
|
|
||
| Raises: | ||
| GithubException: App not installed on the repo or invalid credentials. | ||
| """ | ||
| installation = self._integration.get_repo_installation(owner, repo) | ||
| return self._integration.get_access_token(installation.id).token | ||
|
|
||
| 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) | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -9,8 +9,8 @@ | |||||
| Usage: | ||||||
| from oar.core.release_discovery import ReleaseDiscovery | ||||||
|
|
||||||
| # Initialize with GitHub token | ||||||
| discovery = ReleaseDiscovery() # Uses GITHUB_TOKEN env var | ||||||
| # Initialize with GitHub App Writer credentials | ||||||
| discovery = ReleaseDiscovery() | ||||||
|
|
||||||
| # Get all supported y-streams | ||||||
| y_streams = discovery.get_supported_ystreams() | ||||||
|
|
@@ -32,10 +32,11 @@ | |||||
| from typing import List, Optional | ||||||
|
|
||||||
| import yaml | ||||||
| from github import Auth, Github | ||||||
| from semver import VersionInfo | ||||||
|
|
||||||
| from oar.core.const import ENV_VAR_GITHUB_APP_WRITER_ID, ENV_VAR_GITHUB_APP_WRITER_PRIVATE_KEY | ||||||
| from oar.core.exceptions import ReleaseDiscoveryException | ||||||
| from oar.core.github_app import GitHubApp | ||||||
|
|
||||||
| logger = logging.getLogger(__name__) | ||||||
|
|
||||||
|
|
@@ -53,37 +54,52 @@ class ReleaseDiscovery: | |||||
|
|
||||||
| def __init__( | ||||||
| self, | ||||||
| github_token: Optional[str] = None, | ||||||
| repo_name: Optional[str] = None, | ||||||
| branch: Optional[str] = None | ||||||
| branch: Optional[str] = None, | ||||||
| ): | ||||||
| """ | ||||||
| Initialize ReleaseDiscovery with authenticated GitHub API. | ||||||
|
|
||||||
| Args: | ||||||
| github_token: GitHub personal access token (default: from GITHUB_TOKEN env) | ||||||
| repo_name: GitHub repository name (default: "openshift/release-tests") | ||||||
| branch: Branch name (default: "z-stream") | ||||||
|
|
||||||
| Raises: | ||||||
| ReleaseDiscoveryException: If GitHub token is missing | ||||||
| ReleaseDiscoveryException: If GitHub App Writer credentials are missing | ||||||
| """ | ||||||
| token = github_token or os.environ.get("GITHUB_TOKEN") | ||||||
| if not token: | ||||||
| raise ReleaseDiscoveryException("GitHub token not found. Set GITHUB_TOKEN environment variable.") | ||||||
|
|
||||||
| self.repo_name = repo_name or self.DEFAULT_REPO | ||||||
| self.branch = branch or self.DEFAULT_BRANCH | ||||||
|
|
||||||
| if self.repo_name != self.DEFAULT_REPO: | ||||||
| raise ReleaseDiscoveryException( | ||||||
| f"ReleaseDiscovery only supports {self.DEFAULT_REPO}, got {self.repo_name}" | ||||||
| ) | ||||||
|
Comment on lines
+73
to
+76
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With raising this exception, it doesn't make sense to keep optional |
||||||
|
|
||||||
| # Split repo_name into owner and repository for GraphQL queries | ||||||
| self.git_repo_owner, self.git_repo_name = self.repo_name.split('/', 1) | ||||||
|
|
||||||
| auth = Auth.Token(token) | ||||||
| self._github = Github(auth=auth) | ||||||
| self._github = self._init_github_client() | ||||||
|
|
||||||
| # Tracking files data (fetched via GraphQL) | ||||||
| self._tracking_data: Optional[dict] = None | ||||||
|
|
||||||
| def _init_github_client(self): | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider adding return type here for better readability:
Suggested change
Requires Github import, so not sure if it's worth it.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider adding also docstring similar to |
||||||
| app_id = os.environ.get(ENV_VAR_GITHUB_APP_WRITER_ID) | ||||||
| private_key_path = os.environ.get(ENV_VAR_GITHUB_APP_WRITER_PRIVATE_KEY) | ||||||
| if not app_id or not private_key_path: | ||||||
| raise ReleaseDiscoveryException( | ||||||
| f"{ENV_VAR_GITHUB_APP_WRITER_ID} and " | ||||||
| f"{ENV_VAR_GITHUB_APP_WRITER_PRIVATE_KEY} must be set." | ||||||
| ) | ||||||
| try: | ||||||
| return GitHubApp(app_id, private_key_path).client_for_repo( | ||||||
| self.git_repo_owner, self.git_repo_name | ||||||
| ) | ||||||
| except Exception as e: | ||||||
| raise ReleaseDiscoveryException( | ||||||
| f"Failed to initialize GitHub App Writer ({type(e).__name__})" | ||||||
| ) from e | ||||||
|
|
||||||
| def get_supported_ystreams(self) -> List[str]: | ||||||
| """ | ||||||
| Get all supported y-streams by discovering directories in _releases/. | ||||||
|
|
||||||
Uh oh!
There was an error while loading. Please reload this page.