Skip to content

Commit 9c9eeb9

Browse files
feat: add support for deployment approval endpoint
Add support for the deployment approval endpoint[1] [1] https://docs.gitlab.com/ee/api/deployments.html#approve-or-reject-a-blocked-deployment Closes: python-gitlab#2253
1 parent e095735 commit 9c9eeb9

File tree

4 files changed

+199
-3
lines changed

4 files changed

+199
-3
lines changed

docs/gl_objects/deployments.rst

+12
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,18 @@ Update a deployment::
4040
deployment.status = "failed"
4141
deployment.save()
4242

43+
Approve a deployment::
44+
45+
deployment = project.deployments.get(42)
46+
# `status` must be either "approved" or "rejected".
47+
deployment.approval(status="approved")
48+
49+
Reject a deployment::
50+
51+
deployment = project.deployments.get(42)
52+
# Using the optional `comment` and `represented_as` arguments
53+
deployment.approval(status="rejected", comment="Fails CI", represented_as="security")
54+
4355
Merge requests associated with a deployment
4456
===========================================
4557

gitlab/exceptions.py

+4
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,10 @@ class GitlabUserRejectError(GitlabOperationError):
301301
pass
302302

303303

304+
class GitlabDeploymentApprovalError(GitlabOperationError):
305+
pass
306+
307+
304308
# For an explanation of how these type-hints work see:
305309
# https://mypy.readthedocs.io/en/stable/generics.html#declaring-decorators
306310
#

gitlab/v4/objects/deployments.py

+51-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1-
from typing import Any, cast, Union
1+
"""
2+
GitLab API:
3+
https://docs.gitlab.com/ee/api/deployments.html
4+
"""
5+
from typing import Any, cast, Dict, Optional, TYPE_CHECKING, Union
26

7+
from gitlab import cli
8+
from gitlab import exceptions as exc
39
from gitlab.base import RESTManager, RESTObject
410
from gitlab.mixins import CreateMixin, RetrieveMixin, SaveMixin, UpdateMixin
511
from gitlab.types import RequiredOptional
@@ -15,6 +21,50 @@
1521
class ProjectDeployment(SaveMixin, RESTObject):
1622
mergerequests: ProjectDeploymentMergeRequestManager
1723

24+
@cli.register_custom_action(
25+
"ProjectDeployment",
26+
mandatory=("status",),
27+
optional=("comment", "represented_as"),
28+
)
29+
@exc.on_http_error(exc.GitlabDeploymentApprovalError)
30+
def approval(
31+
self,
32+
status: str,
33+
comment: Optional[str] = None,
34+
represented_as: Optional[str] = None,
35+
**kwargs: Any,
36+
) -> Dict[str, Any]:
37+
"""Approve or reject a blocked deployment.
38+
39+
Args:
40+
status: Either "approved" or "rejected"
41+
comment: A comment to go with the approval
42+
represented_as: The name of the User/Group/Role to use for the
43+
approval, when the user belongs to multiple
44+
approval rules.
45+
**kwargs: Extra options to send to the server (e.g. sudo)
46+
47+
Raises:
48+
GitlabAuthenticationError: If authentication is not correct
49+
GitlabMRApprovalError: If the approval failed
50+
51+
Returns:
52+
A dict containing the result.
53+
54+
https://docs.gitlab.com/ee/api/deployments.html#approve-or-reject-a-blocked-deployment
55+
"""
56+
path = f"{self.manager.path}/{self.encoded_id}/approval"
57+
data = {"status": status}
58+
if comment is not None:
59+
data["comment"] = comment
60+
if represented_as is not None:
61+
data["represented_as"] = represented_as
62+
63+
server_data = self.manager.gitlab.http_post(path, post_data=data, **kwargs)
64+
if TYPE_CHECKING:
65+
assert isinstance(server_data, dict)
66+
return server_data
67+
1868

1969
class ProjectDeploymentManager(RetrieveMixin, CreateMixin, UpdateMixin, RESTManager):
2070
_path = "/projects/{project_id}/deployments"

tests/unit/objects/test_deployments.py

+132-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,25 @@
66

77

88
@pytest.fixture
9-
def resp_deployment():
9+
def resp_deployment_get():
10+
with responses.RequestsMock() as rsps:
11+
rsps.add(
12+
method=responses.GET,
13+
url="http://localhost/api/v4/projects/1/deployments/42",
14+
json=response_get_content,
15+
content_type="application/json",
16+
status=200,
17+
)
18+
yield rsps
19+
20+
21+
@pytest.fixture
22+
def deployment(project):
23+
return project.deployments.get(42, lazy=True)
24+
25+
26+
@pytest.fixture
27+
def resp_deployment_create():
1028
content = {"id": 42, "status": "success", "ref": "main"}
1129

