Skip to content

Commit b33f134

Browse files
committed
generalize to work with all integrations
1 parent a342e00 commit b33f134

File tree

4 files changed

+71
-42
lines changed

4 files changed

+71
-42
lines changed

src/sentry/conf/server.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -882,7 +882,6 @@ def SOCIAL_AUTH_DEFAULT_USERNAME() -> str:
882882
"sentry.integrations.github.tasks.codecov_account_unlink",
883883
"sentry.integrations.github.tasks.link_all_repos",
884884
"sentry.integrations.github.tasks.pr_comment",
885-
"sentry.integrations.github.tasks.sync_repos",
886885
"sentry.integrations.github.tasks.sync_repos_on_install_change",
887886
"sentry.integrations.gitlab.tasks",
888887
"sentry.integrations.jira.tasks",
@@ -892,6 +891,7 @@ def SOCIAL_AUTH_DEFAULT_USERNAME() -> str:
892891
"sentry.integrations.slack.tasks.link_slack_user_identities",
893892
"sentry.integrations.slack.tasks.post_message",
894893
"sentry.integrations.slack.tasks.send_notifications_on_activity",
894+
"sentry.integrations.source_code_management.sync_repos",
895895
"sentry.integrations.source_code_management.tasks",
896896
"sentry.integrations.tasks.create_comment",
897897
"sentry.integrations.tasks.kick_off_status_syncs",
@@ -1251,8 +1251,8 @@ def SOCIAL_AUTH_DEFAULT_USERNAME() -> str:
12511251
"task": "sdk.control:sentry.tasks.release_registry.fetch_release_registry_data_control",
12521252
"schedule": crontab("*/5", "*", "*", "*", "*"),
12531253
},
1254-
"github-repo-sync-beat": {
1255-
"task": "integrations.control:sentry.integrations.github.tasks.sync_repos.github_repo_sync_beat",
1254+
"scm-repo-sync-beat": {
1255+
"task": "integrations.control:sentry.integrations.source_code_management.sync_repos.scm_repo_sync_beat",
12561256
"schedule": timedelta(minutes=1),
12571257
},
12581258
}

src/sentry/integrations/github/tasks/__init__.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,12 @@
22
from .codecov_account_unlink import codecov_account_unlink
33
from .link_all_repos import link_all_repos
44
from .pr_comment import github_comment_workflow
5-
from .sync_repos import github_repo_sync_beat, sync_repos_for_org
65
from .sync_repos_on_install_change import sync_repos_on_install_change
76

87
__all__ = (
98
"codecov_account_link",
109
"codecov_account_unlink",
1110
"github_comment_workflow",
12-
"github_repo_sync_beat",
1311
"link_all_repos",
14-
"sync_repos_for_org",
1512
"sync_repos_on_install_change",
1613
)

src/sentry/integrations/github/tasks/sync_repos.py renamed to src/sentry/integrations/source_code_management/sync_repos.py

Lines changed: 65 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
"""
2-
Periodic repo sync for GitHub integrations.
2+
Periodic repo sync for SCM integrations.
33
4-
The beat task (`github_repo_sync_beat`) runs on a schedule and uses
5-
CursoredScheduler to iterate over all active GitHub OrganizationIntegrations.
6-
For each one, it dispatches `sync_repos_for_org` which diffs GitHub's repo
7-
list against Sentry's Repository table and creates/disables/re-enables as needed.
4+
The beat task (`scm_repo_sync_beat`) runs on a schedule and uses
5+
CursoredScheduler to iterate over all active SCM OrganizationIntegrations.
6+
For each one, it dispatches `sync_repos_for_org` which diffs the provider's
7+
repo list against Sentry's Repository table and creates/disables/re-enables
8+
as needed.
89
"""
910

1011
import logging
12+
from collections.abc import Mapping
1113
from datetime import timedelta
14+
from typing import Any
1215

1316
from taskbroker_client.retry import Retry
1417

1518
from sentry import features
1619
from sentry.constants import ObjectStatus
20+
from sentry.features.exceptions import FeatureNotRegistered
1721
from sentry.integrations.models.organization_integration import OrganizationIntegration
1822
from sentry.integrations.services.integration import integration_service
1923
from sentry.integrations.services.repository.service import repository_service
@@ -24,6 +28,7 @@
2428
from sentry.organizations.services.organization import organization_service
2529
from sentry.plugins.providers.integration_repository import (
2630
RepoExistsError,
31+
RepositoryInputConfig,
2732
get_integration_repository_provider,
2833
)
2934
from sentry.shared_integrations.exceptions import ApiError
@@ -33,13 +38,37 @@
3338
from sentry.utils import metrics
3439
from sentry.utils.cursored_scheduler import CursoredScheduler
3540

