Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5f10e62
feat(github): Add support for fetching issue types from GitHub organi…
Asynchronite Mar 24, 2026
e7d86bd
Merge branch 'master' into feat/github-types-addition
Asynchronite Mar 24, 2026
f6c461d
feat(github): update issue type fetching and improve sorting logic
Asynchronite Mar 24, 2026
0079f5c
ref(codeowners): remove generateDashboardFromSeerModal from coverage …
Asynchronite Mar 24, 2026
524bbea
Merge branch 'master' into feat/github-types-addition
Asynchronite Mar 24, 2026
a4d1642
fix(github): remove unnecessary parameter from natural_sort_pair method
Asynchronite Mar 24, 2026
e975279
fix(github): correct indentation in natural_sort_pair method
Asynchronite Mar 24, 2026
7a73f6e
feat(github): add support for issue type in create_issue method
Asynchronite Mar 24, 2026
4d34013
fix(github): handle IntegrationResourceNotFoundError when fetching or…
Asynchronite Mar 24, 2026
612e34b
fix(github): handle multiple exceptions in get_org_types method
Asynchronite Mar 24, 2026
ffc3773
Revert "fix(github): handle multiple exceptions in get_org_types method"
Asynchronite Mar 24, 2026
9bd2a32
Merge branch 'master' into feat/github-types-addition
Asynchronite Mar 24, 2026
ce9b55c
Merge branch 'master' into feat/github-types-addition
Asynchronite Mar 24, 2026
b5d1321
style(github): Format sorting code for labels and types for better re…
Asynchronite Mar 24, 2026
f77a54b
feat(tests): Mock organization issue types endpoint in GitHubIssueBas…
Asynchronite Mar 24, 2026
f627022
feat(tests): Add mock for organization issue types endpoint in GitHub…
Asynchronite Mar 24, 2026
07ac07d
feat(tests): Add mock for GitHub issue types endpoint in GitHubIssueB…
Asynchronite Mar 24, 2026
11ce58d
fix(tests): Simplify unpacking of issue config fields in GitHubIssueB…
Asynchronite Mar 24, 2026
b196a68
refactor(tests): Comment out organization issue types endpoint mock i…
Asynchronite Mar 24, 2026
e7e136f
Revert "refactor(tests): Comment out organization issue types endpoin…
Asynchronite Mar 24, 2026
f4dd3e1
Merge branch 'getsentry:master' into feat/github-types-addition
Asynchronite Mar 25, 2026
f958f03
refactor(github): Rename issue type fetching methods for clarity
Asynchronite Mar 28, 2026
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
7 changes: 7 additions & 0 deletions src/sentry/integrations/github/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -920,6 +920,13 @@ def get_labels(self, owner: str, repo: str) -> list[Any]:
"""
return self._get_with_pagination(f"/repos/{owner}/{repo}/labels")

def get_org_issue_types(self, owner: str) -> list[Any]:
"""
Fetches all issue types for an organization.
https://docs.github.com/en/rest/orgs/issue-types#list-issue-types-for-an-organization
"""
return self._get_with_pagination(f"/orgs/{owner}/issue-types")

def check_file(self, repo: Repository, path: str, version: str | None) -> object | None:
return self.head_cached(path=f"/repos/{repo.name}/contents/{path}", params={"ref": version})

Expand Down
49 changes: 42 additions & 7 deletions src/sentry/integrations/github/issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,14 @@ def get_create_issue_config(

assignees = self.get_allowed_assignees(default_repo) if default_repo else []
labels: Sequence[tuple[str, str]] = []
issue_types: Sequence[tuple[str, str]] = []
if default_repo:
owner, repo = default_repo.split("/")
labels = self.get_repo_labels(owner, repo)
try:
issue_types = self.get_org_issue_types(owner)
except IntegrationResourceNotFoundError:
issue_types = []
Comment on lines +188 to +190
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The exception handling for get_org_issue_types is too narrow, only catching IntegrationResourceNotFoundError. Other API errors, like 403s, will cause uncaught exceptions and crash callers.
Severity: HIGH

Suggested Fix

Broaden the exception handler to catch IntegrationError in addition to or instead of IntegrationResourceNotFoundError. This will gracefully handle other expected API errors like 403 Forbidden (IntegrationConfigurationError) and fall back to an empty list as intended.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: src/sentry/integrations/github/issues.py#L188-L190

Potential issue: The call to `get_org_issue_types` is wrapped in a `try...except` block
that only catches `IntegrationResourceNotFoundError`. However, the underlying API can
return other errors, such as a 403 Forbidden if the feature is not enabled for an
organization, which raises an `IntegrationConfigurationError`. This uncaught exception
will propagate. When this function is called from `IntegrationConfigSerializer`, which
has no exception handling, the serializer will crash. This breaks the ticket rules
configuration modal for GitHub integrations where the issue types feature is unavailable
for reasons other than a 404.

Did we get this right? 👍 / 👎 to inform future reviews.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-404 errors from issue types API crash form

Medium Severity

The try/except around get_org_issue_types only catches IntegrationResourceNotFoundError (HTTP 404). If the GitHub API returns a 403 (e.g., the GitHub App lacks org-level permissions for the newer issue types API) or any other non-404 error, raise_error converts it to IntegrationConfigurationError or IntegrationError, which propagates uncaught and prevents the entire issue creation form from loading. Since issue types is an optional, non-required field and a relatively new GitHub feature (March 2025), this new API call can break a previously-working form for existing installations. Broadening the exception handler to catch a wider set of exceptions (or catching Exception and falling back to []) would make this more resilient.

Additional Locations (1)
Fix in Cursor Fix in Web


autocomplete_url = reverse(
"sentry-integration-github-search", args=[org.slug, self.model.id]
Expand Down Expand Up @@ -217,6 +222,15 @@ def get_create_issue_config(
"required": False,
"choices": labels,
},
{
"name": "type",
"label": "Type",
"default": "",
"type": "select",
"multiple": False,
"required": False,
"choices": issue_types,
},
]

def create_issue(self, data: Mapping[str, Any], **kwargs: Any) -> Mapping[str, Any]:
Expand Down Expand Up @@ -253,6 +267,8 @@ def create_issue(self, data: Mapping[str, Any], **kwargs: Any) -> Mapping[str, A
issue_data["assignee"] = data["assignee"]
if data.get("labels"):
issue_data["labels"] = data["labels"]
if data.get("type"):
issue_data["type"] = data["type"]

try:
issue = client.create_issue(repo=repo, data=issue_data)
Expand Down Expand Up @@ -373,15 +389,34 @@ def get_repo_labels(self, owner: str, repo: str) -> Sequence[tuple[str, str]]:
except Exception as e:
self.raise_error(e)

def natural_sort_pair(pair: tuple[str, str]) -> list[str | int]:
return [
int(text) if text.isdecimal() else text.lower()
for text in re.split("([0-9]+)", pair[0])
]

# sort alphabetically
labels = tuple(
sorted([(label["name"], label["name"]) for label in response], key=natural_sort_pair)
sorted(
[(label["name"], label["name"]) for label in response], key=self.natural_sort_pair
)
)

return labels

def get_org_issue_types(self, owner: str) -> Sequence[tuple[str, str]]:
client = self.get_client()
try:
response = client.get_org_issue_types(owner)
except Exception as e:
self.raise_error(e)

# sort alphabetically
types = tuple(
sorted(
[(type_obj["name"], type_obj["name"]) for type_obj in response],
key=self.natural_sort_pair,
)
)

return types

def natural_sort_pair(self, pair: tuple[str, str]) -> list[str | int]:
return [
int(text) if text.isdecimal() else text.lower()
for text in re.split("([0-9]+)", pair[0])
]
18 changes: 17 additions & 1 deletion tests/sentry/integrations/github/test_issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,15 @@ def test_get_create_issue_config_without_group(self) -> None:
],
)

responses.add(
responses.GET,
"https://api.github.com/orgs/getsentry/issue-types",
json=[{"name": "bug"}, {"name": "task"}],
)

install = self.install
config = install.get_create_issue_config(None, self.user, params={})
[repo_field, assignee_field, label_field] = config
repo_field, assignee_field, label_field = config[:3]
assert repo_field["name"] == "repo"
assert repo_field["type"] == "select"
assert repo_field["label"] == "GitHub Repository"
Expand Down Expand Up @@ -635,6 +641,11 @@ def test_repo_dropdown_choices(self) -> None:
"https://api.github.com/repos/getsentry/sentry/labels",
json=[{"name": "bug"}, {"name": "enhancement"}],
)
responses.add(
responses.GET,
"https://api.github.com/orgs/getsentry/issue-types",
json=[{"name": "bug"}, {"name": "task"}],
)

responses.add(
responses.GET,
Expand Down Expand Up @@ -787,6 +798,11 @@ def test_default_repo_create_fields(self) -> None:
"https://api.github.com/repos/getsentry/sentry/labels",
json=[{"name": "bug"}, {"name": "enhancement"}],
)
responses.add(
responses.GET,
"https://api.github.com/orgs/getsentry/issue-types",
json=[{"name": "bug"}, {"name": "task"}],
)
event = self.store_event(
data={"event_id": "a" * 32, "timestamp": self.min_ago}, project_id=self.project.id
)
Expand Down
Loading