Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,5 @@ version.py

# AI Agents
.crush

.history/
8 changes: 6 additions & 2 deletions sync_jira_actions/sync_pr.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import os

from github import Github
from github import GithubException
from sync_issue import _create_jira_issue
from sync_issue import _find_jira_issue

Expand All @@ -30,8 +31,11 @@ def _is_collaborator_or_org_member(github, repo, username):
return 'collaborator'
if repo.owner.type == 'Organization':
org = github.get_organization(repo.owner.login)
if org.has_in_members(github.get_user(username)):
return 'organization member'
try:
if org.has_in_members(github.get_user(username)):
return 'organization member'
except GithubException:
print(f'WARNING ⚠️ Could not check org membership for @{username}, treating as external contributor')
return None


Expand Down
9 changes: 7 additions & 2 deletions sync_jira_actions/sync_to_jira.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import os

from github import Github
from github import GithubException
from jira import JIRA
from sync_issue import handle_comment_created
from sync_issue import handle_comment_deleted
Expand Down Expand Up @@ -135,8 +136,12 @@ def main(): # noqa
user_type = 'collaborator'
elif repo.owner.type == 'Organization':
org = github.get_organization(repo.owner.login)
if org.has_in_members(github.get_user(gh_issue['user']['login'])):
user_type = 'organization member'
try:
if org.has_in_members(github.get_user(gh_issue['user']['login'])):
user_type = 'organization member'
except GithubException:
username = gh_issue['user']['login']
print(f'WARNING ⚠️ Could not check org membership for @{username},' ' treating as external contributor')
if user_type:
print(f'Skipping PR sync - author @{gh_issue["user"]["login"]} is a {user_type}')
return
Expand Down
31 changes: 31 additions & 0 deletions tests/test_sync_pr.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,34 @@ def test_sync_remain_prs_skips_collaborators(sync_pr_module, mock_sync_issue, mo
# Verify no JIRA issue was created for collaborator PR
assert mock_create_jira_issue.call_count == 0
assert mock_find_jira_issue.call_count == 0


def test_is_collaborator_or_org_member_handles_bot_accounts(sync_pr_module):
"""Test that _is_collaborator_or_org_member returns None for bot accounts"""
from github import GithubException

mock_github_instance = MagicMock()
mock_repo = MagicMock()
mock_repo.has_in_collaborators.return_value = False
mock_repo.owner.type = 'Organization'
mock_repo.owner.login = 'fake-org'

mock_github_instance.get_user.side_effect = GithubException(404, 'Not Found', None)

result = sync_pr_module._is_collaborator_or_org_member(mock_github_instance, mock_repo, 'copilot[bot]')

assert result is None


def test_sync_remain_prs_handles_bot_accounts(sync_pr_module, mock_sync_issue, mock_github):
"""Test that PRs from bot accounts (e.g. copilot[bot]) don't crash the sync"""
mock_jira = MagicMock()
mock_create_jira_issue, mock_find_jira_issue = mock_sync_issue

mock_github.get_pulls.return_value[0].user.login = 'copilot[bot]'

with patch.object(sync_pr_module, '_is_collaborator_or_org_member', return_value=None):
sync_pr_module.sync_remain_prs(mock_jira)

assert mock_find_jira_issue.call_count == 1
assert mock_create_jira_issue.call_count == 1
40 changes: 40 additions & 0 deletions tests/test_sync_to_jira.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,43 @@ def test_handle_issue_opened_event(mock_environment, sync_to_jira_main, monkeypa
with patch('sync_jira_actions.sync_to_jira.handle_issue_opened') as mock_handle_issue_opened:
sync_to_jira_main()
mock_handle_issue_opened.assert_called_once()


def test_pr_opened_by_bot_account_does_not_crash(mock_environment, monkeypatch):
"""Test that PRs opened by bot accounts (e.g. copilot[bot]) don't crash the sync"""
from github import GithubException

event_data = {
'action': 'opened',
'pull_request': {
'number': 42,
'title': 'Bot PR',
'body': 'Automated PR',
'user': {'login': 'copilot[bot]'},
'html_url': 'https://github.com/espressif/esp-idf/pull/42',
'state': 'open',
'labels': [],
},
}
mock_environment.write_text(json.dumps(event_data))
monkeypatch.setenv('GITHUB_EVENT_NAME', 'pull_request')
monkeypatch.setenv('JIRA_PROJECT', 'TEST_PROJECT')

mock_repo = MagicMock()
mock_repo.has_in_collaborators.return_value = False
mock_repo.owner.type = 'Organization'
mock_repo.owner.login = 'espressif'

mock_github_instance = MagicMock()
mock_github_instance.get_repo.return_value = mock_repo
mock_github_instance.get_user.side_effect = GithubException(404, 'Not Found', None)

with (
patch('sync_jira_actions.sync_to_jira.Github', return_value=mock_github_instance),
patch('sync_jira_actions.sync_to_jira._JIRA'),
patch('sync_jira_actions.sync_to_jira.handle_issue_opened') as mock_handle_issue_opened,
):
from sync_jira_actions.sync_to_jira import main

main()
mock_handle_issue_opened.assert_called_once()
Loading