fix(backend): repository search is not working in the production environment (#11386)

This commit is contained in:
Hiep Le
2025-10-15 23:24:27 +07:00
committed by GitHub
parent 72179f45d3
commit 58d67a2480
8 changed files with 86 additions and 14 deletions

View File

@@ -13,7 +13,13 @@ class BitBucketReposMixin(BitBucketMixinBase):
"""
async def search_repositories(
self, query: str, per_page: int, sort: str, order: str, public: bool
self,
query: str,
per_page: int,
sort: str,
order: str,
public: bool,
app_mode: AppMode,
) -> list[Repository]:
"""Search for repositories."""
repositories = []

View File

@@ -161,6 +161,29 @@ class GitHubReposMixin(GitHubMixinBase):
logger.warning(f'Failed to get user organizations: {e}')
return []
async def get_organizations_from_installations(self) -> list[str]:
"""Get list of organization logins from GitHub App installations.
This method provides a more reliable way to get organizations that the
GitHub App has access to, regardless of user membership context.
"""
try:
# Get installations with account details
url = f'{self.BASE_URL}/user/installations'
response, _ = await self._make_request(url)
installations = response.get('installations', [])
orgs = []
for installation in installations:
account = installation.get('account', {})
if account.get('type') == 'Organization':
orgs.append(account.get('login'))
return orgs
except Exception as e:
logger.warning(f'Failed to get organizations from installations: {e}')
return []
def _fuzzy_match_org_name(self, query: str, org_name: str) -> bool:
"""Check if query fuzzy matches organization name."""
query_lower = query.lower().replace('-', '').replace('_', '').replace(' ', '')
@@ -181,7 +204,13 @@ class GitHubReposMixin(GitHubMixinBase):
return False
async def search_repositories(
self, query: str, per_page: int, sort: str, order: str, public: bool
self,
query: str,
per_page: int,
sort: str,
order: str,
public: bool,
app_mode: AppMode,
) -> list[Repository]:
url = f'{self.BASE_URL}/search/repositories'
params = {
@@ -206,9 +235,12 @@ class GitHubReposMixin(GitHubMixinBase):
query_with_user = f'org:{org} in:name {repo_query}'
params['q'] = query_with_user
elif not public:
# Expand search scope to include user's repositories and organizations they're a member of
# Expand search scope to include user's repositories and organizations the app has access to
user = await self.get_user()
user_orgs = await self.get_user_organizations()
if app_mode == AppMode.SAAS:
user_orgs = await self.get_organizations_from_installations()
else:
user_orgs = await self.get_user_organizations()
# Search in user repos and org repos separately
all_repos = []

View File

@@ -75,6 +75,7 @@ class GitLabReposMixin(GitLabMixinBase):
sort: str = 'updated',
order: str = 'desc',
public: bool = False,
app_mode: AppMode = AppMode.OSS,
) -> list[Repository]:
if public:
# When public=True, query is a GitLab URL that we need to parse

View File

@@ -294,12 +294,13 @@ class ProviderHandler:
per_page: int,
sort: str,
order: str,
app_mode: AppMode,
) -> list[Repository]:
if selected_provider:
service = self.get_service(selected_provider)
public = self._is_repository_url(query, selected_provider)
user_repos = await service.search_repositories(
query, per_page, sort, order, public
query, per_page, sort, order, public, app_mode
)
return self._deduplicate_repositories(user_repos)
@@ -309,7 +310,7 @@ class ProviderHandler:
service = self.get_service(provider)
public = self._is_repository_url(query, provider)
service_repos = await service.search_repositories(
query, per_page, sort, order, public
query, per_page, sort, order, public, app_mode
)
all_repos.extend(service_repos)
except Exception as e:

View File

@@ -458,7 +458,13 @@ class GitService(Protocol):
...
async def search_repositories(
self, query: str, per_page: int, sort: str, order: str, public: bool
self,
query: str,
per_page: int,
sort: str,
order: str,
public: bool,
app_mode: AppMode,
) -> list[Repository]:
"""Search for public repositories"""
...

View File

@@ -148,7 +148,7 @@ async def search_repositories(
)
try:
repos: list[Repository] = await client.search_repositories(
selected_provider, query, per_page, sort, order
selected_provider, query, per_page, sort, order, server_config.app_mode
)
return repos

View File

@@ -8,6 +8,7 @@ from pydantic import SecretStr
from openhands.integrations.bitbucket.bitbucket_service import BitBucketService
from openhands.integrations.service_types import OwnerType, Repository
from openhands.integrations.service_types import ProviderType as ServiceProviderType
from openhands.server.types import AppMode
@pytest.fixture
@@ -37,7 +38,12 @@ async def test_search_repositories_url_parsing_standard_url(bitbucket_service):
) as mock_get_repo:
url = 'https://bitbucket.org/workspace/repo'
repositories = await bitbucket_service.search_repositories(
query=url, per_page=10, sort='updated', order='desc', public=True
query=url,
per_page=10,
sort='updated',
order='desc',
public=True,
app_mode=AppMode.OSS,
)
# Verify the correct workspace/repo combination was extracted and passed
@@ -70,7 +76,12 @@ async def test_search_repositories_url_parsing_with_extra_path_segments(
# Test complex URL with query params, fragments, and extra paths
url = 'https://bitbucket.org/my-workspace/my-repo/src/feature-branch/src/main.py?at=feature-branch&fileviewer=file-view-default#lines-25'
repositories = await bitbucket_service.search_repositories(
query=url, per_page=10, sort='updated', order='desc', public=True
query=url,
per_page=10,
sort='updated',
order='desc',
public=True,
app_mode=AppMode.OSS,
)
# Verify the correct workspace/repo combination was extracted from complex URL
@@ -87,7 +98,12 @@ async def test_search_repositories_url_parsing_invalid_url(bitbucket_service):
) as mock_get_repo:
url = 'not-a-valid-url'
repositories = await bitbucket_service.search_repositories(
query=url, per_page=10, sort='updated', order='desc', public=True
query=url,
per_page=10,
sort='updated',
order='desc',
public=True,
app_mode=AppMode.OSS,
)
# Should return empty list for invalid URL and not call API
@@ -105,7 +121,12 @@ async def test_search_repositories_url_parsing_insufficient_path_segments(
) as mock_get_repo:
url = 'https://bitbucket.org/workspace'
repositories = await bitbucket_service.search_repositories(
query=url, per_page=10, sort='updated', order='desc', public=True
query=url,
per_page=10,
sort='updated',
order='desc',
public=True,
app_mode=AppMode.OSS,
)
# Should return empty list for insufficient path segments and not call API

View File

@@ -277,7 +277,7 @@ async def test_github_search_repositories_with_organizations():
patch.object(service, 'get_user', return_value=mock_user),
patch.object(
service,
'get_user_organizations',
'get_organizations_from_installations',
return_value=['All-Hands-AI', 'example-org'],
),
patch.object(
@@ -285,7 +285,12 @@ async def test_github_search_repositories_with_organizations():
) as mock_request,
):
repositories = await service.search_repositories(
query='openhands', per_page=10, sort='stars', order='desc', public=False
query='openhands',
per_page=10,
sort='stars',
order='desc',
public=False,
app_mode=AppMode.SAAS,
)
# Verify that separate requests were made for user and each organization