diff --git a/api/core/workflows_services.py b/api/core/workflows_services.py index 4481e08ed2ef..9d615443c6db 100644 --- a/api/core/workflows_services.py +++ b/api/core/workflows_services.py @@ -24,6 +24,7 @@ class ChangeRequestCommitService: def __init__(self, change_request: "ChangeRequest") -> None: self.change_request = change_request + @transaction.atomic def commit(self, committed_by: "FFAdminUser") -> None: if not self.change_request.is_approved(): raise ChangeRequestNotApprovedError( @@ -130,6 +131,12 @@ def _publish_segments(self) -> None: logger.warning("missing-live-segment", draft_segment=draft_segment.uuid) continue + # Prevent overwriting system segments + if getattr(live_segment, "is_system_segment", False): + raise ValueError( + "System segments cannot be overwritten via change request drafts." + ) + # Make a revision of the live segment revision = live_segment.clone(is_revision=True) logger.info( diff --git a/api/tests/unit/features/workflows/core/test_unit_workflows_models.py b/api/tests/unit/features/workflows/core/test_unit_workflows_models.py index 9f3036f37bed..667b4541f7b9 100644 --- a/api/tests/unit/features/workflows/core/test_unit_workflows_models.py +++ b/api/tests/unit/features/workflows/core/test_unit_workflows_models.py @@ -1283,3 +1283,44 @@ def test_change_request_commit__v1_segment_override_draft__inherits_mv_hashing_s # Then the draft carries the superseded override's id as its bucketing salt draft_feature_state.refresh_from_db() assert draft_feature_state.mv_hashing_salt == live_override.id + + +def test_change_request_commit__system_segment_draft__raises_value_error( + segment: Segment, + change_request: ChangeRequest, + admin_user: FFAdminUser, + feature: Feature, + environment: Environment, +) -> None: + # Given + segment.is_system_segment = True + segment.save() + + Segment.objects.create( + name="system-segment-draft", + change_request=change_request, + project=segment.project, + version_of=segment, + ) + + # Add a feature state to test transaction rollback behavior + feature_state = FeatureState.objects.create( + feature=feature, + environment=environment, + change_request=change_request, + version=None, + ) + initial_version = feature_state.version + + # When / Then + with pytest.raises( + ValueError, + match="System segments cannot be overwritten via change request drafts.", + ): + change_request.commit(admin_user) + + # Assert that the transaction rolled back successfully + feature_state.refresh_from_db() + change_request.refresh_from_db() + assert feature_state.version == initial_version + assert change_request.committed_at is None