qontract-reconcile 0.10.2.dev233__py3-none-any.whl → 0.10.2.dev234__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {qontract_reconcile-0.10.2.dev233.dist-info → qontract_reconcile-0.10.2.dev234.dist-info}/METADATA +2 -2
- {qontract_reconcile-0.10.2.dev233.dist-info → qontract_reconcile-0.10.2.dev234.dist-info}/RECORD +13 -13
- reconcile/change_owners/change_owners.py +1 -1
- reconcile/change_owners/decision.py +7 -10
- reconcile/gitlab_fork_compliance.py +1 -2
- reconcile/gitlab_housekeeping.py +25 -15
- reconcile/gitlab_members.py +1 -1
- reconcile/gitlab_owners.py +11 -8
- reconcile/utils/gitlab_api.py +86 -92
- reconcile/utils/vcs.py +5 -5
- tools/qontract_cli.py +12 -9
- {qontract_reconcile-0.10.2.dev233.dist-info → qontract_reconcile-0.10.2.dev234.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.2.dev233.dist-info → qontract_reconcile-0.10.2.dev234.dist-info}/entry_points.txt +0 -0
{qontract_reconcile-0.10.2.dev233.dist-info → qontract_reconcile-0.10.2.dev234.dist-info}/METADATA
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: qontract-reconcile
|
3
|
-
Version: 0.10.2.
|
3
|
+
Version: 0.10.2.dev234
|
4
4
|
Summary: Collection of tools to reconcile services with their desired state as defined in the app-interface DB.
|
5
5
|
Project-URL: homepage, https://github.com/app-sre/qontract-reconcile
|
6
6
|
Project-URL: repository, https://github.com/app-sre/qontract-reconcile
|
@@ -43,7 +43,7 @@ Requires-Dist: pygithub<1.59,>=1.58
|
|
43
43
|
Requires-Dist: pyjwt~=2.7
|
44
44
|
Requires-Dist: pyopenssl~=23.0
|
45
45
|
Requires-Dist: pypd<1.2.0,>=1.1.0
|
46
|
-
Requires-Dist: python-gitlab==
|
46
|
+
Requires-Dist: python-gitlab==6.0.0
|
47
47
|
Requires-Dist: requests-oauthlib~=1.3
|
48
48
|
Requires-Dist: requests~=2.32
|
49
49
|
Requires-Dist: rich<14.0.0,>=13.3.0
|
{qontract_reconcile-0.10.2.dev233.dist-info → qontract_reconcile-0.10.2.dev234.dist-info}/RECORD
RENAMED
@@ -27,12 +27,12 @@ reconcile/github_owners.py,sha256=viE1KJ-zaTxuZ5yItg2C263J0brn-Q-3hR_DkYDMbhY,31
|
|
27
27
|
reconcile/github_repo_invites.py,sha256=U9UCzNVwrZ7MqODtFah8ogH0NNY-XjBin7G9gqHtCUY,2690
|
28
28
|
reconcile/github_repo_permissions_validator.py,sha256=PNqL4dqa2OaNBy-NmLVN-t1HZa6eS6HgSYmfOunYqtA,1798
|
29
29
|
reconcile/github_validator.py,sha256=-j17tn3csFVjPMSPL3te48iWVkPZCncRXdeKeLdGjjQ,931
|
30
|
-
reconcile/gitlab_fork_compliance.py,sha256=
|
31
|
-
reconcile/gitlab_housekeeping.py,sha256=
|
30
|
+
reconcile/gitlab_fork_compliance.py,sha256=kRbHeVIqFkGIIxgPlS36jBtgCH4huWbSYK-bfyfahrU,4552
|
31
|
+
reconcile/gitlab_housekeeping.py,sha256=pZZn7Ww0FobqJrepVWoK44srh4W5P4WyAs3u81Xf4yM,26622
|
32
32
|
reconcile/gitlab_labeler.py,sha256=BA2dbXsN9hErUwJl22qcxfeH7XiPCuQ9LN3NddWdnpo,4540
|
33
|
-
reconcile/gitlab_members.py,sha256=
|
33
|
+
reconcile/gitlab_members.py,sha256=O_RR3sOgK0Sv8fXSC3QwV4wcCJyKRE2MzJRiaWEqTgQ,8294
|
34
34
|
reconcile/gitlab_mr_sqs_consumer.py,sha256=i_MDVfA3Uk_TJiNkfEJzhO6_rwR7z3I3dH9oEw686U4,2681
|
35
|
-
reconcile/gitlab_owners.py,sha256=
|
35
|
+
reconcile/gitlab_owners.py,sha256=C0Pzlsi9Z8o6JlpuvYOdYVBEHvPjS-1L4q6-E7s1agk,14503
|
36
36
|
reconcile/gitlab_permissions.py,sha256=Do8npcSK89dlA2T9LLdkhngTtv9ovmI4sohTFp7gglc,8373
|
37
37
|
reconcile/gitlab_projects.py,sha256=JIB1UP8CnwSkngEMZE7DFQETMX6sJMp4DXaKoS-Pdkc,1879
|
38
38
|
reconcile/integrations_manager.py,sha256=KKMui_zXHNbMT0o5jnW2JpDzIKuMPqBvamit3I84pDE,9313
|
@@ -167,10 +167,10 @@ reconcile/change_owners/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG
|
|
167
167
|
reconcile/change_owners/approver.py,sha256=Z3_11vnK2WNOxjEEXVDh0224-_-qbt9d6mBeVE-7fsc,2259
|
168
168
|
reconcile/change_owners/bundle.py,sha256=h30fU-JmLH5a-rCAovpzTeTkkkgZztsZ5A2raee0YuU,5355
|
169
169
|
reconcile/change_owners/change_log_tracking.py,sha256=lRHdC6-sqzUClmoVNq8v5rygAzeQJRxbKbJNDB3YI8E,9499
|
170
|
-
reconcile/change_owners/change_owners.py,sha256=
|
170
|
+
reconcile/change_owners/change_owners.py,sha256=U0KmAkDlzxmj4odk2sRrGotfEOq1hC_mb85iCwAzVD0,17105
|
171
171
|
reconcile/change_owners/change_types.py,sha256=5eSvS2_npUriq9RN4LdAWdYUiNzF91K1pDUEVYDIQ4k,32023
|
172
172
|
reconcile/change_owners/changes.py,sha256=YTqwUYutQ6JVSSYmC2Ph5ROCiVix42Vnzy47-i57z4Q,18119
|
173
|
-
reconcile/change_owners/decision.py,sha256=
|
173
|
+
reconcile/change_owners/decision.py,sha256=755rHmrnhfM_xVKnCPlLPOVm_TCJVb3lSkkUvxFM61Q,7491
|
174
174
|
reconcile/change_owners/diff.py,sha256=0vyu29xCL24ZhUa7hqBni0NaxoCYRXLwvA-h8V23YQ4,9009
|
175
175
|
reconcile/change_owners/implicit_ownership.py,sha256=6BehZvx4IjrphmOt_LLLk9_02Fl5BY5jd00Wuz_PBZk,4234
|
176
176
|
reconcile/change_owners/self_service_roles.py,sha256=xSe5AKZxXAIo0vWOMM5hImQ_rd-dQ38y_5dG5L6X0so,9655
|
@@ -611,7 +611,7 @@ reconcile/utils/external_resources.py,sha256=YzTb0xAcNdmKO326mGQy7BmST56CZcdru4l
|
|
611
611
|
reconcile/utils/filtering.py,sha256=S4PbMHuFr3ED0P2Q_ea5CAaB7FimI62B-F5YTaKrphA,402
|
612
612
|
reconcile/utils/git.py,sha256=o4p9m8jlzCJDcutl2HErvGLhL6sZ1NB4Aw3zGcQIzso,2427
|
613
613
|
reconcile/utils/github_api.py,sha256=S1vO-hvYPzm5BIychVIHSYibMns0HBmLgS78MkPfunE,3402
|
614
|
-
reconcile/utils/gitlab_api.py,sha256=
|
614
|
+
reconcile/utils/gitlab_api.py,sha256=0wJObojbXXk8Cgh8ymNWlwD1CdENmpsMo1zDSTddnoE,28335
|
615
615
|
reconcile/utils/gpg.py,sha256=EKG7_fdMv8BMlV5yUdPiqoTx-KrzmVSEAl2sLkaKwWI,1123
|
616
616
|
reconcile/utils/gql.py,sha256=C0thIm_k9MBldfqwHzyqtYZk9sIvMdm9IbbnXLGwjD8,14158
|
617
617
|
reconcile/utils/grouping.py,sha256=vr9SFHZ7bqmHYrvYcEZt-Er3-yQYfAAdq5sHLZVmXPY,456
|
@@ -667,7 +667,7 @@ reconcile/utils/three_way_diff_strategy.py,sha256=oQcHXd9LVhirJfoaOBoHUYuZVGfyL2
|
|
667
667
|
reconcile/utils/throughput.py,sha256=iP4UWAe2LVhDo69mPPmgo9nQ7RxHD6_GS8MZe-aSiuM,344
|
668
668
|
reconcile/utils/vault.py,sha256=aSA8l9cJlPUHpChFGl27nSY-Mpq9FMjBo7Dcgb1BVfM,15036
|
669
669
|
reconcile/utils/vaultsecretref.py,sha256=0KUSzuvTRxPyKY919TO3-B_eYg4_76fzKvMF8j5s1G0,911
|
670
|
-
reconcile/utils/vcs.py,sha256=
|
670
|
+
reconcile/utils/vcs.py,sha256=AtAGxhGRL7JcG3mmXN2qRE5Cw89c4I6cRAYQ8x3Fq7Y,10222
|
671
671
|
reconcile/utils/acs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
672
672
|
reconcile/utils/acs/base.py,sha256=4UsDrCpAOuddL3PKNuIQYoJP1BtZQNNB8_KEX0lXneg,2532
|
673
673
|
reconcile/utils/acs/policies.py,sha256=jpbi3qpGkBD_X6MfzsX12dPajUbmACmhIOz_0rDvYzs,5489
|
@@ -775,7 +775,7 @@ tools/app_sre_tekton_access_reporter.py,sha256=o9prLUgQpwO3msRWc2as1xT1y9OB3znkp
|
|
775
775
|
tools/app_sre_tekton_access_revalidation.py,sha256=66nHEaY-bIqxIhpcmwN8AvQZu6ZXenfkg4Fut0pVZRM,2726
|
776
776
|
tools/glitchtip_access_reporter.py,sha256=o01A6b88t3Wie6tj_tJWWVo2J01LxQ_a9giGm4UzEaU,2901
|
777
777
|
tools/glitchtip_access_revalidation.py,sha256=PXN5wxl6OX8sxddPaakDF3X79nFLvpm-lz0mWLVelw0,2806
|
778
|
-
tools/qontract_cli.py,sha256=
|
778
|
+
tools/qontract_cli.py,sha256=9TfODEGBzRjpTFp0UGPE_9iqj0LMBaNbT3DdXeqYLBg,159113
|
779
779
|
tools/template_validation.py,sha256=qpKYaTgk0GOPGa2Ct5_5sKdwIHtCAKIBGzsMPuJU5fw,3371
|
780
780
|
tools/cli_commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
781
781
|
tools/cli_commands/container_images_report.py,sha256=_8GuBsl_qYP5qfa5aa9krwyKHYu82MSh02deHEroSa4,5459
|
@@ -801,7 +801,7 @@ tools/saas_promotion_state/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJ
|
|
801
801
|
tools/saas_promotion_state/saas_promotion_state.py,sha256=UfwwRLS5Ya4_Nh1w5n1dvoYtchQvYE9yj1VANt2IKqI,3925
|
802
802
|
tools/sre_checkpoints/__init__.py,sha256=CDaDaywJnmRCLyl_NCcvxi-Zc0hTi_3OdwKiFOyS39I,145
|
803
803
|
tools/sre_checkpoints/util.py,sha256=zEDbGr18ZeHNQwW8pUsr2JRjuXIPz--WAGJxZo9sv_Y,894
|
804
|
-
qontract_reconcile-0.10.2.
|
805
|
-
qontract_reconcile-0.10.2.
|
806
|
-
qontract_reconcile-0.10.2.
|
807
|
-
qontract_reconcile-0.10.2.
|
804
|
+
qontract_reconcile-0.10.2.dev234.dist-info/METADATA,sha256=iNMIf9DSOc-sdEsqs0SNiNRMJkJ8Oy3g5h-s8CwN9p8,24352
|
805
|
+
qontract_reconcile-0.10.2.dev234.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
806
|
+
qontract_reconcile-0.10.2.dev234.dist-info/entry_points.txt,sha256=5i9l54La3vQrDLAdwDKQWC0iG4sV9RRfOb1BpvzOWLc,698
|
807
|
+
qontract_reconcile-0.10.2.dev234.dist-info/RECORD,,
|
@@ -382,7 +382,7 @@ def run(
|
|
382
382
|
|
383
383
|
comments = gl.get_merge_request_comments(merge_request)
|
384
384
|
good_to_test_approvers = {
|
385
|
-
c
|
385
|
+
c.username for c in comments if c.body.strip() == "/good-to-test"
|
386
386
|
}
|
387
387
|
|
388
388
|
change_admitted = is_change_admitted(
|
@@ -1,12 +1,8 @@
|
|
1
|
-
import operator
|
2
1
|
from collections import defaultdict
|
3
|
-
from collections.abc import
|
4
|
-
Iterable,
|
5
|
-
Mapping,
|
6
|
-
)
|
2
|
+
from collections.abc import Iterable
|
7
3
|
from dataclasses import dataclass
|
8
4
|
from enum import Enum
|
9
|
-
from
|
5
|
+
from operator import attrgetter
|
10
6
|
|
11
7
|
from reconcile.change_owners.approver import (
|
12
8
|
Approver,
|
@@ -16,6 +12,7 @@ from reconcile.change_owners.bundle import FileRef
|
|
16
12
|
from reconcile.change_owners.change_types import ChangeTypeContext, DiffCoverage
|
17
13
|
from reconcile.change_owners.changes import BundleFileChange
|
18
14
|
from reconcile.change_owners.diff import Diff
|
15
|
+
from reconcile.utils.gitlab_api import Comment
|
19
16
|
|
20
17
|
|
21
18
|
class DecisionCommand(Enum):
|
@@ -33,12 +30,12 @@ class Decision:
|
|
33
30
|
|
34
31
|
|
35
32
|
def get_approver_decisions_from_mr_comments(
|
36
|
-
comments: Iterable[
|
33
|
+
comments: Iterable[Comment],
|
37
34
|
) -> list[Decision]:
|
38
35
|
decisions: list[Decision] = []
|
39
|
-
for c in sorted(comments, key=
|
40
|
-
commenter = c
|
41
|
-
comment_body = c.
|
36
|
+
for c in sorted(comments, key=attrgetter("created_at")):
|
37
|
+
commenter = c.username
|
38
|
+
comment_body = c.body
|
42
39
|
for line in comment_body.split("\n") if comment_body else []:
|
43
40
|
line = line.strip()
|
44
41
|
if line == DecisionCommand.APPROVED.value:
|
@@ -77,8 +77,7 @@ class GitlabForkCompliance:
|
|
77
77
|
continue
|
78
78
|
LOG.info([f"adding {member.username} as maintainer"])
|
79
79
|
user_payload = {"user_id": member.id, "access_level": MAINTAINER_ACCESS}
|
80
|
-
|
81
|
-
member.save()
|
80
|
+
self.src.project.members.create(user_payload)
|
82
81
|
|
83
82
|
# Last but not least, we remove the blocked label, in case
|
84
83
|
# it is set
|
reconcile/gitlab_housekeeping.py
CHANGED
@@ -14,10 +14,12 @@ from operator import itemgetter
|
|
14
14
|
from typing import Any, cast
|
15
15
|
|
16
16
|
import gitlab
|
17
|
+
from gitlab.const import PipelineStatus
|
17
18
|
from gitlab.v4.objects import (
|
18
19
|
ProjectCommit,
|
19
20
|
ProjectIssue,
|
20
21
|
ProjectMergeRequest,
|
22
|
+
ProjectMergeRequestPipeline,
|
21
23
|
)
|
22
24
|
from prometheus_client import (
|
23
25
|
Counter,
|
@@ -133,11 +135,16 @@ def _calculate_time_since_approval(approved_at: str) -> float:
|
|
133
135
|
|
134
136
|
|
135
137
|
def get_timed_out_pipelines(
|
136
|
-
pipelines: list[
|
137
|
-
|
138
|
+
pipelines: list[ProjectMergeRequestPipeline],
|
139
|
+
pipeline_timeout: int = 60,
|
140
|
+
) -> list[ProjectMergeRequestPipeline]:
|
138
141
|
now = datetime.utcnow()
|
139
142
|
|
140
|
-
pending_pipelines = [
|
143
|
+
pending_pipelines = [
|
144
|
+
p
|
145
|
+
for p in pipelines
|
146
|
+
if p.status in {PipelineStatus.PENDING, PipelineStatus.RUNNING}
|
147
|
+
]
|
141
148
|
|
142
149
|
if not pending_pipelines:
|
143
150
|
return []
|
@@ -145,7 +152,7 @@ def get_timed_out_pipelines(
|
|
145
152
|
timed_out_pipelines = []
|
146
153
|
|
147
154
|
for p in pending_pipelines:
|
148
|
-
update_time = datetime.strptime(p
|
155
|
+
update_time = datetime.strptime(p.updated_at, DATE_FORMAT)
|
149
156
|
|
150
157
|
elapsed = (now - update_time).total_seconds()
|
151
158
|
|
@@ -160,20 +167,19 @@ def clean_pipelines(
|
|
160
167
|
dry_run: bool,
|
161
168
|
gl: GitLabApi,
|
162
169
|
fork_project_id: int,
|
163
|
-
pipelines: list[
|
170
|
+
pipelines: list[ProjectMergeRequestPipeline],
|
164
171
|
) -> None:
|
165
172
|
if not dry_run:
|
166
173
|
gl_piplelines = gl.get_project_by_id(fork_project_id).pipelines
|
167
174
|
|
168
175
|
for p in pipelines:
|
169
|
-
logging.info(["canceling", p
|
176
|
+
logging.info(["canceling", p.web_url])
|
170
177
|
if not dry_run:
|
171
178
|
try:
|
172
|
-
gl_piplelines.get(p
|
179
|
+
gl_piplelines.get(p.id, lazy=True).cancel()
|
173
180
|
except gitlab.exceptions.GitlabPipelineCancelError as err:
|
174
181
|
logging.error(
|
175
|
-
f"unable to cancel {p
|
176
|
-
f"error message {err.error_message}"
|
182
|
+
f"unable to cancel {p.web_url} - error message {err.error_message}"
|
177
183
|
)
|
178
184
|
|
179
185
|
|
@@ -188,9 +194,9 @@ def verify_on_demand_tests(
|
|
188
194
|
Check if MR has passed all necessary test jobs and add comments to indicate test results.
|
189
195
|
"""
|
190
196
|
pipelines = gl.get_merge_request_pipelines(mr)
|
191
|
-
running_pipelines = [p for p in pipelines if p
|
197
|
+
running_pipelines = [p for p in pipelines if p.status == PipelineStatus.RUNNING]
|
192
198
|
if running_pipelines:
|
193
|
-
# wait for pipelines
|
199
|
+
# wait for pipelines completion
|
194
200
|
return False
|
195
201
|
|
196
202
|
commit = next(mr.commits())
|
@@ -510,7 +516,9 @@ def rebase_merge_requests(
|
|
510
516
|
continue
|
511
517
|
# possible statuses:
|
512
518
|
# running, pending, success, failed, canceled, skipped
|
513
|
-
running_pipelines = [
|
519
|
+
running_pipelines = [
|
520
|
+
p for p in pipelines if p.status == PipelineStatus.RUNNING
|
521
|
+
]
|
514
522
|
if running_pipelines:
|
515
523
|
continue
|
516
524
|
|
@@ -591,7 +599,9 @@ def merge_merge_requests(
|
|
591
599
|
if wait_for_pipeline:
|
592
600
|
# possible statuses:
|
593
601
|
# running, pending, success, failed, canceled, skipped
|
594
|
-
running_pipelines = [
|
602
|
+
running_pipelines = [
|
603
|
+
p for p in pipelines if p.status == PipelineStatus.RUNNING
|
604
|
+
]
|
595
605
|
if running_pipelines:
|
596
606
|
if insist:
|
597
607
|
# This raise causes the method to restart due to the usage of
|
@@ -606,8 +616,8 @@ def merge_merge_requests(
|
|
606
616
|
)
|
607
617
|
continue
|
608
618
|
|
609
|
-
last_pipeline_result = pipelines[0]
|
610
|
-
if last_pipeline_result !=
|
619
|
+
last_pipeline_result = pipelines[0].status
|
620
|
+
if last_pipeline_result != PipelineStatus.SUCCESS:
|
611
621
|
continue
|
612
622
|
|
613
623
|
logging.info(["merge", gl.project.name, mr.iid])
|
reconcile/gitlab_members.py
CHANGED
@@ -216,7 +216,7 @@ def reconcile_gitlab_members(
|
|
216
216
|
gl.get_access_level_string(gitlab_user.access_level),
|
217
217
|
])
|
218
218
|
if not dry_run:
|
219
|
-
gl.add_group_member(group, gitlab_user)
|
219
|
+
gl.add_group_member(group, gitlab_user.user, gitlab_user.access_level)
|
220
220
|
for key, group_member in diff_data.delete.items():
|
221
221
|
logging.info([
|
222
222
|
key,
|
reconcile/gitlab_owners.py
CHANGED
@@ -89,19 +89,19 @@ class MRApproval:
|
|
89
89
|
comments = self.gitlab.get_merge_request_comments(self.mr)
|
90
90
|
for comment in comments:
|
91
91
|
# Only interested in '/lgtm' comments
|
92
|
-
if comment
|
92
|
+
if comment.body != "/lgtm":
|
93
93
|
continue
|
94
94
|
|
95
95
|
# Only interested in comments created after the top commit
|
96
96
|
# creation time
|
97
|
-
comment_created_at = dateparser.parse(comment
|
97
|
+
comment_created_at = dateparser.parse(comment.created_at)
|
98
98
|
if (
|
99
99
|
comment_created_at < self.top_commit_created_at
|
100
100
|
and not self.persistent_lgtm
|
101
101
|
):
|
102
102
|
continue
|
103
103
|
|
104
|
-
lgtms.append(comment
|
104
|
+
lgtms.append(comment.username)
|
105
105
|
return lgtms
|
106
106
|
|
107
107
|
def expand_groups(self, owners: list[str]) -> set[str]:
|
@@ -173,19 +173,22 @@ class MRApproval:
|
|
173
173
|
comments = self.gitlab.get_merge_request_comments(self.mr)
|
174
174
|
for comment in comments:
|
175
175
|
# Only interested on our own comments
|
176
|
-
if comment
|
176
|
+
if comment.username != self.gitlab.user.username:
|
177
177
|
continue
|
178
178
|
|
179
179
|
# Ignoring non-approval comments
|
180
|
-
body = comment
|
180
|
+
body = comment.body
|
181
181
|
if not body.startswith(COMMENT_PREFIX):
|
182
182
|
continue
|
183
183
|
|
184
184
|
# If the comment was created before the last commit,
|
185
185
|
# it means we had a push after the comment. In this case,
|
186
186
|
# we delete the comment and move on.
|
187
|
-
comment_created_at = dateparser.parse(comment
|
188
|
-
if
|
187
|
+
comment_created_at = dateparser.parse(comment.created_at)
|
188
|
+
if (
|
189
|
+
comment_created_at < self.top_commit_created_at
|
190
|
+
and comment.note is not None
|
191
|
+
):
|
189
192
|
# Deleting stale comments
|
190
193
|
_LOG.info([
|
191
194
|
f"Project:{self.gitlab.project.id} "
|
@@ -193,7 +196,7 @@ class MRApproval:
|
|
193
196
|
f"- removing stale comment"
|
194
197
|
])
|
195
198
|
if not self.dry_run:
|
196
|
-
self.gitlab.delete_comment(comment
|
199
|
+
self.gitlab.delete_comment(comment.note)
|
197
200
|
continue
|
198
201
|
|
199
202
|
# At this point, we've found an approval comment comment
|
reconcile/utils/gitlab_api.py
CHANGED
@@ -7,16 +7,12 @@ from collections.abc import (
|
|
7
7
|
Iterable,
|
8
8
|
Mapping,
|
9
9
|
)
|
10
|
+
from dataclasses import dataclass
|
10
11
|
from functools import cached_property
|
11
|
-
from operator import
|
12
|
-
attrgetter,
|
13
|
-
itemgetter,
|
14
|
-
)
|
12
|
+
from operator import attrgetter
|
15
13
|
from typing import (
|
16
14
|
Any,
|
17
|
-
Protocol,
|
18
15
|
Self,
|
19
|
-
TypedDict,
|
20
16
|
cast,
|
21
17
|
)
|
22
18
|
from urllib.parse import urlparse
|
@@ -47,6 +43,7 @@ from gitlab.v4.objects import (
|
|
47
43
|
ProjectMergeRequest,
|
48
44
|
ProjectMergeRequestManager,
|
49
45
|
ProjectMergeRequestNote,
|
46
|
+
ProjectMergeRequestPipeline,
|
50
47
|
ProjectMergeRequestResourceLabelEvent,
|
51
48
|
User,
|
52
49
|
)
|
@@ -97,15 +94,19 @@ class MRStatus:
|
|
97
94
|
GROUP_BOT_NAME_REGEX = re.compile(r"group_.+_bot_.+")
|
98
95
|
|
99
96
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
97
|
+
@dataclass(frozen=True)
|
98
|
+
class Assignment:
|
99
|
+
author: str
|
100
|
+
assignee: str
|
104
101
|
|
105
102
|
|
106
|
-
|
107
|
-
|
108
|
-
|
103
|
+
@dataclass(frozen=True)
|
104
|
+
class Comment:
|
105
|
+
id: int
|
106
|
+
username: str
|
107
|
+
body: str
|
108
|
+
created_at: str
|
109
|
+
note: ProjectMergeRequestNote | None = None
|
109
110
|
|
110
111
|
|
111
112
|
class GitLabApi:
|
@@ -254,7 +255,7 @@ class GitLabApi:
|
|
254
255
|
"remove_source_branch": str(remove_source_branch),
|
255
256
|
"labels": labels,
|
256
257
|
}
|
257
|
-
return
|
258
|
+
return self.project.mergerequests.create(data)
|
258
259
|
|
259
260
|
def mr_exists(self, title: str) -> bool:
|
260
261
|
mrs = self.get_merge_requests(state=MRState.OPENED)
|
@@ -274,10 +275,7 @@ class GitLabApi:
|
|
274
275
|
|
275
276
|
def get_app_sre_group_users(self) -> list[GroupMember]:
|
276
277
|
app_sre_group = self.gl.groups.get("app-sre")
|
277
|
-
return
|
278
|
-
list[GroupMember],
|
279
|
-
app_sre_group.members.list(get_all=True),
|
280
|
-
)
|
278
|
+
return app_sre_group.members.list(get_all=True)
|
281
279
|
|
282
280
|
def get_group_if_exists(self, group_name: str) -> Group | None:
|
283
281
|
try:
|
@@ -312,7 +310,7 @@ class GitLabApi:
|
|
312
310
|
|
313
311
|
def get_group_members(self, group: Group) -> list[GroupMember]:
|
314
312
|
return [
|
315
|
-
|
313
|
+
m
|
316
314
|
for m in group.members.list(iterator=True)
|
317
315
|
if not self._is_bot_username(m.username)
|
318
316
|
]
|
@@ -331,17 +329,17 @@ class GitLabApi:
|
|
331
329
|
member.access_level = access_level
|
332
330
|
member.save()
|
333
331
|
|
334
|
-
def add_group_member(self, group: Group,
|
335
|
-
gitlab_user = self.get_user(
|
332
|
+
def add_group_member(self, group: Group, username: str, access_level: int) -> None:
|
333
|
+
gitlab_user = self.get_user(username)
|
336
334
|
if gitlab_user is not None:
|
337
335
|
try:
|
338
336
|
group.members.create({
|
339
337
|
"user_id": gitlab_user.id,
|
340
|
-
"access_level":
|
338
|
+
"access_level": access_level,
|
341
339
|
})
|
342
340
|
except GitlabCreateError:
|
343
|
-
member = group.members.get(
|
344
|
-
member.access_level =
|
341
|
+
member = group.members.get(gitlab_user.id)
|
342
|
+
member.access_level = access_level
|
345
343
|
member.save()
|
346
344
|
|
347
345
|
def remove_group_member(self, group: Group, user_id: str) -> None:
|
@@ -398,37 +396,27 @@ class GitLabApi:
|
|
398
396
|
return self.gl.projects.get(project_id)
|
399
397
|
|
400
398
|
def get_issues(self, state: str) -> list[ProjectIssue]:
|
401
|
-
return
|
402
|
-
list[ProjectIssue],
|
403
|
-
self.project.issues.list(state=state, get_all=True),
|
404
|
-
)
|
399
|
+
return self.project.issues.list(state=state, get_all=True)
|
405
400
|
|
406
401
|
def get_merge_request(self, mr_id: str | int) -> ProjectMergeRequest:
|
407
402
|
return self.project.mergerequests.get(mr_id)
|
408
403
|
|
409
404
|
def get_merge_requests(self, state: str) -> list[ProjectMergeRequest]:
|
410
|
-
return
|
411
|
-
list[ProjectMergeRequest],
|
412
|
-
self.project.mergerequests.list(state=state, get_all=True),
|
413
|
-
)
|
405
|
+
return self.project.mergerequests.list(state=state, get_all=True)
|
414
406
|
|
415
407
|
@staticmethod
|
416
408
|
def get_merge_request_label_events(
|
417
409
|
mr: ProjectMergeRequest,
|
418
410
|
) -> list[ProjectMergeRequestResourceLabelEvent]:
|
419
|
-
return
|
420
|
-
list[ProjectMergeRequestResourceLabelEvent],
|
421
|
-
mr.resourcelabelevents.list(get_all=True),
|
422
|
-
)
|
411
|
+
return mr.resourcelabelevents.list(get_all=True)
|
423
412
|
|
424
413
|
@staticmethod
|
425
|
-
def get_merge_request_pipelines(
|
426
|
-
|
427
|
-
|
428
|
-
items = mr.pipelines.list(iterator=True)
|
414
|
+
def get_merge_request_pipelines(
|
415
|
+
mr: ProjectMergeRequest,
|
416
|
+
) -> list[ProjectMergeRequestPipeline]:
|
429
417
|
return sorted(
|
430
|
-
|
431
|
-
key=
|
418
|
+
mr.pipelines.list(iterator=True),
|
419
|
+
key=attrgetter("created_at"),
|
432
420
|
reverse=True,
|
433
421
|
)
|
434
422
|
|
@@ -456,25 +444,28 @@ class GitLabApi:
|
|
456
444
|
def get_merge_request_comments(
|
457
445
|
merge_request: ProjectMergeRequest,
|
458
446
|
include_description: bool = False,
|
459
|
-
) -> list[
|
447
|
+
) -> list[Comment]:
|
460
448
|
comments = []
|
461
449
|
if include_description:
|
462
|
-
comments.append(
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
"
|
474
|
-
"
|
475
|
-
|
476
|
-
|
477
|
-
|
450
|
+
comments.append(
|
451
|
+
Comment(
|
452
|
+
id=MR_DESCRIPTION_COMMENT_ID,
|
453
|
+
username=merge_request.author["username"],
|
454
|
+
body=merge_request.description or "",
|
455
|
+
created_at=merge_request.created_at,
|
456
|
+
)
|
457
|
+
)
|
458
|
+
comments.extend(
|
459
|
+
Comment(
|
460
|
+
id=note.id,
|
461
|
+
username=note.author["username"],
|
462
|
+
body=note.body or "",
|
463
|
+
created_at=note.created_at,
|
464
|
+
note=note,
|
465
|
+
)
|
466
|
+
for note in merge_request.notes.list(iterator=True)
|
467
|
+
if not note.system
|
468
|
+
)
|
478
469
|
return comments
|
479
470
|
|
480
471
|
@staticmethod
|
@@ -488,9 +479,12 @@ class GitLabApi:
|
|
488
479
|
) -> None:
|
489
480
|
comments = self.get_merge_request_comments(merge_request)
|
490
481
|
for c in comments:
|
491
|
-
|
492
|
-
|
493
|
-
|
482
|
+
if (
|
483
|
+
c.username == self.user.username
|
484
|
+
and c.body.startswith(startswith)
|
485
|
+
and c.note is not None
|
486
|
+
):
|
487
|
+
self.delete_comment(c.note)
|
494
488
|
|
495
489
|
@retry()
|
496
490
|
def get_project_labels(self) -> set[str]:
|
@@ -631,7 +625,7 @@ class GitLabApi:
|
|
631
625
|
item.save()
|
632
626
|
|
633
627
|
def get_user(self, username: str) -> User | None:
|
634
|
-
user =
|
628
|
+
user = self.gl.users.list(search=username, page=1, per_page=1)
|
635
629
|
if not user:
|
636
630
|
logging.error(f"{username} user not found")
|
637
631
|
return None
|
@@ -732,25 +726,22 @@ class GitLabApi:
|
|
732
726
|
self.create_branch("production", "master")
|
733
727
|
|
734
728
|
def is_last_action_by_team(
|
735
|
-
self, mr: ProjectMergeRequest, team_usernames:
|
729
|
+
self, mr: ProjectMergeRequest, team_usernames: set[str], hold_labels: list[str]
|
736
730
|
) -> bool:
|
737
731
|
# what is the time of the last app-sre response?
|
738
732
|
last_action_by_team = None
|
739
733
|
# comments
|
740
734
|
comments = self.get_merge_request_comments(mr)
|
741
|
-
comments.sort(key=
|
735
|
+
comments.sort(key=attrgetter("created_at"), reverse=True)
|
742
736
|
for comment in comments:
|
743
|
-
username = comment
|
737
|
+
username = comment.username
|
744
738
|
if username == self.user.username:
|
745
739
|
continue
|
746
740
|
if username in team_usernames:
|
747
|
-
last_action_by_team = comment
|
741
|
+
last_action_by_team = comment.created_at
|
748
742
|
break
|
749
743
|
# labels
|
750
|
-
label_events =
|
751
|
-
list[ProjectMergeRequestResourceLabelEvent],
|
752
|
-
mr.resourcelabelevents.list(get_all=True),
|
753
|
-
)
|
744
|
+
label_events = mr.resourcelabelevents.list(get_all=True)
|
754
745
|
for label in reversed(label_events):
|
755
746
|
if label.action == "add" and label.label["name"] in hold_labels:
|
756
747
|
username = label.user["username"]
|
@@ -774,11 +765,11 @@ class GitLabApi:
|
|
774
765
|
break
|
775
766
|
# comments
|
776
767
|
for comment in comments:
|
777
|
-
username = comment
|
768
|
+
username = comment.username
|
778
769
|
if username == self.user.username:
|
779
770
|
continue
|
780
771
|
if username not in team_usernames:
|
781
|
-
last_action_not_by_team = comment
|
772
|
+
last_action_not_by_team = comment.created_at
|
782
773
|
break
|
783
774
|
|
784
775
|
if not last_action_not_by_team:
|
@@ -787,19 +778,20 @@ class GitLabApi:
|
|
787
778
|
return last_action_not_by_team < last_action_by_team
|
788
779
|
|
789
780
|
def is_assigned_by_team(
|
790
|
-
self, mr: ProjectMergeRequest, team_usernames:
|
781
|
+
self, mr: ProjectMergeRequest, team_usernames: set[str]
|
791
782
|
) -> bool:
|
792
783
|
if not mr.assignee:
|
793
784
|
return False
|
794
785
|
last_assignment = self.last_assignment(mr)
|
795
|
-
if
|
786
|
+
if last_assignment is None:
|
796
787
|
return False
|
797
|
-
|
798
|
-
|
799
|
-
|
788
|
+
return (
|
789
|
+
last_assignment.author in team_usernames
|
790
|
+
and mr.assignee["username"] == last_assignment.assignee
|
791
|
+
)
|
800
792
|
|
801
793
|
@staticmethod
|
802
|
-
def last_assignment(mr: ProjectMergeRequest) ->
|
794
|
+
def last_assignment(mr: ProjectMergeRequest) -> Assignment | None:
|
803
795
|
"""
|
804
796
|
Get the last assignment of a merge request.
|
805
797
|
:param mr: merge request
|
@@ -809,7 +801,10 @@ class GitLabApi:
|
|
809
801
|
notes = mr.notes.list(sort="desc", order_by="created_at", iterator=True)
|
810
802
|
return next(
|
811
803
|
(
|
812
|
-
(
|
804
|
+
Assignment(
|
805
|
+
author=note.author["username"],
|
806
|
+
assignee=note.body.removeprefix(body_format),
|
807
|
+
)
|
813
808
|
for note in notes
|
814
809
|
if note.system and note.body.startswith(body_format)
|
815
810
|
),
|
@@ -818,15 +813,17 @@ class GitLabApi:
|
|
818
813
|
|
819
814
|
def last_comment(
|
820
815
|
self, mr: ProjectMergeRequest, exclude_bot: bool = True
|
821
|
-
) ->
|
816
|
+
) -> Comment | None:
|
822
817
|
comments = self.get_merge_request_comments(mr)
|
823
|
-
comments.sort(key=
|
824
|
-
|
825
|
-
|
826
|
-
|
827
|
-
|
828
|
-
|
829
|
-
|
818
|
+
comments.sort(key=attrgetter("created_at"), reverse=True)
|
819
|
+
return next(
|
820
|
+
(
|
821
|
+
comment
|
822
|
+
for comment in comments
|
823
|
+
if not (exclude_bot and comment.username == self.user.username)
|
824
|
+
),
|
825
|
+
None,
|
826
|
+
)
|
830
827
|
|
831
828
|
def get_commit_sha(self, ref: str, repo_url: str) -> str:
|
832
829
|
project = self.get_project(repo_url)
|
@@ -841,10 +838,7 @@ class GitLabApi:
|
|
841
838
|
return response.get("commits", [])
|
842
839
|
|
843
840
|
def get_personal_access_tokens(self) -> list[PersonalAccessToken]:
|
844
|
-
return
|
845
|
-
list[PersonalAccessToken],
|
846
|
-
self.gl.personal_access_tokens.list(get_all=True),
|
847
|
-
)
|
841
|
+
return self.gl.personal_access_tokens.list(get_all=True)
|
848
842
|
|
849
843
|
@staticmethod
|
850
844
|
def get_directory_contents(
|
reconcile/utils/vcs.py
CHANGED
@@ -9,6 +9,7 @@ from enum import Enum
|
|
9
9
|
from typing import Literal
|
10
10
|
from urllib.parse import urlparse
|
11
11
|
|
12
|
+
from gitlab.const import PipelineStatus
|
12
13
|
from gitlab.v4.objects import ProjectMergeRequest
|
13
14
|
|
14
15
|
from reconcile.typed_queries.github_orgs import GithubOrgV1
|
@@ -154,14 +155,13 @@ class VCS:
|
|
154
155
|
pipelines = self._gitlab_instance.get_merge_request_pipelines(mr)
|
155
156
|
if not pipelines:
|
156
157
|
return MRCheckStatus.NONE
|
157
|
-
|
158
|
-
last_pipeline_result = pipelines[0]["status"]
|
158
|
+
last_pipeline_result = pipelines[0].status
|
159
159
|
match last_pipeline_result:
|
160
|
-
case
|
160
|
+
case PipelineStatus.SUCCESS:
|
161
161
|
return MRCheckStatus.SUCCESS
|
162
|
-
case
|
162
|
+
case PipelineStatus.RUNNING:
|
163
163
|
return MRCheckStatus.RUNNING
|
164
|
-
case
|
164
|
+
case PipelineStatus.FAILED:
|
165
165
|
return MRCheckStatus.FAILED
|
166
166
|
case _:
|
167
167
|
# Lets assume all other states as non-present
|
tools/qontract_cli.py
CHANGED
@@ -27,6 +27,7 @@ import click
|
|
27
27
|
import click.core
|
28
28
|
import requests
|
29
29
|
import yaml
|
30
|
+
from gitlab.const import PipelineStatus
|
30
31
|
from rich import box
|
31
32
|
from rich import print as rich_print
|
32
33
|
from rich.console import Console, Group
|
@@ -2331,15 +2332,17 @@ def app_interface_review_queue(ctx: click.Context) -> None:
|
|
2331
2332
|
pipelines = gl.get_merge_request_pipelines(mr)
|
2332
2333
|
if not pipelines:
|
2333
2334
|
continue
|
2334
|
-
running_pipelines = [
|
2335
|
+
running_pipelines = [
|
2336
|
+
p for p in pipelines if p.status == PipelineStatus.RUNNING
|
2337
|
+
]
|
2335
2338
|
if running_pipelines:
|
2336
2339
|
continue
|
2337
|
-
last_pipeline_result = pipelines[0]
|
2338
|
-
if last_pipeline_result !=
|
2340
|
+
last_pipeline_result = pipelines[0].status
|
2341
|
+
if last_pipeline_result != PipelineStatus.SUCCESS:
|
2339
2342
|
continue
|
2340
2343
|
|
2341
2344
|
author = mr.author["username"]
|
2342
|
-
app_sre_team_members =
|
2345
|
+
app_sre_team_members = {u.username for u in gl.get_app_sre_group_users()}
|
2343
2346
|
if author in app_sre_team_members:
|
2344
2347
|
continue
|
2345
2348
|
|
@@ -2357,7 +2360,7 @@ def app_interface_review_queue(ctx: click.Context) -> None:
|
|
2357
2360
|
if (
|
2358
2361
|
last_comment
|
2359
2362
|
and trigger_phrases_regex
|
2360
|
-
and not re.fullmatch(trigger_phrases_regex, last_comment
|
2363
|
+
and not re.fullmatch(trigger_phrases_regex, last_comment.body)
|
2361
2364
|
):
|
2362
2365
|
continue
|
2363
2366
|
|
@@ -2417,7 +2420,7 @@ def app_interface_open_selfserviceable_mr_queue(ctx: click.Context) -> None:
|
|
2417
2420
|
|
2418
2421
|
# skip MRs where AppSRE is involved already (author or assignee)
|
2419
2422
|
author = mr.author["username"]
|
2420
|
-
app_sre_team_members =
|
2423
|
+
app_sre_team_members = {u.username for u in gl.get_app_sre_group_users()}
|
2421
2424
|
if author in app_sre_team_members:
|
2422
2425
|
continue
|
2423
2426
|
is_assigned_by_app_sre = gl.is_assigned_by_team(mr, app_sre_team_members)
|
@@ -2428,11 +2431,11 @@ def app_interface_open_selfserviceable_mr_queue(ctx: click.Context) -> None:
|
|
2428
2431
|
pipelines = gl.get_merge_request_pipelines(mr)
|
2429
2432
|
if not pipelines:
|
2430
2433
|
continue
|
2431
|
-
running_pipelines = [p for p in pipelines if p
|
2434
|
+
running_pipelines = [p for p in pipelines if p.status == PipelineStatus.RUNNING]
|
2432
2435
|
if running_pipelines:
|
2433
2436
|
continue
|
2434
|
-
last_pipeline_result = pipelines[0]
|
2435
|
-
if last_pipeline_result !=
|
2437
|
+
last_pipeline_result = pipelines[0].status
|
2438
|
+
if last_pipeline_result != PipelineStatus.SUCCESS:
|
2436
2439
|
continue
|
2437
2440
|
|
2438
2441
|
item = {
|
{qontract_reconcile-0.10.2.dev233.dist-info → qontract_reconcile-0.10.2.dev234.dist-info}/WHEEL
RENAMED
File without changes
|
File without changes
|