1+ #!/usr/bin/env python
2+ """
3+ A script which runs as part of our GitHub Actions build to send notifications
4+ to Slack. Notifies when build status changes.
5+ Usage:
6+ ./github-slack-notify.py $STATUS
7+ where $STATUS is the current build status.
8+ Requires GITHUB_TOKEN and SLACK_WEBHOOK_URL to be in the environment.
9+ """
10+ import os
11+ import sys
12+
13+ import requests
14+
15+
16+ def statusemoji (status ):
17+ return {"success" : ":heavy_check_mark:" , "failure" : ":rotating_light:" }.get (
18+ status , ":warning:"
19+ )
20+
21+
22+ def get_message_title ():
23+ return os .getenv ("SLACK_MESSAGE_TITLE" , "GitHub Actions Tests" )
24+
25+
26+ def get_build_url (job_name , run_id ):
27+ repo = os .environ ["GITHUB_REPOSITORY" ]
28+ token = os .environ ["GITHUB_TOKEN" ]
29+ response = requests .get (
30+ f"https://api.github.com/repos/" + str (repo )+ "/actions/runs/" + str (run_id ) + "/jobs" ,
31+ headers = {
32+ "Authorization" : f"Bearer { token } " ,
33+ "Accept" : "application/vnd.github.v3+json" ,
34+ },
35+ )
36+ print ("https://api.github.com/repos/" + str (repo )+ "/actions/runs/" + str (run_id ) + "/jobs" , response .json ())
37+ jobs = response .json ()["jobs" ]
38+ print (jobs )
39+ for job in jobs :
40+ if job_name in job ["name" ]:
41+ return job ["html_url" ]
42+
43+
44+ return "https://github.com/{GITHUB_REPOSITORY}/commit/{GITHUB_SHA}/checks" .format (
45+ ** os .environ
46+ )
47+
48+
49+ def is_pr_build ():
50+ return os .environ ["GITHUB_REF" ].startswith ("refs/pull/" )
51+
52+
53+ def get_branchname ():
54+ # split on slashes, strip 'refs/heads/*' , and rejoin
55+ # this is also the tag name if a tag is used
56+ return "/" .join (os .environ ["GITHUB_REF" ].split ("/" )[2 :])
57+
58+
59+ def _get_workflow_id_map ():
60+ repo = os .environ ["GITHUB_REPOSITORY" ]
61+ token = os .environ ["GITHUB_TOKEN" ]
62+ r = requests .get (
63+ f"https://api.github.com/repos/{ repo } /actions/workflows" ,
64+ headers = {
65+ "Authorization" : f"Bearer { token } " ,
66+ "Accept" : "application/vnd.github.v3+json" ,
67+ },
68+ )
69+
70+ ret = {}
71+ for w in r .json ().get ("workflows" , []):
72+ ret [w ["name" ]] = w ["id" ]
73+ print (f">> workflow IDs: { ret } " )
74+ return ret
75+
76+
77+ def get_last_build_status ():
78+ branch = get_branchname ()
79+ repo = os .environ ["GITHUB_REPOSITORY" ]
80+ token = os .environ ["GITHUB_TOKEN" ]
81+ workflow_id = _get_workflow_id_map ()[os .environ ["GITHUB_WORKFLOW" ]]
82+
83+ r = requests .get (
84+ f"https://api.github.com/repos/{ repo } /actions/workflows/{ workflow_id } /runs" ,
85+ params = {"branch" : branch , "status" : "completed" , "per_page" : 1 },
86+ headers = {
87+ "Authorization" : f"Bearer { token } " ,
88+ "Accept" : "application/vnd.github.v3+json" ,
89+ },
90+ )
91+ print (f">> get past workflow runs params: branch={ branch } ,repo={ repo } " )
92+ print (f">> get past workflow runs result: status={ r .status_code } " )
93+ runs_docs = r .json ().get ("workflow_runs" , [])
94+ # no suitable status was found for a previous build, so the status is "None"
95+ if not runs_docs :
96+ print (">>> no previous run found for workflow" )
97+ return None
98+ conclusion = runs_docs [0 ]["conclusion" ]
99+ print (f">>> previous run found with conclusion={ conclusion } " )
100+ return conclusion
101+
102+
103+ def check_status_changed (status ):
104+ # NOTE: last_status==None is always considered a change. This is intentional
105+ last_status = get_last_build_status ()
106+ res = last_status != status
107+ if res :
108+ print (f"status change detected (old={ last_status } , new={ status } )" )
109+ else :
110+ print (f"no status change detected (old={ last_status } , new={ status } )" )
111+ return res
112+
113+
114+ def get_failure_message ():
115+ return os .getenv ("SLACK_FAILURE_MESSAGE" , "tests failed" )
116+
117+
118+ def build_payload (status , job_name , run_id ):
119+ context = f"{ statusemoji (status )} build for { get_branchname ()} : { status } "
120+ message = f"<{ get_build_url (job_name , run_id )} |{ get_message_title ()} >"
121+ if "fail" in status .lower ():
122+ message = f"{ message } : { get_failure_message ()} "
123+ return {
124+ "channel" : os .getenv ("SLACK_CHANNEL" , "#servicex-github" ),
125+ "username" : "Prajwal Kiran Kumar" ,
126+ "blocks" : [
127+ {"type" : "section" , "text" : {"type" : "mrkdwn" , "text" : message }},
128+ {"type" : "context" , "elements" : [{"type" : "mrkdwn" , "text" : context }]},
129+ ],
130+ }
131+
132+
133+ def on_main_repo ():
134+ """check if running from a fork"""
135+ res = os .environ ["GITHUB_REPOSITORY" ].lower () == "ssl-hep/servicex-backend-tests"
136+ print (f"Checking main repo: { res } " )
137+ return res
138+
139+
140+ def should_notify (status ):
141+ res = check_status_changed (status ) and on_main_repo () and not is_pr_build ()
142+ print (f"Should notify: { res } " )
143+ return res
144+
145+
146+
147+ def main ():
148+ status = sys .argv [1 ]
149+ job_name = sys .argv [2 ]
150+ run_id = sys .argv [3 ]
151+
152+ if should_notify (status ):
153+ r = requests .post (os .environ ["SLACK_WEBHOOK_URL" ], json = build_payload (status , job_name , run_id ))
154+ print (f">> webhook response: status={ r .status_code } " )
155+
156+ if __name__ == "__main__" :
157+ main ()
0 commit comments