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