88from django import forms
99from django .http .request import HttpRequest
1010from django .http .response import HttpResponseBase
11+ from django .urls import reverse
1112from django .utils .translation import gettext_lazy as _
1213from rest_framework .fields import BooleanField , CharField , URLField
1314
2425 IntegrationProvider ,
2526)
2627from sentry .integrations .gitlab .constants import GITLAB_WEBHOOK_VERSION , GITLAB_WEBHOOK_VERSION_KEY
27- from sentry .integrations .gitlab .tasks import update_all_project_webhooks
28+ from sentry .integrations .gitlab .types import GitLabIssueStatus
29+ from sentry .integrations .models .integration_external_project import IntegrationExternalProject
2830from sentry .integrations .pipeline import IntegrationPipeline
2931from sentry .integrations .referrer_ids import GITLAB_PR_BOT_REFERRER
3032from sentry .integrations .services .integration import integration_service
33+ from sentry .integrations .services .repository import repository_service
3134from sentry .integrations .services .repository .model import RpcRepository
3235from sentry .integrations .source_code_management .commit_context import (
3336 CommitContextIntegration ,
4851 ApiForbiddenError ,
4952 ApiUnauthorized ,
5053 IntegrationConfigurationError ,
54+ IntegrationError ,
5155 IntegrationProviderError ,
5256)
5357from sentry .snuba .referrer import Referrer
@@ -275,6 +279,54 @@ def get_organization_config(self) -> list[dict[str, Any]]:
275279
276280 has_issue_sync = features .has ("organizations:integrations-issue-sync" , organization )
277281
282+ # Add outbound status sync configuration if feature flag is enabled
283+ if self .check_feature_flag ():
284+ # Get currently configured external projects to display their labels
285+ current_project_items = []
286+ if self .org_integration :
287+ external_projects = IntegrationExternalProject .objects .filter (
288+ organization_integration_id = self .org_integration .id
289+ )
290+
291+ if external_projects .exists ():
292+ current_project_items = [
293+ {"value" : project .external_id , "label" : project .name }
294+ for project in external_projects
295+ ]
296+
297+ config .insert (
298+ 0 ,
299+ {
300+ "name" : self .outbound_status_key ,
301+ "type" : "choice_mapper" ,
302+ "label" : _ ("Sync Sentry Status to GitLab" ),
303+ "help" : _ (
304+ "When a Sentry issue changes status, change the status of the linked ticket in GitLab."
305+ ),
306+ "addButtonText" : _ ("Add GitLab Project" ),
307+ "addDropdown" : {
308+ "emptyMessage" : _ ("All projects configured" ),
309+ "noResultsMessage" : _ ("Could not find GitLab project" ),
310+ "items" : current_project_items ,
311+ "url" : reverse (
312+ "sentry-extensions-gitlab-search" ,
313+ args = [organization .slug , self .model .id ],
314+ ),
315+ "searchField" : "project" ,
316+ },
317+ "mappedSelectors" : {
318+ "on_resolve" : {"choices" : GitLabIssueStatus .get_choices ()},
319+ "on_unresolve" : {"choices" : GitLabIssueStatus .get_choices ()},
320+ },
321+ "columnLabels" : {
322+ "on_resolve" : _ ("When resolved" ),
323+ "on_unresolve" : _ ("When unresolved" ),
324+ },
325+ "mappedColumnLabel" : _ ("GitLab Project" ),
326+ "formatMessageValue" : False ,
327+ },
328+ )
329+
278330 if not has_issue_sync :
279331 for field in config :
280332 field ["disabled" ] = True
@@ -290,6 +342,43 @@ def update_organization_config(self, data: MutableMapping[str, Any]) -> None:
290342
291343 config = self .org_integration .config
292344
345+ # Handle status sync configuration
346+ if "sync_status_forward" in data :
347+ project_mappings = data .pop ("sync_status_forward" )
348+
349+ # Validate that all mappings have both statuses
350+ if any (
351+ not mapping .get ("on_unresolve" ) or not mapping .get ("on_resolve" )
352+ for mapping in project_mappings .values ()
353+ ):
354+ raise IntegrationError ("Resolve and unresolve status are required." )
355+
356+ data ["sync_status_forward" ] = bool (project_mappings )
357+
358+ IntegrationExternalProject .objects .filter (
359+ organization_integration_id = self .org_integration .id
360+ ).delete ()
361+
362+ for project_path , statuses in project_mappings .items ():
363+ # Validate status values
364+ valid_statuses = {GitLabIssueStatus .OPENED .value , GitLabIssueStatus .CLOSED .value }
365+ if statuses ["on_resolve" ] not in valid_statuses :
366+ raise IntegrationError (
367+ f"Invalid resolve status: { statuses ['on_resolve' ]} . Must be 'opened' or 'closed'."
368+ )
369+ if statuses ["on_unresolve" ] not in valid_statuses :
370+ raise IntegrationError (
371+ f"Invalid unresolve status: { statuses ['on_unresolve' ]} . Must be 'opened' or 'closed'."
372+ )
373+
374+ IntegrationExternalProject .objects .create (
375+ organization_integration_id = self .org_integration .id ,
376+ external_id = project_path ,
377+ name = project_path ,
378+ resolved_status = statuses ["on_resolve" ],
379+ unresolved_status = statuses ["on_unresolve" ],
380+ )
381+
293382 # Check webhook version BEFORE updating config to determine if migration is needed
294383 current_webhook_version = config .get (GITLAB_WEBHOOK_VERSION_KEY , 0 )
295384
@@ -302,11 +391,9 @@ def update_organization_config(self, data: MutableMapping[str, Any]) -> None:
302391 if org_integration is not None :
303392 self .org_integration = org_integration
304393
305- # Only update webhooks if:
306- # 1. A sync setting was enabled, AND
307- # 2. The webhook version is outdated
394+ # Only update webhooks if the webhook version is outdated
308395 if current_webhook_version < GITLAB_WEBHOOK_VERSION :
309- update_all_project_webhooks . delay (
396+ repository_service . schedule_update_gitlab_project_webhooks (
310397 integration_id = self .model .id ,
311398 organization_id = self .organization_id ,
312399 )
0 commit comments