Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
328dd80
fix: add github multi org
emekanwaoma Oct 10, 2025
8ef479f
Merge branch 'main' of https://github.com/port-labs/ocean into mulito…
emekanwaoma Oct 10, 2025
7d9983d
fix: bump integration
emekanwaoma Oct 10, 2025
b0ff23b
fix: update github organization
emekanwaoma Oct 10, 2025
ea914f4
fix: add default mapping
emekanwaoma Oct 13, 2025
907642e
fix: update changelog
emekanwaoma Oct 13, 2025
7439309
Merge branch 'main' of https://github.com/port-labs/ocean into mulito…
emekanwaoma Oct 14, 2025
948b019
fix: support organizations and multiorgs
emekanwaoma Oct 14, 2025
3f4e313
fix: update changelog and add logs
emekanwaoma Oct 14, 2025
f157523
fix: optimize organization exporter
emekanwaoma Oct 14, 2025
9543234
fix: fix tests
emekanwaoma Oct 14, 2025
b4e038a
Merge branch 'main' of https://github.com/port-labs/ocean into mulito…
emekanwaoma Oct 15, 2025
dd57e9e
Merge branch 'main' of https://github.com/port-labs/ocean into mulito…
emekanwaoma Oct 16, 2025
995e3ec
fix: remove breaking changes
emekanwaoma Oct 16, 2025
413c195
Take organization from the port app config
mk-armah Oct 17, 2025
e16bef0
fix: update github
emekanwaoma Oct 20, 2025
3d55f9b
Merge branch 'main' of https://github.com/port-labs/ocean into mulito…
emekanwaoma Oct 20, 2025
0066b2b
fix: update spec yaml
emekanwaoma Oct 20, 2025
af3e58f
fix: update changelog
emekanwaoma Oct 20, 2025
2286d44
fix: remove run time checkable
emekanwaoma Oct 20, 2025
342f938
Merge branch 'main' of https://github.com/port-labs/ocean into mulito…
emekanwaoma Oct 20, 2025
c009014
Merge branch 'main' of https://github.com/port-labs/ocean into mulito…
emekanwaoma Oct 20, 2025
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
5 changes: 3 additions & 2 deletions integrations/github/.port/spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ features:
- type: exporter
section: Git
resources:
- kind: organization
- kind: repository
- kind: folder
- kind: user
Expand Down Expand Up @@ -44,9 +45,9 @@ configurations:
sensitive: true
description: If App ID is passed, then app private key is required.
- name: githubOrganization
required: true
required: false
type: string
description: GitHub organization name
description: GitHub organization name (optional - if not provided, will sync all organizations the personal access token user is a member of)
- name: webhookSecret
type: string
description: Optional secret used to verify incoming webhook requests. Ensures that only legitimate events from GitHub are accepted.
Expand Down
11 changes: 11 additions & 0 deletions integrations/github/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

<!-- towncrier release notes start -->

## 1.5.11-beta (2025-10-10)


### Features

- Added multi-organization support for GitHub integration
- Updated all resync functions to iterate through multiple organizations
- Modified webhook processors to include organization context
- Updated exporters to support organization parameters


## 1.5.10-beta (2025-09-30)


Expand Down
2 changes: 0 additions & 2 deletions integrations/github/github/clients/http/base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,10 @@
class AbstractGithubClient(ABC):
def __init__(
self,
organization: str,
github_host: str,
authenticator: "AbstractGitHubAuthenticator",
**kwargs: Any,
) -> None:
self.organization = organization
self.github_host = github_host
self.authenticator = authenticator
self.kwargs = kwargs
Expand Down
1 change: 0 additions & 1 deletion integrations/github/github/clients/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,5 @@
def integration_config(authenticator: AbstractGitHubAuthenticator) -> Dict[str, Any]:
return {
"authenticator": authenticator,
"organization": ocean.integration_config["github_organization"],
"github_host": ocean.integration_config["github_host"],
}
43 changes: 27 additions & 16 deletions integrations/github/github/core/exporters/branch_exporter.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,41 @@
import asyncio
from typing import Any
from typing import Any, cast
from github.core.exporters.abstract_exporter import AbstractGithubExporter
from port_ocean.core.ocean_types import ASYNC_GENERATOR_RESYNC_TYPE, RAW_ITEM
from loguru import logger
from github.core.options import ListBranchOptions, SingleBranchOptions
from github.clients.http.rest_client import GithubRestClient
from github.helpers.utils import enrich_with_repository, extract_repo_params
from github.helpers.utils import enrich_with_repository, parse_github_options


class RestBranchExporter(AbstractGithubExporter[GithubRestClient]):

