diff --git a/api/integrations/gitlab/mappers.py b/api/integrations/gitlab/mappers.py index 7846af55b487..1567e0f3ce08 100644 --- a/api/integrations/gitlab/mappers.py +++ b/api/integrations/gitlab/mappers.py @@ -56,6 +56,21 @@ def map_gitlab_resource_to_tag_label( return None +def map_resource_url_to_filter_value(url: str) -> list[str]: + """Return a list of equivalent URL shapes for use as an ``__in`` filter + value when looking up a linked GitLab resource. + + GitLab delivers issue webhooks with ``/-/work_items/`` URLs even when + the feature was linked via the legacy ``/-/issues/`` form, and vice + versa. Both shapes refer to the same issue. + """ + if "/-/issues/" in url: + return [url, url.replace("/-/issues/", "/-/work_items/", 1)] + if "/-/work_items/" in url: + return [url, url.replace("/-/work_items/", "/-/issues/", 1)] + return [url] + + def map_gitlab_webhook_payload_to_tag_label( payload: GitLabWebhookPayload, ) -> GitLabTagLabel | None: diff --git a/api/integrations/gitlab/services.py b/api/integrations/gitlab/services.py index 76a51ea608c9..7e1d2353b3a6 100644 --- a/api/integrations/gitlab/services.py +++ b/api/integrations/gitlab/services.py @@ -27,6 +27,7 @@ from integrations.gitlab.mappers import ( map_gitlab_resource_to_tag_label, map_gitlab_webhook_payload_to_tag_label, + map_resource_url_to_filter_value, ) from integrations.gitlab.models import GitLabConfiguration, GitLabWebhook from integrations.gitlab.types import GitLabWebhookPayload @@ -215,7 +216,7 @@ def apply_tag_for_event( if not ( feature := Feature.objects.filter( project=webhook.gitlab_configuration.project, - external_resources__url=resource_url, + external_resources__url__in=map_resource_url_to_filter_value(resource_url), external_resources__type__in=GITLAB_RESOURCE_TYPES, ).first() ): diff --git a/api/tests/integration/features/test_gitlab_webhook.py b/api/tests/integration/features/test_gitlab_webhook.py index 4bf29b55fceb..ff39417b21be 100644 --- a/api/tests/integration/features/test_gitlab_webhook.py +++ b/api/tests/integration/features/test_gitlab_webhook.py @@ -71,6 +71,50 @@ def _link( return _link +@pytest.mark.parametrize( + "linked_url, payload_url", + [ + ( + "https://gitlab.example.com/testorg/testrepo/-/issues/42", + "https://gitlab.example.com/testorg/testrepo/-/work_items/42", + ), + ( + "https://gitlab.example.com/testorg/testrepo/-/work_items/42", + "https://gitlab.example.com/testorg/testrepo/-/issues/42", + ), + ], +) +def test_gitlab_webhook__issue_url_variants__still_matches_feature( + api_client: APIClient, + feature: int, + webhook_url: str, + link_feature: LinkFeatureFixture, + linked_url: str, + payload_url: str, +) -> None: + # Given — GitLab may deliver ``work_items`` URLs even when the link uses the + # legacy ``issues`` path (or vice versa). Both shapes refer to the same issue. + link_feature(linked_url, ResourceType.GITLAB_ISSUE, metadata={"state": "opened"}) + payload = { + "object_kind": "issue", + "object_attributes": {"url": payload_url, "state": "closed"}, + } + + # When + response = api_client.post( + webhook_url, + data=payload, + format="json", + HTTP_X_GITLAB_TOKEN=WEBHOOK_SECRET, + ) + + # Then + assert response.status_code == status.HTTP_200_OK + labels = set(Feature.objects.get(id=feature).tags.values_list("label", flat=True)) + assert GitLabTagLabel.ISSUE_CLOSED.value in labels + assert GitLabTagLabel.ISSUE_OPEN.value not in labels + + @pytest.mark.django_db() def test_gitlab_webhook__issue_close_event__switches_tag_to_issue_closed( api_client: APIClient, diff --git a/docs/docs/deployment-self-hosting/observability/_events-catalogue.md b/docs/docs/deployment-self-hosting/observability/_events-catalogue.md index 81eb31771b3e..3d827d1d4fb2 100644 --- a/docs/docs/deployment-self-hosting/observability/_events-catalogue.md +++ b/docs/docs/deployment-self-hosting/observability/_events-catalogue.md @@ -98,7 +98,7 @@ Attributes: ### `gitlab.feature.tagged` Logged at `info` from: - - `api/integrations/gitlab/services.py:229` + - `api/integrations/gitlab/services.py:230` Attributes: - `action` @@ -111,7 +111,7 @@ Attributes: ### `gitlab.webhook.deregistered` Logged at `info` from: - - `api/integrations/gitlab/services.py:159` + - `api/integrations/gitlab/services.py:160` Attributes: - `gitlab.hook.id` @@ -122,7 +122,7 @@ Attributes: ### `gitlab.webhook.deregistration_failed` Logged at `warning` from: - - `api/integrations/gitlab/services.py:152` + - `api/integrations/gitlab/services.py:153` Attributes: - `exc_info` @@ -134,7 +134,7 @@ Attributes: ### `gitlab.webhook.registered` Logged at `info` from: - - `api/integrations/gitlab/services.py:112` + - `api/integrations/gitlab/services.py:113` Attributes: - `gitlab.hook.id` @@ -146,7 +146,7 @@ Attributes: ### `gitlab.webhook.registration_failed` Logged at `error` from: - - `api/integrations/gitlab/services.py:97` + - `api/integrations/gitlab/services.py:98` Attributes: - `exc_info`