Skip to content

chore(github): Add business+ plan check for multi org #91905

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

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 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 src/sentry/features/permanent.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ def register_permanent_features(manager: FeatureManager):
"organizations:seer-based-priority": False,
# Enable Vercel integration - there is a custom handler in getsentry
"organizations:integrations-vercel": True,
# Enable GitHub multi-org for users to connect many Sentry orgs to a single GitHub org.
"organizations:integrations-scm-multi-org": True,
# Enable issue view endpoints and UI
"organizations:issue-views": False,
}
Expand Down
16 changes: 14 additions & 2 deletions src/sentry/integrations/github/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -887,6 +887,15 @@ def _get_eligible_multi_org_installations(
class GithubOrganizationSelection(PipelineView):
def dispatch(self, request: HttpRequest, pipeline: Pipeline) -> HttpResponseBase:
self.active_user_organization = determine_active_organization(request)
has_business_plan = (
features.has(
"organizations:integrations-scm-multi-org",
organization=self.active_user_organization.organization,
actor=request.user,
Copy link
Member

Choose a reason for hiding this comment

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

qq: whats the purpose of passing user here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

🤔 I probably yoinked this from a previous feature flag checker, but looking further yeah we dont need to be passing the user/it wont be used.

)
if self.active_user_organization is not None
else False
)

if self.active_user_organization is None or not features.has(
"organizations:github-multi-org",
Expand All @@ -912,7 +921,7 @@ def dispatch(self, request: HttpRequest, pipeline: Pipeline) -> HttpResponseBase
)

if chosen_installation_id := request.GET.get("chosen_installation_id"):
if chosen_installation_id == "-1":
if chosen_installation_id == "-1" or not has_business_plan:
return pipeline.next_step()

# Verify that the given GH installation belongs to the person installing the pipeline
Expand All @@ -936,7 +945,10 @@ def dispatch(self, request: HttpRequest, pipeline: Pipeline) -> HttpResponseBase
return self.render_react_view(
request=request,
pipeline_name="githubInstallationSelect",
props={"installation_info": installation_info},
props={
"installation_info": installation_info,
Copy link
Member

Choose a reason for hiding this comment

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

if they don't have multi org, should we still be passing the full list of installations to the FE?

Copy link
Member

Choose a reason for hiding this comment

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

i think we should also error if somebody tries to pass a chosen installation id that's not -1 and they don't have multi org

Copy link
Contributor Author

@Christinarlong Christinarlong May 22, 2025

Choose a reason for hiding this comment

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

For the passing the full list comment, I think it's useful to keep in case we want to do the per gh org upsell in the dropdown in the UI.

"has_business_plan": has_business_plan,
},
)


Expand Down
1 change: 1 addition & 0 deletions tests/sentry/api/serializers/test_organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ def test_simple(self):
"integrations-incident-management",
"integrations-issue-basic",
"integrations-issue-sync",
"integrations-scm-multi-org",
"integrations-stacktrace-link",
"integrations-ticket-rules",
"integrations-vercel",
Expand Down
59 changes: 58 additions & 1 deletion tests/sentry/integrations/github/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -1272,6 +1272,7 @@ def test_get_account_id_backfill_missing(self):
integration = Integration.objects.get(id=integration_id)
assert integration.metadata["account_id"] == 60591805

@with_feature("organizations:integrations-scm-multi-org")
@with_feature("organizations:github-multi-org")
@responses.activate
@patch("sentry.integrations.utils.metrics.EventLifecycle.record_event")
Expand Down Expand Up @@ -1315,12 +1316,13 @@ def test_github_installation_calls_ui(self, mock_render, mock_record):
mock_render.assert_called_with(
request=ANY,
pipeline_name="githubInstallationSelect",
props={"installation_info": installations},
props={"installation_info": installations, "has_business_plan": True},
)

# SLO assertions
assert_success_metric(mock_record)

@with_feature("organizations:integrations-scm-multi-org")
@with_feature("organizations:github-multi-org")
@responses.activate
@patch("sentry.integrations.utils.metrics.EventLifecycle.record_event")
Expand Down Expand Up @@ -1405,6 +1407,7 @@ def test_github_installation_stores_chosen_installation(self, mock_record):
# SLO assertions
assert_success_metric(mock_record)

@with_feature("organizations:integrations-scm-multi-org")
@with_feature("organizations:github-multi-org")
@responses.activate
@patch("sentry.integrations.utils.metrics.EventLifecycle.record_event")
Expand Down Expand Up @@ -1476,6 +1479,58 @@ def test_github_installation_fails_on_invalid_installation(self, mock_record):

assert_failure_metric(mock_record, GitHubInstallationError.INVALID_INSTALLATION)

@with_feature(
{"organizations:github-multi-org": True, "organizations:integrations-scm-multi-org": False}
)
@responses.activate
@patch("sentry.integrations.utils.metrics.EventLifecycle.record_event")
@patch.object(PipelineView, "render_react_view", return_value=HttpResponse())
def test_github_installation_calls_ui_no_biz_plan(self, mock_render, mock_record):
self._setup_with_existing_installations()
installations = [
{
"installation_id": "1",
"github_account": "santry",
"avatar_url": "https://github.com/knobiknows/all-the-bufo/raw/main/all-the-bufo/bufo-pitchforks.png",
},
{
"installation_id": "2",
"github_account": "bufo-bot",
"avatar_url": "https://github.com/knobiknows/all-the-bufo/raw/main/all-the-bufo/bufo-pog.png",
},
{
"installation_id": "-1",
"github_account": "Integrate with a new GitHub organization",
"avatar_url": "",
},
]

resp = self.client.get(self.init_path)
assert resp.status_code == 302
redirect = urlparse(resp["Location"])
assert redirect.scheme == "https"
assert redirect.netloc == "github.com"
assert redirect.path == "/login/oauth/authorize"
assert (
redirect.query
== f"client_id=github-client-id&state={self.pipeline.signature}&redirect_uri=http://testserver/extensions/github/setup/"
)
resp = self.client.get(
"{}?{}".format(
self.setup_path,
urlencode({"code": "12345678901234567890", "state": self.pipeline.signature}),
)
)
mock_render.assert_called_with(
request=ANY,
pipeline_name="githubInstallationSelect",
props={"installation_info": installations, "has_business_plan": False},
)

# SLO assertions
assert_success_metric(mock_record)

@with_feature("organizations:integrations-scm-multi-org")
@with_feature("organizations:github-multi-org")
@responses.activate
@patch("sentry.integrations.utils.metrics.EventLifecycle.record_event")
Expand Down Expand Up @@ -1543,6 +1598,7 @@ def test_github_installation_skips_chosen_installation(self, mock_record):
# SLO assertions
assert_success_metric(mock_record)

@with_feature("organizations:integrations-scm-multi-org")
@with_feature("organizations:github-multi-org")
@responses.activate
def test_github_installation_gets_owner_orgs(self):
Expand All @@ -1554,6 +1610,7 @@ def test_github_installation_gets_owner_orgs(self):

assert owner_orgs == ["santry"]

@with_feature("organizations:integrations-scm-multi-org")
@with_feature("organizations:github-multi-org")
@responses.activate
def test_github_installation_filters_valid_installations(self):
Expand Down
Loading