async def fetch_branch(self, repo_name: str, branch_name: str) -> RAW_ITEM:
endpoint = f"{self.client.base_url}/repos/{self.client.organization}/{repo_name}/branches/{branch_name}"
async def fetch_branch(
self, repo_name: str, branch_name: str, organization: str
) -> RAW_ITEM:
endpoint = f"{self.client.base_url}/repos/{organization}/{repo_name}/branches/{branch_name}"
response = await self.client.send_api_request(endpoint)
return response

async def get_resource[
ExporterOptionsT: SingleBranchOptions
](self, options: ExporterOptionsT) -> RAW_ITEM:

repo_name, params = extract_repo_params(dict(options))
repo_name, organization, params = parse_github_options(dict(options))
branch_name = params["branch_name"]
protection_rules = bool(params["protection_rules"])
repo_name = cast(str, repo_name)

response = await self.fetch_branch(repo_name, branch_name)
response = await self.fetch_branch(repo_name, branch_name, organization)

if protection_rules:
response = await self._enrich_branch_with_protection_rules(
repo_name, response
repo_name, response, organization
)

logger.info(f"Fetched branch: {branch_name} for repo: {repo_name}")
logger.info(
f"Fetched branch: {branch_name} for repo: {repo_name} from {organization}"
)

return enrich_with_repository(response, repo_name)

Expand All @@ -39,19 +44,22 @@ async def get_paginated_resources[
](self, options: ExporterOptionsT) -> ASYNC_GENERATOR_RESYNC_TYPE:
"""Get all branches in the repository with pagination."""

repo_name, params = extract_repo_params(dict(options))
repo_name, organization, params = parse_github_options(dict(options))
detailed = bool(params.pop("detailed"))
protection_rules = bool(params.pop("protection_rules"))
repo_name = cast(str, repo_name)

