1+ from datetime import datetime , timezone , timedelta
12import logging
23
4+ from common .constants import JiraLabels
5+
6+ from .constants import DATETIME_MIN_UTC , GITLAB_GROUPS
37from .errata_utils import get_erratum_for_link
8+ from .gitlab_utils import search_gitlab_project_mrs
49from .work_item_handler import WorkItemHandler
510from .jira_utils import add_issue_label , change_issue_status
611from .supervisor_types import (
712 FullIssue ,
813 IssueStatus ,
14+ MergeRequestState ,
915 PreliminaryTesting ,
10- TestCoverage ,
1116 TestingState ,
1217 WorkflowResult ,
1318)
1419from .testing_analyst import analyze_issue
1520
16-
1721logger = logging .getLogger (__name__ )
1822
1923
@@ -40,29 +44,116 @@ def resolve_set_status(self, status: IssueStatus, why: str):
4044 def resolve_flag_attention (self , why : str ):
4145 add_issue_label (
4246 self .issue .key ,
43- "jotnar_needs_attention" ,
47+ JiraLabels . NEEDS_ATTENTION . value ,
4448 why ,
4549 dry_run = self .dry_run ,
4650 )
4751
4852 return WorkflowResult (status = why , reschedule_in = - 1 )
4953
50- async def run (self ) -> WorkflowResult :
51- """
52- Runs the workflow for a single issue.
54+ def label_merge_if_needed (self ):
55+ """Add the jotnar_merged label to the issue
56+
57+ This function will only add jotnar_merged label to the issue if it matches
58+ all the following requirements:
59+ 1. Issue has either jotnar_backported or jotnar_rebased label.
60+ 2. Issue doesn't have jotnar_merged label.
61+ 3. A merged MR is found on Gitlab.
62+
63+ Returns:
64+ True if a merge gitlab issue was found and the merged label was added,
65+ otherwise, return False.
5366 """
5467 issue = self .issue
68+ component = issue .components [0 ]
69+
70+ if (
71+ JiraLabels .BACKPORTED .value in issue .labels
72+ or JiraLabels .REBASED .value in issue .labels
73+ ) and JiraLabels .MERGED .value not in issue .labels :
74+ for group in GITLAB_GROUPS :
75+ merged_mrs = search_gitlab_project_mrs (
76+ f"redhat/{ group } /{ component } " ,
77+ issue .key ,
78+ state = MergeRequestState .MERGED ,
79+ )
5580
56- logger .info ("Running workflow for issue %s" , issue .url )
81+ if merged_mr := next (merged_mrs , None ):
82+ add_issue_label (
83+ issue .key ,
84+ JiraLabels .MERGED .value ,
85+ f"A [merge request| { merged_mr .url } ]. resolving this issue has been merged; waiting for errata creation and final testing." ,
86+ dry_run = self .dry_run ,
87+ )
88+
89+ issue .labels .append (JiraLabels .MERGED .value )
90+ return True
5791
58- if "jotnar_needs_attention" in issue .labels :
92+ return False
93+
94+ def get_latest_merged_timestamp (self ):
95+ """This function will return DATETIME_MIN_UTC if it doesn't find any merged MRs"""
96+ issue = self .issue
97+ component = issue .components [0 ]
98+
99+ def get_merged_mrs ():
100+ for group in GITLAB_GROUPS :
101+ project = f"redhat/{ group } /{ component } "
102+ yield from search_gitlab_project_mrs (
103+ project ,
104+ issue .key ,
105+ state = MergeRequestState .MERGED ,
106+ )
107+
108+ return max (
109+ (mr .merged_at or DATETIME_MIN_UTC for mr in get_merged_mrs ()),
110+ default = DATETIME_MIN_UTC ,
111+ )
112+
113+ async def run_before_errata_created (self ) -> WorkflowResult :
114+ """Workflow for issues with no errata link"""
115+ issue = self .issue
116+
117+ if not any (
118+ label
119+ in (
120+ JiraLabels .BACKPORTED .value ,
121+ JiraLabels .REBASED .value ,
122+ JiraLabels .MERGED .value ,
123+ )
124+ for label in issue .labels
125+ ):
59126 return self .resolve_remove_work_item (
60- "Issue has the jotnar_needs_attention label "
127+ f "Issue without target labels: { issue . labels } "
61128 )
62129
63- if issue . errata_link is None :
64- return self .resolve_remove_work_item ( "Issue has no errata_link" )
130+ if JiraLabels . MERGED . value not in issue . labels :
131+ self .label_merge_if_needed ( )
65132
133+ if JiraLabels .MERGED .value not in issue .labels :
134+ return self .resolve_wait (
135+ "No merged MR found, reschedule it for 3 hours" ,
136+ reschedule_in = 60 * 60 * 3 ,
137+ )
138+
139+ latest_merged_timestamp = self .get_latest_merged_timestamp ()
140+ cur_time = datetime .now (tz = timezone .utc )
141+ time_diff = abs (cur_time - latest_merged_timestamp )
142+ if time_diff < timedelta (days = 1 ):
143+ return self .resolve_wait (
144+ "Wait for the associated erratum to be created" ,
145+ reschedule_in = 60 * 60 ,
146+ )
147+ else :
148+ return self .resolve_flag_attention (
149+ "A merge request was merged for this issue more than 24 hours ago but no errata "
150+ "was created. Please investigate and look for gating failures or other reasons that "
151+ "might have blocked errata creation."
152+ )
153+
154+ async def run_after_errata_created (self ) -> WorkflowResult :
155+ issue = self .issue
156+ assert issue .errata_link is not None
66157 if issue .fixed_in_build is None :
67158 return self .resolve_flag_attention (
68159 "Issue has errata_link but no fixed_in_build"
@@ -80,6 +171,10 @@ async def run(self) -> WorkflowResult:
80171 "happened before the gitlab pull request was merged"
81172 )
82173
174+ # We still want the jotnar_merged label for JIRA dashboards even if we never saw
175+ # the merged merge request in the pre-errata-creation state.
176+ self .label_merge_if_needed ()
177+
83178 if issue .status in (
84179 IssueStatus .NEW ,
85180 IssueStatus .PLANNING ,
@@ -121,3 +216,27 @@ async def run(self) -> WorkflowResult:
121216 return self .resolve_remove_work_item (f"Issue status is { issue .status } " )
122217 else :
123218 raise ValueError (f"Unknown issue status: { issue .status } " )
219+
220+ async def run (self ) -> WorkflowResult :
221+ """
222+ Runs the workflow for a single issue.
223+ """
224+ issue = self .issue
225+
226+ logger .info ("Running workflow for issue %s" , issue .url )
227+
228+ if JiraLabels .NEEDS_ATTENTION .value in issue .labels :
229+ return self .resolve_remove_work_item (
230+ "Issue has the jotnar_needs_attention label"
231+ )
232+
233+ if len (issue .components ) != 1 :
234+ return self .resolve_flag_attention (
235+ "This issue has multiple components. "
236+ "Jotnar only handles issues with single component currently."
237+ )
238+
239+ if issue .errata_link is None :
240+ return await self .run_before_errata_created ()
241+ else :
242+ return await self .run_after_errata_created ()
0 commit comments