@@ -293,6 +293,33 @@ def add_tag(self, tag):
293293 new_tags = ", " .join (current_tags )
294294 self .add_or_update_custom_field (REDMINE_CUSTOM_FIELD_ID_TAGS , new_tags )
295295
296+ def has_open_subtasks (self ):
297+ """
298+ Checks if the issue has any open subtasks.
299+ Returns True if open subtasks exist, False otherwise.
300+ """
301+ self .logger .debug ("Checking for open subtasks." )
302+ try :
303+ if not hasattr (self .issue , 'children' ):
304+ self .logger .debug ("Issue has no subtasks." )
305+ return False
306+
307+ open_subtasks_info = []
308+ for subtask in self .issue .children :
309+ subtask .refresh () # fetch status
310+ if not subtask .status .is_closed :
311+ open_subtasks_info .append (f"#{ subtask .id } (status: { subtask .status .name } )" )
312+
313+ if open_subtasks_info :
314+ self .logger .info (f"Cannot change status. Issue has open subtasks: { ', ' .join (open_subtasks_info )} ." )
315+ return True
316+ else :
317+ self .logger .debug ("All subtasks are closed." )
318+ return False
319+ except redminelib .exceptions .ResourceAttrError :
320+ self .logger .debug ("Issue has no subtasks (ResourceAttrError on 'children' attribute)." )
321+ return False
322+
296323 def get_update_payload (self , suppress_mail = True ): # Added suppress_mail parameter
297324 today = datetime .now (timezone .utc ).isoformat (timespec = 'seconds' )
298325 self .add_or_update_custom_field (REDMINE_CUSTOM_FIELD_ID_UPKEEP_TIMESTAMP , today )
@@ -525,17 +552,11 @@ def _transform_clear_stale_merge_commit(self, issue_update):
525552 related "Fixed In" field, as they are now considered stale.
526553 """
527554 issue_update .logger .debug ("Running _transform_clear_stale_merge_commit" )
528- try :
529- issue_with_journals = self .R .issue .get (issue_update .issue .id , include = ['journals' ])
530- except redminelib .exceptions .ResourceNotFoundError :
531- issue_update .logger .warning ("Could not fetch issue with journals. Skipping stale merge commit check." )
532- return False
533-
534555 last_pr_id_change = None
535556 last_merge_commit_set = None
536557
537558 # Journals are ordered oldest to newest, so reverse to find the latest changes first.
538- for journal in reversed (issue_with_journals .journals ):
559+ for journal in reversed (issue_update . issue .journals ):
539560 if last_pr_id_change and last_merge_commit_set :
540561 break
541562
@@ -783,6 +804,8 @@ def _transform_backport_resolved(self, issue_update):
783804
784805 # If PR is merged and it's a backport tracker with 'Pending Backport' status, update to 'Resolved'
785806 if issue_update .issue .status .id != REDMINE_STATUS_ID_RESOLVED :
807+ if issue_update .has_open_subtasks ():
808+ return False
786809 issue_update .logger .info (f"Issue status is '{ issue_update .issue .status .name } ', which is not 'Resolved'." )
787810 issue_update .logger .info ("Updating status to 'Resolved' because its PR is merged." )
788811 changed = issue_update .change_field ('status_id' , REDMINE_STATUS_ID_RESOLVED )
@@ -883,6 +906,9 @@ def _transform_resolve_main_issue_from_backports(self, issue_update):
883906 issue_update .logger .info (f"Not in 'Pending Backport' status ({ issue_update .issue .status .name } ). Skipping." )
884907 return False
885908
909+ if issue_update .has_open_subtasks ():
910+ return False
911+
886912 issue_update .logger .info ("Issue is a main tracker in 'Pending Backport' status. Checking related backports." )
887913
888914 expected_backport_releases_str = issue_update .get_custom_field (REDMINE_CUSTOM_FIELD_ID_BACKPORT )
@@ -896,18 +922,10 @@ def _transform_resolve_main_issue_from_backports(self, issue_update):
896922 issue_update .logger .warning (f"No backport releases specified in custom field { REDMINE_CUSTOM_FIELD_ID_BACKPORT } ." )
897923
898924 copied_to_backports_ids = []
899- try :
900- # Fetch the issue again with 'include=relations' to ensure relations are loaded
901- issue_update .logger .debug ("Fetching issue relations to find 'copied_to' links." )
902- issue_with_relations = self .R .issue .get (issue_update .issue .id , include = ['relations' ])
903-
904- for relation in issue_with_relations .relations :
905- if relation .relation_type == 'copied_to' :
906- copied_to_backports_ids .append (relation .issue_to_id )
907- issue_update .logger .info (f"Found 'Copied to' issue IDs: { copied_to_backports_ids } " )
908- except redminelib .exceptions .ResourceAttrError as e :
909- issue_update .logger .warning (f"Could not fetch relations for issue: { e } . Skipping backport status check." )
910- return False
925+ for relation in issue_update .issue .relations :
926+ if relation .relation_type == 'copied_to' :
927+ copied_to_backports_ids .append (relation .issue_to_id )
928+ issue_update .logger .info (f"Found 'Copied to' issue IDs: { copied_to_backports_ids } " )
911929
912930 if not copied_to_backports_ids and not expected_backport_releases :
913931 # If no backports are expected and no 'Copied to' issues exist,
@@ -1027,6 +1045,9 @@ def _transform_set_status_on_merge(self, issue_update):
10271045 issue_update .logger .info (f"Issue is already closed or 'Pending Backport'. Skipping status update on merge." )
10281046 return False
10291047
1048+ if issue_update .has_open_subtasks ():
1049+ return False
1050+
10301051 issue_update .logger .info (f"Issue has a merge commit ({ merge_commit } ) and current status is '{ issue_update .issue .status .name } '." )
10311052
10321053 backports_field_value = issue_update .get_custom_field (REDMINE_CUSTOM_FIELD_ID_BACKPORT )
@@ -1079,6 +1100,7 @@ def _process_issue_transformations(self, issue):
10791100 and sends a single update API call if changes are made.
10801101 """
10811102 self .issues_inspected += 1
1103+ issue = self .R .issue .get (issue .id , include = ['children' , 'journals' , 'relations' ])
10821104 issue_update = IssueUpdate (issue , self .session , self .G )
10831105 issue_update .logger .debug ("Beginning transformation processing." )
10841106
0 commit comments