summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/gl_objects/deployments.rst12
-rw-r--r--gitlab/exceptions.py4
-rw-r--r--gitlab/v4/objects/deployments.py52
-rw-r--r--tests/unit/objects/test_deployments.py134
4 files changed, 199 insertions, 3 deletions
diff --git a/docs/gl_objects/deployments.rst b/docs/gl_objects/deployments.rst
index ae10103..9c810ce 100644
--- a/docs/gl_objects/deployments.rst
+++ b/docs/gl_objects/deployments.rst
@@ -40,6 +40,18 @@ Update a deployment::
deployment.status = "failed"
deployment.save()
+Approve a deployment::
+
+ deployment = project.deployments.get(42)
+ # `status` must be either "approved" or "rejected".
+ deployment.approval(status="approved")
+
+Reject a deployment::
+
+ deployment = project.deployments.get(42)
+ # Using the optional `comment` and `represented_as` arguments
+ deployment.approval(status="rejected", comment="Fails CI", represented_as="security")
+
Merge requests associated with a deployment
===========================================
diff --git a/gitlab/exceptions.py b/gitlab/exceptions.py
index 01439e4..633de5b 100644
--- a/gitlab/exceptions.py
+++ b/gitlab/exceptions.py
@@ -301,6 +301,10 @@ class GitlabUserRejectError(GitlabOperationError):
pass
+class GitlabDeploymentApprovalError(GitlabOperationError):
+ pass
+
+
# For an explanation of how these type-hints work see:
# https://mypy.readthedocs.io/en/stable/generics.html#declaring-decorators
#
diff --git a/gitlab/v4/objects/deployments.py b/gitlab/v4/objects/deployments.py
index a431603..145273b 100644
--- a/gitlab/v4/objects/deployments.py
+++ b/gitlab/v4/objects/deployments.py
@@ -1,5 +1,11 @@
-from typing import Any, cast, Union
+"""
+GitLab API:
+https://docs.gitlab.com/ee/api/deployments.html
+"""
+from typing import Any, cast, Dict, Optional, TYPE_CHECKING, Union
+from gitlab import cli
+from gitlab import exceptions as exc
from gitlab.base import RESTManager, RESTObject
from gitlab.mixins import CreateMixin, RetrieveMixin, SaveMixin, UpdateMixin
from gitlab.types import RequiredOptional
@@ -15,6 +21,50 @@ __all__ = [
class ProjectDeployment(SaveMixin, RESTObject):
mergerequests: ProjectDeploymentMergeRequestManager
+ @cli.register_custom_action(
+ "ProjectDeployment",
+ mandatory=("status",),
+ optional=("comment", "represented_as"),
+ )
+ @exc.on_http_error(exc.GitlabDeploymentApprovalError)
+ def approval(
+ self,
+ status: str,
+ comment: Optional[str] = None,
+ represented_as: Optional[str] = None,
+ **kwargs: Any,
+ ) -> Dict[str, Any]:
+ """Approve or reject a blocked deployment.
+
+ Args:
+ status: Either "approved" or "rejected"
+ comment: A comment to go with the approval
+ represented_as: The name of the User/Group/Role to use for the
+ approval, when the user belongs to multiple
+ approval rules.
+ **kwargs: Extra options to send to the server (e.g. sudo)
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabMRApprovalError: If the approval failed
+
+ Returns:
+ A dict containing the result.
+
+ https://docs.gitlab.com/ee/api/deployments.html#approve-or-reject-a-blocked-deployment
+ """
+ path = f"{self.manager.path}/{self.encoded_id}/approval"
+ data = {"status": status}
+ if comment is not None:
+ data["comment"] = comment
+ if represented_as is not None:
+ data["represented_as"] = represented_as
+
+ server_data = self.manager.gitlab.http_post(path, post_data=data, **kwargs)
+ if TYPE_CHECKING:
+ assert isinstance(server_data, dict)
+ return server_data
+
class ProjectDeploymentManager(RetrieveMixin, CreateMixin, UpdateMixin, RESTManager):
_path = "/projects/{project_id}/deployments"
diff --git a/tests/unit/objects/test_deployments.py b/tests/unit/objects/test_deployments.py
index 92e33c2..e7099f2 100644
--- a/tests/unit/objects/test_deployments.py
+++ b/tests/unit/objects/test_deployments.py
@@ -6,7 +6,25 @@ import responses
@pytest.fixture
-def resp_deployment():
+def resp_deployment_get():
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url="http://localhost/api/v4/projects/1/deployments/42",
+ json=response_get_content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def deployment(project):
+ return project.deployments.get(42, lazy=True)
+
+
+@pytest.fixture
+def resp_deployment_create():
content = {"id": 42, "status": "success", "ref": "main"}
with responses.RequestsMock() as rsps:
@@ -31,7 +49,42 @@ def resp_deployment():
yield rsps
-def test_deployment(project, resp_deployment):
+@pytest.fixture
+def resp_deployment_approval():
+ content = {
+ "user": {
+ "id": 100,
+ "username": "security-user-1",
+ "name": "security user-1",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/e130fcd3a1681f41a3de69d10841afa9?s=80&d=identicon",
+ "web_url": "http://localhost:3000/security-user-1",
+ },
+ "status": "approved",
+ "created_at": "2022-02-24T20:22:30.097Z",
+ "comment": "Looks good to me",
+ }
+
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.POST,
+ url="http://localhost/api/v4/projects/1/deployments/42/approval",
+ json=content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+def test_deployment_get(project, resp_deployment_get):
+ deployment = project.deployments.get(42)
+ assert deployment.id == 42
+ assert deployment.iid == 2
+ assert deployment.status == "success"
+ assert deployment.ref == "main"
+
+
+def test_deployment_create(project, resp_deployment_create):
deployment = project.deployments.create(
{
"environment": "Test",
@@ -48,3 +101,80 @@ def test_deployment(project, resp_deployment):
deployment.status = "failed"
deployment.save()
assert deployment.status == "failed"
+
+
+def test_deployment_approval(deployment, resp_deployment_approval) -> None:
+ result = deployment.approval(status="approved")
+ assert result["status"] == "approved"
+ assert result["comment"] == "Looks good to me"
+
+
+response_get_content = {
+ "id": 42,
+ "iid": 2,
+ "ref": "main",
+ "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+ "created_at": "2016-08-11T11:32:35.444Z",
+ "updated_at": "2016-08-11T11:34:01.123Z",
+ "status": "success",
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://localhost:3000/root",
+ },
+ "environment": {
+ "id": 9,
+ "name": "production",
+ "external_url": "https://about.gitlab.com",
+ },
+ "deployable": {
+ "id": 664,
+ "status": "success",
+ "stage": "deploy",
+ "name": "deploy",
+ "ref": "main",
+ "tag": False,
+ "coverage": None,
+ "created_at": "2016-08-11T11:32:24.456Z",
+ "started_at": None,
+ "finished_at": "2016-08-11T11:32:35.145Z",
+ "user": {
+ "id": 1,
+ "name": "Administrator",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://gitlab.dev/root",
+ "created_at": "2015-12-21T13:14:24.077Z",
+ "bio": None,
+ "location": None,
+ "skype": "",
+ "linkedin": "",
+ "twitter": "",
+ "website_url": "",
+ "organization": "",
+ },
+ "commit": {
+ "id": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+ "short_id": "a91957a8",
+ "title": "Merge branch 'rename-readme' into 'main'\r",
+ "author_name": "Administrator",
+ "author_email": "admin@example.com",
+ "created_at": "2016-08-11T13:28:26.000+02:00",
+ "message": "Merge branch 'rename-readme' into 'main'\r\n\r\nRename README\r\n\r\n\r\n\r\nSee merge request !2",
+ },
+ "pipeline": {
+ "created_at": "2016-08-11T07:43:52.143Z",
+ "id": 42,
+ "ref": "main",
+ "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+ "status": "success",
+ "updated_at": "2016-08-11T07:43:52.143Z",
+ "web_url": "http://gitlab.dev/root/project/pipelines/5",
+ },
+ "runner": None,
+ },
+}