@@ -97,6 +97,300 @@ def notifyProvisionRoks(channels: list[str], rc: int, additionalMsg: str | None
9797 return response .data .get ("ok" , False )
9898
9999
100+ def notifyPipelineStart (channels : list [str ], instanceId : str | None = None , pipelineName : str | None = None ) -> dict | None :
101+ """Send Slack notification about pipeline start and create thread for all channels."""
102+ # Exit early if no channels provided
103+ if not channels or len (channels ) == 0 :
104+ print ("No Slack channels provided - skipping pipeline start notification" )
105+ return None
106+
107+ # For update pipeline, use mas-pipelines namespace (no instance ID)
108+ # For install/upgrade pipelines, use mas-{instanceId}-pipelines namespace
109+ if instanceId is None or instanceId == "" :
110+ namespace = "mas-pipelines"
111+ else :
112+ namespace = f"mas-{ instanceId } -pipelines"
113+
114+ # Check if thread already exists
115+ threadInfo = SlackUtil .getThreadConfigMap (namespace , instanceId , pipelineName )
116+ if threadInfo is not None :
117+ print ("Pipeline start notification already sent" )
118+ return threadInfo
119+
120+ # Send pipeline started message to all channels
121+ toolchainLink = _getToolchainLink ()
122+ instanceInfo = f"Instance ID: `{ instanceId } `" if instanceId else ""
123+ message = [
124+ SlackUtil .buildHeader (f"🚀 MAS { pipelineName } Pipeline Started" ),
125+ SlackUtil .buildSection (f"Pipeline Run: { pipelineName } \n { instanceInfo } \n { toolchainLink } " )
126+ ]
127+ response = SlackUtil .postMessageBlocks (channels , message )
128+
129+ # Store thread information for all channels in ConfigMap
130+ configMapData = {"instanceId" : instanceId , "pipelineName" : pipelineName }
131+
132+ if isinstance (response , list ):
133+ # Multiple channels - store each channel's thread info
134+ for idx , res in enumerate (response ):
135+ if res .data .get ("ok" , False ):
136+ threadId = res ["ts" ]
137+ channelId = res ["channel" ]
138+ # Store with channel-specific keys
139+ configMapData [f"channel_{ idx } " ] = channelId
140+ configMapData [f"threadId_{ idx } " ] = threadId
141+ configMapData ["channel_count" ] = str (len (response ))
142+ else :
143+ # Single channel
144+ if response .data .get ("ok" , False ):
145+ threadId = response ["ts" ]
146+ channelId = response ["channel" ]
147+ configMapData ["channel_0" ] = channelId
148+ configMapData ["threadId_0" ] = threadId
149+ configMapData ["channel_count" ] = "1"
150+ else :
151+ print ("Failed to send pipeline start Slack message" )
152+ return False
153+
154+ # Create ConfigMap with all channel/thread info
155+ SlackUtil .createThreadConfigMap (namespace , instanceId , pipelineName )
156+ SlackUtil .updateThreadConfigMap (namespace , instanceId , configMapData , pipelineName )
157+ return SlackUtil .getThreadConfigMap (namespace , instanceId , pipelineName )
158+
159+
160+ def notifyAnsibleStart (channels : list [str ], taskName : str , instanceId : str | None = None , pipelineName : str | None = None ) -> bool :
161+ """Send Slack notification about Ansible task start to all channels."""
162+ # Exit early if no channels provided
163+ if not channels or len (channels ) == 0 :
164+ print ("No Slack channels provided - skipping Ansible task start notification" )
165+ return False
166+
167+ # For update pipeline, use mas-pipelines namespace (no instance ID)
168+ # For install/upgrade pipelines, use mas-{instanceId}-pipelines namespace
169+ if instanceId is None or instanceId == "" :
170+ namespace = "mas-pipelines"
171+ else :
172+ namespace = f"mas-{ instanceId } -pipelines"
173+
174+ # Get thread information, create if doesn't exist
175+ threadInfo = SlackUtil .getThreadConfigMap (namespace , instanceId , pipelineName )
176+ if threadInfo is None :
177+ print ("No thread found - creating pipeline start notification" )
178+ threadInfo = notifyPipelineStart (channels , instanceId , pipelineName )
179+
180+ # Get channel count
181+ channelCount = int (threadInfo .get ("channel_count" , "0" ))
182+ if channelCount == 0 :
183+ print ("No channels found in thread info" )
184+ return False
185+
186+ # Send task start message as thread reply to all channels
187+ taskMessage = [
188+ SlackUtil .buildSection (f"⏳ **{ taskName } ** - Started" )
189+ ]
190+
191+ allSuccess = True
192+ taskMessageData = {}
193+
194+ for idx in range (channelCount ):
195+ channelId = threadInfo .get (f"channel_{ idx } " )
196+ threadId = threadInfo .get (f"threadId_{ idx } " )
197+
198+ if channelId and threadId :
199+ response = SlackUtil .postMessageBlocks (channelId , taskMessage , threadId )
200+
201+ # Save message timestamp for this channel
202+ if response .data .get ("ok" , False ):
203+ messageTs = response .data .get ("ts" )
204+ if messageTs :
205+ # Store with task name and channel index as key
206+ taskMessageData [f"task_{ taskName } _{ idx } " ] = messageTs
207+ else :
208+ allSuccess = False
209+ else :
210+ allSuccess = False
211+
212+ # Update ConfigMap with all task message timestamps
213+ if taskMessageData :
214+ SlackUtil .updateThreadConfigMap (namespace , instanceId , taskMessageData , pipelineName )
215+
216+ return allSuccess
217+
218+
219+ def notifyAnsibleComplete (channels : list [str ], rc : int , taskName : str , instanceId : str | None = None , pipelineName : str | None = None ) -> bool :
220+ """Send Slack notification about Ansible task completion status to all channels."""
221+ # Exit early if no channels provided
222+ if not channels or len (channels ) == 0 :
223+ print ("No Slack channels provided - skipping Ansible task completion notification" )
224+ return False
225+
226+ # For update pipeline, use mas-pipelines namespace (no instance ID)
227+ # For install/upgrade pipelines, use mas-{instanceId}-pipelines namespace
228+ if instanceId is None or instanceId == "" :
229+ namespace = "mas-pipelines"
230+ else :
231+ namespace = f"mas-{ instanceId } -pipelines"
232+
233+ # Get thread information, create if doesn't exist
234+ threadInfo = SlackUtil .getThreadConfigMap (namespace , instanceId , pipelineName )
235+ if threadInfo is None :
236+ print ("No thread found - creating pipeline start notification" )
237+ threadInfo = notifyPipelineStart (channels , instanceId , pipelineName )
238+
239+ # Get channel count
240+ channelCount = int (threadInfo .get ("channel_count" , "0" ))
241+ if channelCount == 0 :
242+ print ("No channels found in thread info" )
243+ return False
244+
245+ # Determine status
246+ if rc == 0 :
247+ emoji = "✅"
248+ status = "Success"
249+ else :
250+ emoji = "❌"
251+ status = "Failed"
252+
253+ allSuccess = True
254+
255+ # Update message in each channel
256+ for idx in range (channelCount ):
257+ channelId = threadInfo .get (f"channel_{ idx } " )
258+ threadId = threadInfo .get (f"threadId_{ idx } " )
259+ taskMessageTs = threadInfo .get (f"task_{ taskName } _{ idx } " )
260+
261+ if not channelId or not threadId :
262+ allSuccess = False
263+ continue
264+
265+ # Calculate task duration if we have the message timestamp
266+ durationText = ""
267+ if taskMessageTs :
268+ from datetime import datetime , timezone
269+ try :
270+ # Message timestamp is in format "1234567890.123456"
271+ startTime = float (taskMessageTs )
272+ endTime = datetime .now (timezone .utc ).timestamp ()
273+ duration = int (endTime - startTime )
274+
275+ hours , remainder = divmod (duration , 3600 )
276+ minutes , seconds = divmod (remainder , 60 )
277+
278+ if hours > 0 :
279+ durationText = f" ({ hours } h { minutes } m { seconds } s)"
280+ elif minutes > 0 :
281+ durationText = f" ({ minutes } m { seconds } s)"
282+ else :
283+ durationText = f" ({ seconds } s)"
284+ except Exception as e :
285+ print (f"Failed to calculate duration for channel { idx } : { e } " )
286+
287+ # Build the completion message
288+ taskMessage = [
289+ SlackUtil .buildSection (f"{ emoji } **{ taskName } ** - { status } { durationText } " )
290+ ]
291+ if rc != 0 :
292+ taskMessage .append (SlackUtil .buildSection (f"Return Code: `{ rc } `\n Check logs for details" ))
293+
294+ # If we have the original message timestamp, update it; otherwise post new message
295+ if taskMessageTs :
296+ response = SlackUtil .updateMessageBlocks (channelId , taskMessageTs , taskMessage )
297+ if not response .data .get ("ok" , False ):
298+ allSuccess = False
299+ else :
300+ # Fallback: post new message if task start message wasn't tracked
301+ print (f"No start message found for task { taskName } in channel { idx } , posting new completion message" )
302+ response = SlackUtil .postMessageBlocks (channelId , taskMessage , threadId )
303+ if not response .data .get ("ok" , False ):
304+ allSuccess = False
305+
306+ # Special case, mas-update pipeline
307+ if namespace == "mas-pipelines" and taskName == "post-deps-update-verify-ingress" :
308+ print (f"mas-update pipeline completed with status: { rc } , sending pipeline complete message" )
309+ allSuccess : bool = notifyPipelineComplete (channels , rc , instanceId , pipelineName )
310+
311+ return allSuccess
312+
313+
314+ def notifyPipelineComplete (channels : list [str ], rc : int , instanceId : str | None = None , pipelineName : str | None = None ) -> bool :
315+ """Send Slack notification about pipeline completion to all channels and cleanup ConfigMap."""
316+ # Exit early if no channels provided
317+ if not channels or len (channels ) == 0 :
318+ print ("No Slack channels provided - skipping pipeline completion notification" )
319+ return False
320+
321+ # For update pipeline, use mas-pipelines namespace (no instance ID)
322+ # For install/upgrade pipelines, use mas-{instanceId}-pipelines namespace
323+ if instanceId is None or instanceId == "" :
324+ namespace = "mas-pipelines"
325+ else :
326+ namespace = f"mas-{ instanceId } -pipelines"
327+
328+ # Get thread information
329+ threadInfo = SlackUtil .getThreadConfigMap (namespace , instanceId , pipelineName )
330+ if threadInfo is None :
331+ print ("No thread information found - pipeline may not have started properly" )
332+ return False
333+
334+ # Get channel count
335+ channelCount = int (threadInfo .get ("channel_count" , "0" ))
336+ if channelCount == 0 :
337+ print ("No channels found in thread info" )
338+ return False
339+
340+ startTime = threadInfo .get ("startTime" )
341+
342+ # Calculate duration if start time is available
343+ durationText = ""
344+ if startTime :
345+ from datetime import datetime , timezone
346+ try :
347+ start = datetime .fromisoformat (startTime .replace ("Z" , "+00:00" ))
348+ end = datetime .now (timezone .utc )
349+ duration = end - start
350+ hours , remainder = divmod (int (duration .total_seconds ()), 3600 )
351+ minutes , seconds = divmod (remainder , 60 )
352+ if hours > 0 :
353+ durationText = f"\n Total Duration: { hours } h { minutes } m { seconds } s"
354+ else :
355+ durationText = f"\n Total Duration: { minutes } m { seconds } s"
356+ except Exception :
357+ pass
358+
359+ instanceInfo = f"Instance ID: `{ instanceId } `" if instanceId else ""
360+ if rc == 0 :
361+ emoji = "🎉"
362+ status = "Completed Successfully"
363+ additionalInfo = "\n All tasks completed successfully"
364+ else :
365+ emoji = "💥"
366+ status = "Failed"
367+ additionalInfo = f"\n Pipeline failed with return code: `{ rc } `"
368+
369+ message = [
370+ SlackUtil .buildHeader (f"{ emoji } MAS { pipelineName } Pipeline { status } " ),
371+ SlackUtil .buildSection (f"Pipeline Run: { pipelineName } \n { instanceInfo } { durationText } { additionalInfo } " )
372+ ]
373+
374+ allSuccess = True
375+
376+ # Send completion message to all channels
377+ for idx in range (channelCount ):
378+ channelId = threadInfo .get (f"channel_{ idx } " )
379+ threadId = threadInfo .get (f"threadId_{ idx } " )
380+
381+ if channelId and threadId :
382+ response = SlackUtil .postMessageBlocks (channelId , message , threadId )
383+ if not response .data .get ("ok" , False ):
384+ allSuccess = False
385+ else :
386+ allSuccess = False
387+
388+ # Clean up ConfigMap
389+ SlackUtil .deleteThreadConfigMap (namespace , instanceId , pipelineName )
390+
391+ return allSuccess
392+
393+
100394if __name__ == "__main__" :
101395 # If SLACK_TOKEN or SLACK_CHANNEL env vars are not set then silently exit taking no action
102396 SLACK_TOKEN = os .getenv ("SLACK_TOKEN" , "" )
@@ -112,12 +406,23 @@ if __name__ == "__main__":
112406
113407 # Primary Options
114408 parser .add_argument ("--action" , required = True )
115- parser .add_argument ("--rc" , required = True , type = int )
409+ parser .add_argument ("--rc" , required = False , type = int )
116410 parser .add_argument ("--msg" , required = False , default = None )
411+ parser .add_argument ("--task-name" , required = False , default = "" )
412+ parser .add_argument ("--instance-id" , required = False , default = None )
413+ parser .add_argument ("--pipeline-name" , required = False , default = None )
117414
118415 args , unknown = parser .parse_known_args ()
119416
120417 if args .action == "ocp-provision-fyre" :
121418 notifyProvisionFyre (channelList , args .rc , args .msg )
122419 elif args .action == "ocp-provision-roks" :
123420 notifyProvisionRoks (channelList , args .rc , args .msg )
421+ elif args .action == "pipeline-start" :
422+ notifyPipelineStart (channelList , args .instance_id , args .pipeline_name )
423+ elif args .action == "ansible-start" :
424+ notifyAnsibleStart (channelList , args .task_name , args .instance_id , args .pipeline_name )
425+ elif args .action == "ansible-complete" :
426+ notifyAnsibleComplete (channelList , args .rc , args .task_name , args .instance_id , args .pipeline_name )
427+ elif args .action == "pipeline-complete" :
428+ notifyPipelineComplete (channelList , args .rc , args .instance_id , args .pipeline_name )
0 commit comments