Repository: python-gitlab/python-gitlab

Issue:
Feature Request: Support promoting project labels
## Description of the problem, including code/CLI snippet

This request is for supporting the ability to 'promote' a project label to a group one. This exists in the Gitlab API.

Specifically, https://docs.gitlab.com/ee/api/labels.html#promote-a-project-label-to-a-group-label

## Expected Behavior

`project.labels.promote(label_id)`
or 
`gitlab project-label promote` (CLI)

## Actual Behavior

Promotion not supported

## Specifications

  - python-gitlab version: Latest
  - API version you are using (v3/v4): v4
  - Gitlab server version (or gitlab.com): Any



Discussion:
I'd like to work on this issue - if you could assign it to me, that'd be great! 😁 

@StingRayZA Thanks 👍 

Resolving patch:
diff --git a/gitlab/exceptions.py b/gitlab/exceptions.py
--- a/gitlab/exceptions.py
+++ b/gitlab/exceptions.py
@@ -111,6 +111,10 @@ class GitlabProjectDeployKeyError(GitlabOperationError):
     pass
 
 
+class GitlabPromoteError(GitlabOperationError):
+    pass
+
+
 class GitlabCancelError(GitlabOperationError):
     pass
 
diff --git a/gitlab/mixins.py b/gitlab/mixins.py
--- a/gitlab/mixins.py
+++ b/gitlab/mixins.py
@@ -926,3 +926,50 @@ def render(self, link_url: str, image_url: str, **kwargs: Any) -> Dict[str, Any]
         if TYPE_CHECKING:
             assert not isinstance(result, requests.Response)
         return result
+
+
+class PromoteMixin(_RestObjectBase):
+    _id_attr: Optional[str]
+    _attrs: Dict[str, Any]
+    _module: ModuleType
+    _parent_attrs: Dict[str, Any]
+    _updated_attrs: Dict[str, Any]
+    _update_uses_post: bool = False
+    manager: base.RESTManager
+
+    def _get_update_method(
+        self,
+    ) -> Callable[..., Union[Dict[str, Any], requests.Response]]:
+        """Return the HTTP method to use.
+
+        Returns:
+            object: http_put (default) or http_post
+        """
+        if self._update_uses_post:
+            http_method = self.manager.gitlab.http_post
+        else:
+            http_method = self.manager.gitlab.http_put
+        return http_method
+
+    @exc.on_http_error(exc.GitlabPromoteError)
+    def promote(self, **kwargs: Any) -> Dict[str, Any]:
+        """Promote the item.
+
+        Args:
+            **kwargs: Extra options to send to the server (e.g. sudo)
+
+        Raises:
+            GitlabAuthenticationError: If authentication is not correct
+            GitlabPromoteError: If the item could not be promoted
+            GitlabParsingError: If the json data could not be parsed
+
+        Returns:
+            dict: The updated object data (*not* a RESTObject)
+        """
+
+        path = "%s/%s/promote" % (self.manager.path, self.id)
+        http_method = self._get_update_method()
+        result = http_method(path, **kwargs)
+        if TYPE_CHECKING:
+            assert not isinstance(result, requests.Response)
+        return result
diff --git a/gitlab/v4/objects/labels.py b/gitlab/v4/objects/labels.py
--- a/gitlab/v4/objects/labels.py
+++ b/gitlab/v4/objects/labels.py
@@ -5,6 +5,7 @@
     DeleteMixin,
     ListMixin,
     ObjectDeleteMixin,
+    PromoteMixin,
     RetrieveMixin,
     SaveMixin,
     SubscribableMixin,
@@ -83,7 +84,9 @@ def delete(self, name, **kwargs):
         self.gitlab.http_delete(self.path, query_data={"name": name}, **kwargs)
 
 
-class ProjectLabel(SubscribableMixin, SaveMixin, ObjectDeleteMixin, RESTObject):
+class ProjectLabel(
+    PromoteMixin, SubscribableMixin, SaveMixin, ObjectDeleteMixin, RESTObject
+):
     _id_attr = "name"
 
     # Update without ID, but we need an ID to get from list.


Test patch:
diff --git a/tests/functional/api/test_projects.py b/tests/functional/api/test_projects.py
--- a/tests/functional/api/test_projects.py
+++ b/tests/functional/api/test_projects.py
@@ -1,3 +1,5 @@
+import uuid
+
 import pytest
 
 import gitlab
@@ -159,6 +161,28 @@ def test_project_labels(project):
     assert len(project.labels.list()) == 0
 
 
+def test_project_label_promotion(gl, group):
+    """
+    Label promotion requires the project to be a child of a group (not in a user namespace)
+
+    """
+    _id = uuid.uuid4().hex
+    data = {
+        "name": f"test-project-{_id}",
+        "namespace_id": group.id,
+    }
+    project = gl.projects.create(data)
+
+    label_name = "promoteme"
+    promoted_label = project.labels.create({"name": label_name, "color": "#112233"})
+    promoted_label.promote()
+
+    assert any(label.name == label_name for label in group.labels.list())
+
+    group.labels.delete(label_name)
+    assert not any(label.name == label_name for label in group.labels.list())
+
+
 def test_project_milestones(project):
     milestone = project.milestones.create({"title": "milestone1"})
     assert len(project.milestones.list()) == 1