async for branches in self.client.send_paginated_request(
f"{self.client.base_url}/repos/{self.client.organization}/{repo_name}/branches",
f"{self.client.base_url}/repos/{organization}/{repo_name}/branches",
params,
):
logger.info(
f"Fetched batch of {len(branches)} branches from repository {repo_name}"
f"Fetched batch of {len(branches)} branches from repository {repo_name} from {organization}"
)
tasks = [
self._hydrate_branch(repo_name, b, detailed, protection_rules)
self._hydrate_branch(
repo_name, organization, b, detailed, protection_rules
)
for b in branches
]
hydrated = await asyncio.gather(*tasks)
Expand All @@ -63,32 +71,35 @@ async def get_paginated_resources[
async def _hydrate_branch(
self,
repo_name: str,
organization: str,
branch: dict[str, Any],
detailed: bool,
protection_rules: bool,
) -> dict[str, Any]:
branch_name = branch["name"]

if detailed:
branch = await self.fetch_branch(repo_name, branch_name)
branch = await self.fetch_branch(repo_name, branch_name, organization)
logger.debug(
f"Added extra details for branch '{branch_name}' in repo '{repo_name}'."
)

if protection_rules:
branch = await self._enrich_branch_with_protection_rules(repo_name, branch)
branch = await self._enrich_branch_with_protection_rules(
repo_name, branch, organization
)

return enrich_with_repository(branch, repo_name)

async def _enrich_branch_with_protection_rules(
self, repo_name: str, branch: dict[str, Any]
self, repo_name: str, branch: dict[str, Any], organization: str
) -> RAW_ITEM:
"""Return protection rules or None (404/403 ignored by client)."""
branch_name = branch["name"]

endpoint = (
f"{self.client.base_url}/repos/"
f"{self.client.organization}/{repo_name}/branches/{branch_name}/protection"
f"{organization}/{repo_name}/branches/{branch_name}/protection"
)

protection_rules = await self.client.send_api_request(endpoint)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import cast
from github.core.exporters.abstract_exporter import AbstractGithubExporter
from github.helpers.utils import enrich_with_repository, extract_repo_params
from github.helpers.utils import enrich_with_repository, parse_github_options
from port_ocean.core.ocean_types import ASYNC_GENERATOR_RESYNC_TYPE, RAW_ITEM
from loguru import logger
from github.core.options import (
Expand All @@ -15,31 +16,33 @@ async def get_resource[
ExporterOptionsT: SingleCodeScanningAlertOptions
](self, options: ExporterOptionsT) -> RAW_ITEM:

repo_name, params = extract_repo_params(dict(options))
repo_name, organization, params = parse_github_options(dict(options))
alert_number = params["alert_number"]

endpoint = f"{self.client.base_url}/repos/{self.client.organization}/{repo_name}/code-scanning/alerts/{alert_number}"
endpoint = f"{self.client.base_url}/repos/{organization}/{repo_name}/code-scanning/alerts/{alert_number}"
response = await self.client.send_api_request(endpoint)

logger.info(
f"Fetched code scanning alert with number: {alert_number} for repo: {repo_name}"
f"Fetched code scanning alert with number: {alert_number} for repo: {repo_name} from {organization}"
)

return enrich_with_repository(response, repo_name)
return enrich_with_repository(response, cast(str, repo_name))

async def get_paginated_resources[
ExporterOptionsT: ListCodeScanningAlertOptions
](self, options: ExporterOptionsT) -> ASYNC_GENERATOR_RESYNC_TYPE:
"""Get all code scanning alerts in the repository with pagination."""

repo_name, params = extract_repo_params(dict(options))
repo_name, organization, params = parse_github_options(dict(options))

async for alerts in self.client.send_paginated_request(
f"{self.client.base_url}/repos/{self.client.organization}/{repo_name}/code-scanning/alerts",
f"{self.client.base_url}/repos/{organization}/{repo_name}/code-scanning/alerts",
params,
):
logger.info(
f"Fetched batch of {len(alerts)} code scanning alerts from repository {repo_name}"
f"Fetched batch of {len(alerts)} code scanning alerts from repository {repo_name} from {organization}"
)
batch_data = [enrich_with_repository(alert, repo_name) for alert in alerts]
batch_data = [
enrich_with_repository(alert, cast(str, repo_name)) for alert in alerts
]
yield batch_data
21 changes: 11 additions & 10 deletions integrations/github/github/core/exporters/collaborator_exporter.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import cast
from github.core.exporters.abstract_exporter import AbstractGithubExporter
from github.helpers.utils import enrich_with_repository, extract_repo_params
from github.helpers.utils import enrich_with_repository, parse_github_options
from port_ocean.core.ocean_types import ASYNC_GENERATOR_RESYNC_TYPE, RAW_ITEM
from loguru import logger
from github.core.options import ListCollaboratorOptions, SingleCollaboratorOptions
Expand All @@ -10,40 +11,40 @@ class RestCollaboratorExporter(AbstractGithubExporter[GithubRestClient]):
async def get_resource[
ExporterOptionsT: SingleCollaboratorOptions
](self, options: ExporterOptionsT) -> RAW_ITEM:
repo_name, params = extract_repo_params(dict(options))
repo_name, organization, params = parse_github_options(dict(options))
username = params["username"]

endpoint = f"{self.client.base_url}/repos/{self.client.organization}/{repo_name}/collaborators/{username}/permission"
endpoint = f"{self.client.base_url}/repos/{organization}/{repo_name}/collaborators/{username}/permission"
response = await self.client.send_api_request(endpoint)
if not response:
logger.warning(
f"No collaborator found with identifier: {username} from repository: {repo_name}"
f"No collaborator found with identifier: {username} from repository: {repo_name} from {organization}"
)
return {}

logger.info(
f"Fetched collaborator with identifier: {username} from repository: {repo_name}"
f"Fetched collaborator with identifier: {username} from repository: {repo_name} from {organization}"
)

collaborator = response["user"]
return enrich_with_repository(collaborator, repo_name)
return enrich_with_repository(collaborator, cast(str, repo_name))

async def get_paginated_resources[
ExporterOptionsT: ListCollaboratorOptions
](self, options: ExporterOptionsT) -> ASYNC_GENERATOR_RESYNC_TYPE:
"""Get all collaborators in the repository with pagination."""

repo_name, params = extract_repo_params(dict(options))
repo_name, organization, params = parse_github_options(dict(options))

async for collaborators in self.client.send_paginated_request(
f"{self.client.base_url}/repos/{self.client.organization}/{repo_name}/collaborators",
f"{self.client.base_url}/repos/{organization}/{repo_name}/collaborators",
params,
):
logger.info(
f"Fetched batch of {len(collaborators)} collaborators from repository {repo_name}"
f"Fetched batch of {len(collaborators)} collaborators from repository {repo_name} from {organization}"
)
batch = [
enrich_with_repository(collaborator, repo_name)
enrich_with_repository(collaborator, cast(str, repo_name))
for collaborator in collaborators
]
yield batch
21 changes: 12 additions & 9 deletions integrations/github/github/core/exporters/dependabot_exporter.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import cast
from github.core.exporters.abstract_exporter import AbstractGithubExporter
from github.helpers.utils import enrich_with_repository, extract_repo_params
from github.helpers.utils import enrich_with_repository, parse_github_options
from port_ocean.core.ocean_types import ASYNC_GENERATOR_RESYNC_TYPE, RAW_ITEM
from loguru import logger
from github.core.options import ListDependabotAlertOptions, SingleDependabotAlertOptions
Expand All @@ -12,32 +13,34 @@ async def get_resource[
ExporterOptionsT: SingleDependabotAlertOptions
](self, options: ExporterOptionsT) -> RAW_ITEM:

repo_name, params = extract_repo_params(dict(options))
repo_name, organization, params = parse_github_options(dict(options))
alert_number = params["alert_number"]

endpoint = f"{self.client.base_url}/repos/{self.client.organization}/{repo_name}/dependabot/alerts/{alert_number}"
endpoint = f"{self.client.base_url}/repos/{organization}/{repo_name}/dependabot/alerts/{alert_number}"
response = await self.client.send_api_request(endpoint)

logger.info(
f"Fetched Dependabot alert with number: {alert_number} for repo: {repo_name}"
f"Fetched Dependabot alert with number: {alert_number} for repo: {repo_name} from {organization}"
)

return enrich_with_repository(response, repo_name)
return enrich_with_repository(response, cast(str, repo_name))

async def get_paginated_resources[
ExporterOptionsT: ListDependabotAlertOptions
](self, options: ExporterOptionsT) -> ASYNC_GENERATOR_RESYNC_TYPE:
"""Get all Dependabot alerts in the repository with pagination."""

repo_name, params = extract_repo_params(dict(options))
repo_name, organization, params = parse_github_options(dict(options))
params["state"] = ",".join(params["state"])

async for alerts in self.client.send_paginated_request(
f"{self.client.base_url}/repos/{self.client.organization}/{repo_name}/dependabot/alerts",
f"{self.client.base_url}/repos/{organization}/{repo_name}/dependabot/alerts",
params,
):
logger.info(
f"Fetched batch of {len(alerts)} Dependabot alerts from repository {repo_name}"
f"Fetched batch of {len(alerts)} Dependabot alerts from repository {repo_name} from {organization}"
)
batch = [enrich_with_repository(alert, repo_name) for alert in alerts]
batch = [
enrich_with_repository(alert, cast(str, repo_name)) for alert in alerts
]
yield batch
21 changes: 11 additions & 10 deletions integrations/github/github/core/exporters/deployment_exporter.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import cast
from github.clients.http.rest_client import GithubRestClient
from github.core.exporters.abstract_exporter import AbstractGithubExporter
from github.helpers.utils import enrich_with_repository, extract_repo_params
from github.helpers.utils import enrich_with_repository, parse_github_options
from port_ocean.core.ocean_types import ASYNC_GENERATOR_RESYNC_TYPE, RAW_ITEM
from loguru import logger
from github.core.options import SingleDeploymentOptions, ListDeploymentsOptions
Expand All @@ -12,34 +13,34 @@ async def get_resource[
ExporterOptionsT: SingleDeploymentOptions
](self, options: ExporterOptionsT) -> RAW_ITEM:
"""Get a single deployment for a repository."""
repo_name, params = extract_repo_params(dict(options))
id = params["id"]
repo_name, organization, params = parse_github_options(dict(options))
deployment_id = params["id"]

endpoint = f"{self.client.base_url}/repos/{self.client.organization}/{repo_name}/deployments/{id}"
endpoint = f"{self.client.base_url}/repos/{organization}/{repo_name}/deployments/{deployment_id}"
response = await self.client.send_api_request(endpoint)

logger.info(
f"Fetched deployment with identifier {id} from repository {repo_name}"
f"Fetched deployment with identifier {deployment_id} from repository {repo_name} from {organization}"
)

return enrich_with_repository(response, repo_name)
return enrich_with_repository(response, cast(str, repo_name))

async def get_paginated_resources[
ExporterOptionsT: ListDeploymentsOptions
](self, options: ExporterOptionsT) -> ASYNC_GENERATOR_RESYNC_TYPE:
"""Get all deployments for a repository with pagination."""

repo_name, params = extract_repo_params(dict(options))
repo_name, organization, params = parse_github_options(dict(options))

async for deployments in self.client.send_paginated_request(
f"{self.client.base_url}/repos/{self.client.organization}/{repo_name}/deployments",
f"{self.client.base_url}/repos/{organization}/{repo_name}/deployments",
params,
):
logger.info(
f"Fetched batch of {len(deployments)} deployments from repository {repo_name}"
f"Fetched batch of {len(deployments)} deployments from repository {repo_name} from {organization}"
)
batch = [
enrich_with_repository(deployment, repo_name)
enrich_with_repository(deployment, cast(str, repo_name))
for deployment in deployments
]
yield batch
Loading