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
1011import logging
12+ from collections .abc import Mapping
1113from datetime import timedelta
14+ from typing import Any
1215
1316from taskbroker_client .retry import Retry
1417
1518from sentry import features
1619from sentry .constants import ObjectStatus
20+ from sentry .features .exceptions import FeatureNotRegistered
1721from sentry .integrations .models .organization_integration import OrganizationIntegration
1822from sentry .integrations .services .integration import integration_service
1923from sentry .integrations .services .repository .service import repository_service
2428from sentry .organizations .services .organization import organization_service
2529from sentry .plugins .providers .integration_repository import (
2630 RepoExistsError ,
31+ RepositoryInputConfig ,
2732 get_integration_repository_provider ,
2833)
2934from sentry .shared_integrations .exceptions import ApiError
3338from sentry .utils import metrics
3439from sentry .utils .cursored_scheduler import CursoredScheduler
3540
36- from .link_all_repos import get_repo_config
37-
3841logger = 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 ),
0 commit comments