36-
from .link_all_repos import get_repo_config
37-
3841
logger = logging.getLogger(__name__)
3942

43+
# All SCM providers that support periodic repo sync
44+
SCM_PROVIDERS = [
45+
"github",
46+
"github_enterprise",
47+
"gitlab",
48+
"bitbucket",
49+
"bitbucket_server",
50+
"vsts",
51+
]
52+
53+
54+
def _get_repo_config(repo: Mapping[str, Any], integration_id: int) -> RepositoryInputConfig:
55+
return {
56+
"external_id": str(repo["id"]),
57+
"integration_id": integration_id,
58+
"identifier": repo["full_name"],
59+
}
60+
61+
62+
def _has_feature(flag_name: str, org: Any) -> bool:
63+
"""Check a feature flag, returning False if the flag is not registered."""
64+
try:
65+
return features.has(flag_name, org)
66+
except FeatureNotRegistered:
67+
return False
68+
4069

4170
@instrumented_task(
42-
name="sentry.integrations.github.tasks.sync_repos.sync_repos_for_org",
71+
name="sentry.integrations.source_code_management.sync_repos.sync_repos_for_org",
4372
namespace=integrations_control_tasks,
4473
retry=Retry(times=3, delay=120),
4574
processing_deadline_duration=120,
@@ -50,8 +79,8 @@ def sync_repos_for_org(organization_integration_id: int) -> None:
5079
"""
5180
Sync repositories for a single OrganizationIntegration.
5281
53-
Fetches all repos from GitHub, diffs against Sentry's Repository table,
54-
and creates/disables/re-enables repos as needed.
82+
Fetches all repos from the SCM provider, diffs against Sentry's Repository
83+
table, and creates/disables/re-enables repos as needed.
5584
"""
5685
try:
5786
oi = OrganizationIntegration.objects.get(
@@ -87,23 +116,26 @@ def sync_repos_for_org(organization_integration_id: int) -> None:
87116
return
88117

89118
rpc_org = org_context.organization
90-
if not features.has("organizations:github-repo-auto-sync", rpc_org):
119+
provider_key = integration.provider
120+
121+
# Feature flags are per-provider: organizations:{provider}-repo-auto-sync
122+
if not _has_feature(f"organizations:{provider_key}-repo-auto-sync", rpc_org):
91123
return
92124

93-
provider = f"integrations:{integration.provider}"
94-
dry_run = not features.has("organizations:github-repo-auto-sync-apply", rpc_org)
125+
provider = f"integrations:{provider_key}"
126+
dry_run = not _has_feature(f"organizations:{provider_key}-repo-auto-sync-apply", rpc_org)
95127

96128
with SCMIntegrationInteractionEvent(
97129
interaction_type=SCMIntegrationInteractionType.SYNC_REPOS,
98130
integration_id=integration.id,
99131
organization_id=organization_id,
100-
provider_key=integration.provider,
132+
provider_key=provider_key,
101133
).capture():
102134
installation = integration.get_installation(organization_id=organization_id)
103135
client = installation.get_client()
104136

105137
try:
106-
github_repos = client.get_repos()
138+
provider_repos = client.get_repos()
107139
except ApiError as e:
108140
if installation.is_rate_limited_error(e):
109141
logger.info(
@@ -115,7 +147,7 @@ def sync_repos_for_org(organization_integration_id: int) -> None:
115147
)
116148
raise
117149

118-
github_external_ids = {str(repo["id"]) for repo in github_repos}
150+
provider_external_ids = {str(repo["id"]) for repo in provider_repos}
119151

120152
all_repos = repository_service.get_repositories(
121153
organization_id=organization_id,
@@ -127,22 +159,22 @@ def sync_repos_for_org(organization_integration_id: int) -> None:
127159
r for r in all_repos if r.status == ObjectStatus.DISABLED and r.external_id
128160
]
129161

130-
sentry_active_ids = {r.external_id for r in active_repos}
131-
sentry_disabled_ids = {r.external_id for r in disabled_repos}
162+
sentry_active_ids: set[str] = {r.external_id for r in active_repos} # type: ignore[misc]
163+
sentry_disabled_ids: set[str] = {r.external_id for r in disabled_repos} # type: ignore[misc]
132164

133-
new_ids = github_external_ids - sentry_active_ids - sentry_disabled_ids
134-
removed_ids = sentry_active_ids - github_external_ids
135-
restored_ids = sentry_disabled_ids & github_external_ids
165+
new_ids = provider_external_ids - sentry_active_ids - sentry_disabled_ids
166+
removed_ids = sentry_active_ids - provider_external_ids
167+
restored_ids = sentry_disabled_ids & provider_external_ids
136168

137169
metric_tags = {
138-
"provider": integration.provider,
170+
"provider": provider_key,
139171
"dry_run": str(dry_run),
140172
}
141173
metrics.distribution("scm.repo_sync.new_repos", len(new_ids), tags=metric_tags)
142174
metrics.distribution("scm.repo_sync.removed_repos", len(removed_ids), tags=metric_tags)
143175
metrics.distribution("scm.repo_sync.restored_repos", len(restored_ids), tags=metric_tags)
144176
metrics.distribution(
145-
"scm.repo_sync.provider_total", len(github_external_ids), tags=metric_tags
177+
"scm.repo_sync.provider_total", len(provider_external_ids), tags=metric_tags
146178
)
147179
metrics.distribution(
148180
"scm.repo_sync.sentry_active", len(sentry_active_ids), tags=metric_tags
@@ -155,11 +187,11 @@ def sync_repos_for_org(organization_integration_id: int) -> None:
155187
logger.info(
156188
"scm.repo_sync.diff",
157189
extra={
158-
"provider": integration.provider,
190+
"provider": provider_key,
159191
"integration_id": integration.id,
160192
"organization_id": organization_id,
161193
"dry_run": dry_run,
162-
"provider_total": len(github_external_ids),
194+
"provider_total": len(provider_external_ids),
163195
"sentry_active": len(sentry_active_ids),
164196
"sentry_disabled": len(sentry_disabled_ids),
165197
"new": len(new_ids),
@@ -173,9 +205,9 @@ def sync_repos_for_org(organization_integration_id: int) -> None:
173205

174206
if new_ids:
175207
integration_repo_provider = get_integration_repository_provider(integration)
176-
repo_configs = [
177-
get_repo_config(repo, integration.id)
178-
for repo in github_repos
208+
repo_configs: list[RepositoryInputConfig] = [
209+
_get_repo_config(repo, integration.id)
210+
for repo in provider_repos
179211
if str(repo["id"]) in new_ids
180212
]
181213
if repo_configs:
@@ -204,16 +236,16 @@ def sync_repos_for_org(organization_integration_id: int) -> None:
204236

205237

206238
@instrumented_task(
207-
name="sentry.integrations.github.tasks.sync_repos.github_repo_sync_beat",
239+
name="sentry.integrations.source_code_management.sync_repos.scm_repo_sync_beat",
208240
namespace=integrations_control_tasks,
209241
silo_mode=SiloMode.CONTROL,
210242
)
211-
def github_repo_sync_beat() -> None:
243+
def scm_repo_sync_beat() -> None:
212244
scheduler = CursoredScheduler(
213-
name="github_repo_sync",
214-
schedule_key="github-repo-sync-beat",
245+
name="scm_repo_sync",
246+
schedule_key="scm-repo-sync-beat",
215247
queryset=OrganizationIntegration.objects.filter(
216-
integration__provider="github",
248+
integration__provider__in=SCM_PROVIDERS,
217249
integration__status=ObjectStatus.ACTIVE,
218250
status=ObjectStatus.ACTIVE,
219251
),

tests/sentry/integrations/github/tasks/test_sync_repos.py renamed to tests/sentry/integrations/source_code_management/test_sync_repos.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66

77
from sentry.constants import ObjectStatus
88
from sentry.integrations.github.integration import GitHubIntegrationProvider
9-
from sentry.integrations.github.tasks.sync_repos import sync_repos_for_org
109
from sentry.integrations.models.organization_integration import OrganizationIntegration
10+
from sentry.integrations.source_code_management.sync_repos import sync_repos_for_org
1111
from sentry.models.repository import Repository
1212
from sentry.silo.base import SiloMode
1313
from sentry.testutils.cases import IntegrationTestCase
@@ -21,13 +21,13 @@ class SyncReposForOrgTestCase(IntegrationTestCase):
2121
base_url = "https://api.github.com"
2222
key = "github"
2323

24-
def setUp(self):
24+
def setUp(self) -> None:
2525
super().setUp()
2626
self.oi = OrganizationIntegration.objects.get(
2727
organization_id=self.organization.id, integration=self.integration
2828
)
2929

30-
def _add_repos_response(self, repos):
30+
def _add_repos_response(self, repos: list[dict[str, object]]) -> None:
3131
responses.add(
3232
responses.GET,
3333
self.base_url + "/installation/repositories?per_page=100",

0 commit comments

Comments
 (0)