1230
with responses.RequestsMock() as rsps:
@@ -31,7 +49,42 @@ def resp_deployment():
3149
yield rsps
3250

3351

34-
def test_deployment(project, resp_deployment):
52+
@pytest.fixture
53+
def resp_deployment_approval():
54+
content = {
55+
"user": {
56+
"id": 100,
57+
"username": "security-user-1",
58+
"name": "security user-1",
59+
"state": "active",
60+
"avatar_url": "https://www.gravatar.com/avatar/e130fcd3a1681f41a3de69d10841afa9?s=80&d=identicon",
61+
"web_url": "http://localhost:3000/security-user-1",
62+
},
63+
"status": "approved",
64+
"created_at": "2022-02-24T20:22:30.097Z",
65+
"comment": "Looks good to me",
66+
}
67+
68+
with responses.RequestsMock() as rsps:
69+
rsps.add(
70+
method=responses.POST,
71+
url="http://localhost/api/v4/projects/1/deployments/42/approval",
72+
json=content,
73+
content_type="application/json",
74+
status=200,
75+
)
76+
yield rsps
77+
78+
79+
def test_deployment_get(project, resp_deployment_get):
80+
deployment = project.deployments.get(42)
81+
assert deployment.id == 42
82+
assert deployment.iid == 2
83+
assert deployment.status == "success"
84+
assert deployment.ref == "main"
85+
86+
87+
def test_deployment_create(project, resp_deployment_create):
3588
deployment = project.deployments.create(
3689
{
3790
"environment": "Test",
@@ -48,3 +101,80 @@ def test_deployment(project, resp_deployment):
48101
deployment.status = "failed"
49102
deployment.save()
50103
assert deployment.status == "failed"
104+
105+
106+
def test_deployment_approval(deployment, resp_deployment_approval) -> None:
107+
result = deployment.approval(status="approved")
108+
assert result["status"] == "approved"
109+
assert result["comment"] == "Looks good to me"
110+
111+
112+
response_get_content = {
113+
"id": 42,
114+
"iid": 2,
115+
"ref": "main",
116+
"sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
117+
"created_at": "2016-08-11T11:32:35.444Z",
118+
"updated_at": "2016-08-11T11:34:01.123Z",
119+
"status": "success",
120+
"user": {
121+
"name": "Administrator",
122+
"username": "root",
123+
"id": 1,
124+
"state": "active",
125+
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
126+
"web_url": "http://localhost:3000/root",
127+
},
128+
"environment": {
129+
"id": 9,
130+
"name": "production",
131+
"external_url": "https://about.gitlab.com",
132+
},
133+
"deployable": {
134+
"id": 664,
135+
"status": "success",
136+
"stage": "deploy",
137+
"name": "deploy",
138+
"ref": "main",
139+
"tag": False,
140+
"coverage": None,
141+
"created_at": "2016-08-11T11:32:24.456Z",
142+
"started_at": None,
143+
"finished_at": "2016-08-11T11:32:35.145Z",
144+
"user": {
145+
"id": 1,
146+
"name": "Administrator",
147+
"username": "root",
148+
"state": "active",
149+
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
150+
"web_url": "http://gitlab.dev/root",
151+
"created_at": "2015-12-21T13:14:24.077Z",
152+
"bio": None,
153+
"location": None,
154+
"skype": "",
155+
"linkedin": "",
156+
"twitter": "",
157+
"website_url": "",
158+
"organization": "",
159+
},
160+
"commit": {
161+
"id": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
162+
"short_id": "a91957a8",
163+
"title": "Merge branch 'rename-readme' into 'main'\r",
164+
"author_name": "Administrator",
165+
"author_email": "admin@example.com",
166+
"created_at": "2016-08-11T13:28:26.000+02:00",
167+
"message": "Merge branch 'rename-readme' into 'main'\r\n\r\nRename README\r\n\r\n\r\n\r\nSee merge request !2",
168+
},
169+
"pipeline": {
170+
"created_at": "2016-08-11T07:43:52.143Z",
171+
"id": 42,
172+
"ref": "main",
173+
"sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
174+
"status": "success",
175+
"updated_at": "2016-08-11T07:43:52.143Z",
176+
"web_url": "http://gitlab.dev/root/project/pipelines/5",
177+
},
178+
"runner": None,
179+
},
180+
}

0 commit comments

Comments
 (0)