12
12
from Tests .scripts .utils import logging_wrapper as logging
13
13
from gitlab .v4 .objects .pipelines import ProjectPipeline
14
14
from gitlab .v4 .objects .commits import ProjectCommit
15
+ from itertools import pairwise
15
16
16
17
17
18
CONTENT_NIGHTLY = 'Content Nightly'
@@ -255,65 +256,73 @@ def get_pipelines_and_commits(gitlab_client: Gitlab, project_id,
255
256
return pipelines , commits
256
257
257
258
258
- def get_person_in_charge (commit ) :
259
+ def get_person_in_charge (commit : ProjectCommit ) -> tuple [ str , str , str ] | tuple [ None , None , None ] :
259
260
"""
260
- Returns the name, email, and PR link for the author of the provided commit .
261
+ Returns the name of the person in charge of the commit, the PR link and the beginning of the PR name .
261
262
262
263
Args:
263
264
commit: The Gitlab commit object containing author info.
264
265
265
266
Returns:
266
267
name: The name of the commit author.
267
268
pr: The GitHub PR link for the Gitlab commit.
269
+ beginning_of_pr_name: The beginning of the PR name.
268
270
"""
269
271
name = commit .author_name
270
272
# pr number is always the last id in the commit title, starts with a number sign, may or may not be in parenthesis.
271
273
pr_number = commit .title .split ("#" )[- 1 ].strip ("()" )
274
+ beginning_of_pr_name = commit .title [:20 ] + "..."
272
275
if pr_number .isnumeric ():
273
276
pr = f"https://github.com/demisto/content/pull/{ pr_number } "
274
- return name , pr
277
+ return name , pr , beginning_of_pr_name
275
278
else :
276
- return None , None
279
+ return None , None , None
277
280
278
281
279
- def are_pipelines_in_order (current_pipeline : ProjectPipeline , previous_pipeline : ProjectPipeline ) -> bool :
282
+ def are_pipelines_in_order (pipeline_a : ProjectPipeline , pipeline_b : ProjectPipeline ) -> bool :
280
283
"""
281
- This function checks if the current pipeline was created after the previous pipeline, to avoid rare conditions
282
- that pipelines are not in the same order as the commits.
284
+ Check if the pipelines are in the same order of their commits.
283
285
Args:
284
- current_pipeline : The current pipeline object.
285
- previous_pipeline : The previous pipeline object.
286
+ pipeline_a : The first pipeline object.
287
+ pipeline_b : The second pipeline object.
286
288
Returns:
287
289
bool
288
290
"""
289
291
290
- previous_pipeline_timestamp = parser .parse (previous_pipeline .created_at )
291
- current_pipeline_timestamp = parser .parse (current_pipeline .created_at )
292
- return current_pipeline_timestamp > previous_pipeline_timestamp
292
+ pipeline_a_timestamp = parser .parse (pipeline_a .created_at )
293
+ pipeline_b_timestamp = parser .parse (pipeline_b .created_at )
294
+ return pipeline_a_timestamp > pipeline_b_timestamp
293
295
294
296
295
- def is_pivot (current_pipeline : ProjectPipeline , previous_pipeline : ProjectPipeline ) -> bool | None :
297
+ def is_pivot (current_pipeline : ProjectPipeline , pipeline_to_compare : ProjectPipeline ) -> bool | None :
296
298
"""
297
299
Is the current pipeline status a pivot from the previous pipeline status.
298
300
Args:
299
301
current_pipeline: The current pipeline object.
300
- previous_pipeline: The previous pipeline object.
302
+ pipeline_to_compare: a pipeline object to compare to .
301
303
Returns:
302
304
True status changed from success to failed
303
305
False if the status changed from failed to success
304
306
None if the status didn't change or the pipelines are not in order of commits
305
307
"""
306
308
307
- in_order = are_pipelines_in_order (current_pipeline , previous_pipeline )
309
+ in_order = are_pipelines_in_order (pipeline_a = current_pipeline , pipeline_b = pipeline_to_compare )
308
310
if in_order :
309
- if previous_pipeline .status == 'success' and current_pipeline .status == 'failed' :
311
+ if pipeline_to_compare .status == 'success' and current_pipeline .status == 'failed' :
310
312
return True
311
- if previous_pipeline .status == 'failed' and current_pipeline .status == 'success' :
313
+ if pipeline_to_compare .status == 'failed' and current_pipeline .status == 'success' :
312
314
return False
313
315
return None
314
316
315
317
316
318
def get_reviewer (pr_url : str ) -> str | None :
319
+ """
320
+ Get the first reviewer who approved the PR.
321
+ Args:
322
+ pr_url: The URL of the PR.
323
+ Returns:
324
+ The name of the first reviewer who approved the PR.
325
+ """
317
326
approved_reviewer = None
318
327
try :
319
328
# Extract the owner, repo, and pull request number from the URL
@@ -335,6 +344,14 @@ def get_reviewer(pr_url: str) -> str | None:
335
344
336
345
337
346
def get_slack_user_name (name : str | None , name_mapping_path : str ) -> str :
347
+ """
348
+ Get the slack user name for a given Github name.
349
+ Args:
350
+ name: The name to convert.
351
+ name_mapping_path: The path to the name mapping file.
352
+ Returns:
353
+ The slack user name.
354
+ """
338
355
with open (name_mapping_path ) as map :
339
356
mapping = json .load (map )
340
357
# If the name is the name of the 'docker image update bot' reviewer - return the owner of that bot.
@@ -345,30 +362,131 @@ def get_slack_user_name(name: str | None, name_mapping_path: str) -> str:
345
362
346
363
347
364
def get_commit_by_sha (commit_sha : str , list_of_commits : list [ProjectCommit ]) -> ProjectCommit | None :
365
+ """
366
+ Get a commit by its SHA.
367
+ Args:
368
+ commit_sha: The SHA of the commit.
369
+ list_of_commits: A list of commits.
370
+ Returns:
371
+ The commit object.
372
+ """
348
373
return next ((commit for commit in list_of_commits if commit .id == commit_sha ), None )
349
374
350
375
351
376
def get_pipeline_by_commit (commit : ProjectCommit , list_of_pipelines : list [ProjectPipeline ]) -> ProjectPipeline | None :
377
+ """
378
+ Get a pipeline by its commit.
379
+ Args:
380
+ commit: The commit object.
381
+ list_of_pipelines: A list of pipelines.
382
+ Returns:
383
+ The pipeline object.
384
+ """
352
385
return next ((pipeline for pipeline in list_of_pipelines if pipeline .sha == commit .id ), None )
353
386
354
387
355
- def create_shame_message (current_commit : ProjectCommit ,
356
- pipeline_changed_status : bool , name_mapping_path : str ) -> tuple [str , str , str ] | None :
388
+ def create_shame_message (suspicious_commits : list [ ProjectCommit ] ,
389
+ pipeline_changed_status : bool , name_mapping_path : str ) -> tuple [str , str , str , str ] | None :
357
390
"""
358
- Create a shame message for the person in charge of the commit.
391
+ Create a shame message for the person in charge of the commit, or for multiple people in case of multiple suspicious commits.
392
+ Args:
393
+ suspicious_commits: A list of suspicious commits.
394
+ pipeline_changed_status: A boolean indicating if the pipeline status changed.
395
+ name_mapping_path: The path to the name mapping file.
396
+ Returns:
397
+ A tuple of strings containing the message, the person in charge, the PR link and the color of the message.
359
398
"""
360
- name , pr = get_person_in_charge (current_commit )
361
- if name and pr :
362
- if name == CONTENT_BOT :
363
- name = get_reviewer (pr )
364
- name = get_slack_user_name (name , name_mapping_path )
365
- msg = "broke" if pipeline_changed_status else "fixed"
366
- color = "danger" if pipeline_changed_status else "good"
367
- emoji = ":cry:" if pipeline_changed_status else ":muscle:"
368
- return (f"Hi @{ name } , You { msg } the build! { emoji } " ,
369
- f" That was done in this { slack_link (pr ,'PR.' )} " , color )
370
- return None
399
+ hi_and_status = person_in_charge = in_this_pr = color = ""
400
+ for suspicious_commit in suspicious_commits :
401
+ name , pr , beginning_of_pr = get_person_in_charge (suspicious_commit )
402
+ if name and pr and beginning_of_pr :
403
+ if name == CONTENT_BOT :
404
+ name = get_reviewer (pr )
405
+ name = get_slack_user_name (name , name_mapping_path )
406
+ msg = "broken" if pipeline_changed_status else "fixed"
407
+ color = "danger" if pipeline_changed_status else "good"
408
+ emoji = ":cry:" if pipeline_changed_status else ":muscle:"
409
+ if suspicious_commits .index (suspicious_commit ) == 0 :
410
+ hi_and_status = f"Hi, The build was { msg } { emoji } by:"
411
+ person_in_charge = f"@{ name } "
412
+ in_this_pr = f" That was done in this PR: { slack_link (pr , beginning_of_pr )} "
413
+
414
+ else :
415
+ person_in_charge += f" or @{ name } "
416
+ in_this_pr = ""
417
+
418
+ return (hi_and_status , person_in_charge , in_this_pr , color ) if hi_and_status and person_in_charge and color else None
371
419
372
420
373
421
def slack_link (url : str , text : str ) -> str :
422
+ """
423
+ Create a slack link.
424
+ Args:
425
+ url: The URL to link to.
426
+ text: The text to display.
427
+ Returns:
428
+ The slack link.
429
+ """
374
430
return f"<{ url } |{ text } >"
431
+
432
+
433
+ def was_message_already_sent (commit_index : int , list_of_commits : list , list_of_pipelines : list ) -> bool :
434
+ """
435
+ Check if a message was already sent for newer commits, this is possible if pipelines of later commits,
436
+ finished before the pipeline of the current commit.
437
+ Args:
438
+ commit_index: The index of the current commit.
439
+ list_of_commits: A list of commits.
440
+ list_of_pipelines: A list of pipelines.
441
+ Returns:
442
+
443
+ """
444
+ for previous_commit , current_commit in pairwise (reversed (list_of_commits [:commit_index ])):
445
+ current_pipeline = get_pipeline_by_commit (current_commit , list_of_pipelines )
446
+ previous_pipeline = get_pipeline_by_commit (previous_commit , list_of_pipelines )
447
+ # in rare cases some commits have no pipeline
448
+ if current_pipeline and previous_pipeline and (is_pivot (current_pipeline , previous_pipeline ) is not None ):
449
+ return True
450
+ return False
451
+
452
+
453
+ def get_nearest_newer_commit_with_pipeline (list_of_pipelines : list [ProjectPipeline ], list_of_commits : list [ProjectCommit ],
454
+ current_commit_index : int ) -> tuple [ProjectPipeline , list ] | tuple [None , None ]:
455
+ """
456
+ Get the nearest newer commit that has a pipeline.
457
+ Args:
458
+ list_of_pipelines: A list of pipelines.
459
+ list_of_commits: A list of commits.
460
+ current_commit_index: The index of the current commit.
461
+ Returns:
462
+ A tuple of the nearest pipeline and a list of suspicious commits that have no pipelines.
463
+ """
464
+ suspicious_commits = []
465
+ for index in reversed (range (0 , current_commit_index - 1 )):
466
+ next_commit = list_of_commits [index ]
467
+ suspicious_commits .append (list_of_commits [index + 1 ])
468
+ next_pipeline = get_pipeline_by_commit (next_commit , list_of_pipelines )
469
+ if next_pipeline :
470
+ return next_pipeline , suspicious_commits
471
+ return None , None
472
+
473
+
474
+ def get_nearest_older_commit_with_pipeline (list_of_pipelines : list [ProjectPipeline ], list_of_commits : list [ProjectCommit ],
475
+ current_commit_index : int ) -> tuple [ProjectPipeline , list ] | tuple [None , None ]:
476
+ """
477
+ Get the nearest oldest commit that has a pipeline.
478
+ Args:
479
+ list_of_pipelines: A list of pipelines.
480
+ list_of_commits: A list of commits.
481
+ current_commit_index: The index of the current commit.
482
+ Returns:
483
+ A tuple of the nearest pipeline and a list of suspicious commits that have no pipelines.
484
+ """
485
+ suspicious_commits = []
486
+ for index in range (current_commit_index , len (list_of_commits ) - 1 ):
487
+ previous_commit = list_of_commits [index + 1 ]
488
+ suspicious_commits .append (list_of_commits [index ])
489
+ previous_pipeline = get_pipeline_by_commit (previous_commit , list_of_pipelines )
490
+ if previous_pipeline :
491
+ return previous_pipeline , suspicious_commits
492
+ return None , None
0 commit comments