23
23
from sqlalchemy .sql .functions import count
24
24
from werkzeug .utils import secure_filename
25
25
26
+ from database import DeclEnum
26
27
from decorators import get_menu_entries , template_renderer
27
28
from mailer import Mailer
28
29
from mod_auth .controllers import check_access_rights , login_required
@@ -55,6 +56,13 @@ class Status:
55
56
FAILURE = "failure"
56
57
57
58
59
+ class Workflow_builds (DeclEnum ):
60
+ """Define GitHub Action workflow build names."""
61
+
62
+ LINUX = "Build CCExtractor on Linux"
63
+ WINDOWS = "Build CCExtractor on Windows"
64
+
65
+
58
66
@mod_ci .before_app_request
59
67
def before_app_request () -> None :
60
68
"""Organize menu content such as Platform management before request."""
@@ -431,9 +439,9 @@ def save_xml_to_file(xml_node, folder_name, file_name) -> None:
431
439
)
432
440
433
441
434
- def queue_test (db , gh_commit , commit , test_type , branch = "master" , pr_nr = 0 ) -> None :
442
+ def add_test_entry (db , gh_commit , commit , test_type , branch = "master" , pr_nr = 0 ) -> None :
435
443
"""
436
- Store test details into Test model for each platform, and post the status to GitHub .
444
+ Add test details entry into Test model for each platform.
437
445
438
446
:param db: Database connection.
439
447
:type db: sqlalchemy.orm.scoped_session
@@ -464,6 +472,117 @@ def queue_test(db, gh_commit, commit, test_type, branch="master", pr_nr=0) -> No
464
472
windows_test = Test (TestPlatform .windows , test_type , fork .id , branch , commit , pr_nr )
465
473
db .add (windows_test )
466
474
db .commit ()
475
+
476
+
477
+ def schedule_test (gh_commit , commit , test_type , branch = "master" , pr_nr = 0 ) -> None :
478
+ """
479
+ Post status to GitHub as waiting for Github Actions completion.
480
+
481
+ :param gh_commit: The GitHub API call for the commit. Can be None
482
+ :type gh_commit: Any
483
+ :param commit: The commit hash.
484
+ :type commit: str
485
+ :param test_type: The type of test
486
+ :type test_type: TestType
487
+ :param branch: Branch name
488
+ :type branch: str
489
+ :param pr_nr: Pull Request number, if applicable.
490
+ :type pr_nr: int
491
+ :return: Nothing
492
+ :rtype: None
493
+ """
494
+ from run import log
495
+
496
+ if test_type == TestType .pull_request :
497
+ log .debug ('pull request test type detected' )
498
+ branch = "pull_request"
499
+
500
+ if gh_commit is not None :
501
+ for platform in TestPlatform :
502
+ try :
503
+ gh_commit .post (
504
+ state = Status .PENDING ,
505
+ description = "Waiting for actions to complete" ,
506
+ context = f"CI - { platform .value } " ,
507
+ )
508
+ except ApiError as a :
509
+ log .critical (f'Could not post to GitHub! Response: { a .response } ' )
510
+
511
+
512
+ def deschedule_test (gh_commit , commit , test_type , message = "Tests have been cancelled" , branch = "master" , pr_nr = 0 ,
513
+ state = Status .FAILURE ) -> None :
514
+ """
515
+ Post status to GitHub (default: as failure due to Github Actions incompletion).
516
+
517
+ :param gh_commit: The GitHub API call for the commit. Can be None
518
+ :type gh_commit: Any
519
+ :param commit: The commit hash.
520
+ :type commit: str
521
+ :param test_type: The type of test
522
+ :type test_type: TestType
523
+ :param branch: Branch name
524
+ :type branch: str
525
+ :param pr_nr: Pull Request number, if applicable.
526
+ :type pr_nr: int
527
+ :return: Nothing
528
+ :rtype: None
529
+ """
530
+ from run import log
531
+
532
+ if gh_commit is not None :
533
+ for platform in TestPlatform :
534
+ try :
535
+ gh_commit .post (
536
+ state = state ,
537
+ description = message ,
538
+ context = f"CI - { platform .value } " ,
539
+ )
540
+ except ApiError as a :
541
+ log .critical (f'Could not post to GitHub! Response: { a .response } ' )
542
+
543
+
544
+ def queue_test (db , gh_commit , commit , test_type , branch = "master" , pr_nr = 0 ) -> None :
545
+ """
546
+ Store test details into Test model for each platform, and post the status to GitHub.
547
+
548
+ :param db: Database connection.
549
+ :type db: sqlalchemy.orm.scoped_session
550
+ :param gh_commit: The GitHub API call for the commit. Can be None
551
+ :type gh_commit: Any
552
+ :param commit: The commit hash.
553
+ :type commit: str
554
+ :param test_type: The type of test
555
+ :type test_type: TestType
556
+ :param branch: Branch name
557
+ :type branch: str
558
+ :param pr_nr: Pull Request number, if applicable.
559
+ :type pr_nr: int
560
+ :return: Nothing
561
+ :rtype: None
562
+ """
563
+ from run import log
564
+
565
+ fork_url = f"%/{ g .github ['repository_owner' ]} /{ g .github ['repository' ]} .git"
566
+ fork = Fork .query .filter (Fork .github .like (fork_url )).first ()
567
+
568
+ if test_type == TestType .pull_request :
569
+ log .debug ('pull request test type detected' )
570
+ branch = "pull_request"
571
+
572
+ linux_test = Test .query .filter (and_ (Test .platform == TestPlatform .linux ,
573
+ Test .commit == commit ,
574
+ Test .fork_id == fork .id ,
575
+ Test .test_type == test_type ,
576
+ Test .branch == branch ,
577
+ Test .pr_nr == pr_nr
578
+ )).first ()
579
+ windows_test = Test .query .filter (and_ (Test .platform == TestPlatform .windows ,
580
+ Test .commit == commit ,
581
+ Test .fork_id == fork .id ,
582
+ Test .test_type == test_type ,
583
+ Test .branch == branch ,
584
+ Test .pr_nr == pr_nr
585
+ )).first ()
467
586
add_customized_regression_tests (linux_test .id )
468
587
add_customized_regression_tests (windows_test .id )
469
588
@@ -595,7 +714,7 @@ def start_ci():
595
714
596
715
last_commit .value = ref ['object' ]['sha' ]
597
716
g .db .commit ()
598
- queue_test (g .db , github_status , commit_hash , TestType .commit )
717
+ add_test_entry (g .db , github_status , commit_hash , TestType .commit )
599
718
else :
600
719
g .log .warning ('Unknown push type! Dumping payload for analysis' )
601
720
g .log .warning (payload )
@@ -617,14 +736,8 @@ def start_ci():
617
736
user_id = payload ['pull_request' ]['user' ]['id' ]
618
737
if BlockedUsers .query .filter (BlockedUsers .user_id == user_id ).first () is not None :
619
738
g .log .warning ("User Blacklisted" )
620
- github_status .post (
621
- state = Status .ERROR ,
622
- description = "CI start aborted. You may be blocked from accessing this functionality" ,
623
- target_url = url_for ('home.index' , _external = True )
624
- )
625
739
return 'ERROR'
626
-
627
- queue_test (g .db , github_status , commit_hash , TestType .pull_request , pr_nr = pr_nr )
740
+ add_test_entry (g .db , github_status , commit_hash , TestType .pull_request , pr_nr = pr_nr )
628
741
629
742
elif payload ['action' ] == 'closed' :
630
743
g .log .debug ('PR was closed, no after hash available' )
@@ -636,12 +749,15 @@ def start_ci():
636
749
continue
637
750
progress = TestProgress (test .id , TestStatus .canceled , "PR closed" , datetime .datetime .now ())
638
751
g .db .add (progress )
639
- repository .statuses (test .commit ).post (
640
- state = Status .FAILURE ,
641
- description = "Tests canceled" ,
642
- context = f"CI - { test .platform .value } " ,
643
- target_url = url_for ('test.by_id' , test_id = test .id , _external = True )
644
- )
752
+ # If test run status exists, mark them as cancelled
753
+ for status in repository .commits (test .commit ).status .get ()["statuses" ]:
754
+ if status ["context" ] == f"CI - { test .platform .value } " :
755
+ repository .statuses (test .commit ).post (
756
+ state = Status .FAILURE ,
757
+ description = "Tests cancelled" ,
758
+ context = f"CI - { test .platform .value } " ,
759
+ target_url = url_for ('test.by_id' , test_id = test .id , _external = True )
760
+ )
645
761
646
762
elif event == "issues" :
647
763
g .log .debug ('issues event detected' )
@@ -707,6 +823,68 @@ def start_ci():
707
823
else :
708
824
g .log .warning (f"Unsupported release action: { action } " )
709
825
826
+ elif event == "workflow_run" :
827
+ workflow_name = payload ['workflow_run' ]['name' ]
828
+ if workflow_name in [Workflow_builds .LINUX , Workflow_builds .WINDOWS ]:
829
+ g .log .debug ('workflow_run event detected' )
830
+ commit_hash = payload ['workflow_run' ]['head_sha' ]
831
+ github_status = repository .statuses (commit_hash )
832
+
833
+ if payload ['action' ] == "completed" :
834
+ is_complete = True
835
+ has_failed = False
836
+ builds = {"linux" : False , "windows" : False }
837
+ for workflow in repository .actions .runs .get (
838
+ event = payload ['workflow_run' ]['event' ],
839
+ actor = payload ['sender' ]['login' ],
840
+ branch = payload ['workflow_run' ]['head_branch' ]
841
+ )['workflow_runs' ]:
842
+ if workflow ['head_sha' ] == commit_hash :
843
+ if workflow ['status' ] == "completed" :
844
+ if workflow ['conclusion' ] != "success" :
845
+ has_failed = True
846
+ break
847
+ if workflow ['name' ] == "Build CCExtractor on Linux" :
848
+ builds ["linux" ] = True
849
+ elif workflow ['name' ] == "Build CCExtractor on Windows" :
850
+ builds ["windows" ] = True
851
+ elif workflow ['status' ] != "completed" :
852
+ is_complete = False
853
+ break
854
+
855
+ if has_failed :
856
+ # no runs to be scheduled since build failed
857
+ deschedule_test (github_status , commit_hash , TestType .commit ,
858
+ message = "Cancelling tests as Github Action(s) failed" )
859
+ elif is_complete :
860
+ if payload ['workflow_run' ]['event' ] == "pull_request" :
861
+ # In case of pull request run tests only if it is still in an open state
862
+ # and user is not blacklisted
863
+ for pull_request in repository .pulls .get (state = "open" ):
864
+ if pull_request ['head' ]['sha' ] == commit_hash and any (builds .values ()):
865
+ user_id = pull_request ['user' ]['id' ]
866
+ if BlockedUsers .query .filter (BlockedUsers .user_id == user_id ).first () is not None :
867
+ g .log .warning ("User Blacklisted" )
868
+ github_status .post (
869
+ state = Status .ERROR ,
870
+ description = "CI start aborted. \
871
+ You may be blocked from accessing this functionality" ,
872
+ target_url = url_for ('home.index' , _external = True )
873
+ )
874
+ return 'ERROR'
875
+ queue_test (g .db , github_status , commit_hash ,
876
+ TestType .pull_request , pr_nr = pull_request ['number' ])
877
+ elif any (builds .values ()):
878
+ queue_test (g .db , github_status , commit_hash , TestType .commit )
879
+ else :
880
+ deschedule_test (github_status , commit_hash , TestType .commit ,
881
+ message = "Not ran - no code changes" , state = Status .SUCCESS )
882
+ elif payload ['action' ] == 'requested' :
883
+ schedule_test (github_status , commit_hash , TestType .commit )
884
+ else :
885
+ g .log .warning ('Unknown action type in workflow_run! Dumping payload for analysis' )
886
+ g .log .warning (payload )
887
+
710
888
else :
711
889
g .log .warning (f'CI unrecognized event: { event } ' )
712
890
0 commit comments