6868IN_TEST_RULES_LABEL = os .getenv ('IN_TEST_RULES_LABEL' , 'in-test-rules' )
6969# label to apply to PRs that are excluded due to author membership
7070AUTHOR_MEMBERSHIP_EXCLUSION_LABEL = os .getenv ('AUTHOR_MEMBERSHIP_EXCLUSION_LABEL' , 'test-rules:excluded:author_membership' )
71+ # label to apply to PRs that are manually excluded (by removing in-test-rules label)
72+ MANUAL_EXCLUSION_LABEL = os .getenv ('MANUAL_EXCLUSION_LABEL' , 'test-rules:excluded:manual' )
7173
7274# flag to skip files containing specific text patterns
7375# this is due to test-rules not supporting specific functions
@@ -570,6 +572,23 @@ def save_file(path, content):
570572 file .write (content )
571573
572574
575+ def pr_has_synced_files (pr_number ):
576+ """
577+ Check if a PR has any synced files in the output folder.
578+
579+ Args:
580+ pr_number (int): Pull request number
581+
582+ Returns:
583+ bool: True if files exist for this PR, False otherwise
584+ """
585+ prefix = f"{ pr_number } _"
586+ for filename in os .listdir (OUTPUT_FOLDER ):
587+ if filename .startswith (prefix ) and filename .endswith ('.yml' ):
588+ return True
589+ return False
590+
591+
573592def clean_output_folder (valid_files ):
574593 for filename in os .listdir (OUTPUT_FOLDER ):
575594 file_path = os .path .join (OUTPUT_FOLDER , filename )
@@ -829,15 +848,35 @@ def handle_pr_rules(mode):
829848
830849 for pr in pull_requests :
831850 # Common checks for all modes
851+ # Draft PRs are skipped unless user explicitly added the in-test-rules label
832852 if pr ['draft' ]:
833- print (f"Skipping draft PR #{ pr ['number' ]} : { pr ['title' ]} " )
834- continue
853+ if ADD_TEST_RULES_LABEL and has_label (pr ['number' ], IN_TEST_RULES_LABEL ):
854+ print (f"Processing draft PR #{ pr ['number' ]} (has '{ IN_TEST_RULES_LABEL } ' label): { pr ['title' ]} " )
855+ else :
856+ print (f"Skipping draft PR #{ pr ['number' ]} : { pr ['title' ]} " )
857+ continue
835858 if pr ['base' ]['ref' ] != 'main' :
836859 print (f"Skipping non-main branch PR #{ pr ['number' ]} : { pr ['title' ]} -- dest branch: { pr ['base' ]['ref' ]} " )
837860 continue
838861
839862 pr_number = pr ['number' ]
840863
864+ # Check for manual exclusion label (user opted out of test-rules)
865+ if ADD_TEST_RULES_LABEL and has_label (pr_number , MANUAL_EXCLUSION_LABEL ):
866+ print (f"Skipping manually excluded PR #{ pr_number } : { pr ['title' ]} " )
867+ # Remove in-test-rules label if both are present (manual exclusion takes precedence)
868+ if has_label (pr_number , IN_TEST_RULES_LABEL ):
869+ print (f"\t Removing '{ IN_TEST_RULES_LABEL } ' label since manual exclusion takes precedence" )
870+ remove_label (pr_number , IN_TEST_RULES_LABEL )
871+ continue
872+
873+ # Check if user removed the in-test-rules label (opt-out)
874+ # If PR has synced files but no in-test-rules label, user must have removed it
875+ if ADD_TEST_RULES_LABEL and pr_has_synced_files (pr_number ) and not has_label (pr_number , IN_TEST_RULES_LABEL ):
876+ print (f"PR #{ pr_number } has synced files but '{ IN_TEST_RULES_LABEL } ' label was removed - applying manual exclusion" )
877+ apply_label (pr_number , MANUAL_EXCLUSION_LABEL )
878+ continue
879+
841880 # Organization membership and comment trigger checks (for any mode if flags are set)
842881 process_pr = True
843882 print (f"Processing PR #{ pr_number } : { pr ['title' ]} " )
@@ -867,7 +906,11 @@ def handle_pr_rules(mode):
867906 if not has_label (pr_number , AUTHOR_MEMBERSHIP_EXCLUSION_LABEL ):
868907 print (f"\t PR #{ pr_number } doesn't have the '{ AUTHOR_MEMBERSHIP_EXCLUSION_LABEL } ' label. Applying..." )
869908 apply_label (pr_number , AUTHOR_MEMBERSHIP_EXCLUSION_LABEL )
870-
909+
910+ # Remove in-test-rules label if previously applied
911+ if ADD_TEST_RULES_LABEL and has_label (pr_number , IN_TEST_RULES_LABEL ):
912+ remove_label (pr_number , IN_TEST_RULES_LABEL )
913+
871914 process_pr = False
872915
873916 if not process_pr :
@@ -882,6 +925,9 @@ def handle_pr_rules(mode):
882925 if not has_required_action_completed (latest_sha , REQUIRED_CHECK_NAME , REQUIRED_CHECK_CONCLUSION ):
883926 print (
884927 f"\t Skipping PR #{ pr_number } : Required check '{ REQUIRED_CHECK_NAME } ' has not completed with conclusion '{ REQUIRED_CHECK_CONCLUSION } '" )
928+ # Remove in-test-rules label if previously applied
929+ if ADD_TEST_RULES_LABEL and has_label (pr_number , IN_TEST_RULES_LABEL ):
930+ remove_label (pr_number , IN_TEST_RULES_LABEL )
885931 continue
886932
887933 files = get_files_for_pull_request (pr_number )
@@ -891,12 +937,16 @@ def handle_pr_rules(mode):
891937 yaml_rule_count = count_yaml_rules_in_pr (files )
892938 if yaml_rule_count > MAX_RULES_PER_PR :
893939 print (f"\t Skipping PR #{ pr_number } : Contains { yaml_rule_count } YAML rules (max allowed: { MAX_RULES_PER_PR } )" )
894-
940+
895941 # Apply label to indicate PR was skipped due to too many rules
896942 if not has_label (pr_number , BULK_PR_LABEL ):
897943 print (f"\t PR #{ pr_number } doesn't have the '{ BULK_PR_LABEL } ' label. Applying..." )
898944 apply_label (pr_number , BULK_PR_LABEL )
899-
945+
946+ # Remove in-test-rules label if previously applied
947+ if ADD_TEST_RULES_LABEL and has_label (pr_number , IN_TEST_RULES_LABEL ):
948+ remove_label (pr_number , IN_TEST_RULES_LABEL )
949+
900950 continue
901951 else :
902952 # if it has the label, remove it.
0 commit comments