1# Copyright 2018 The gRPC Authors 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15from __future__ import print_function 16 17import datetime 18import json 19import os 20import sys 21import time 22import traceback 23 24import jwt 25import requests 26 27_GITHUB_API_PREFIX = "https://api.github.com" 28_GITHUB_REPO = "grpc/grpc" 29_GITHUB_APP_ID = 22338 30_INSTALLATION_ID = 519109 31 32_ACCESS_TOKEN_CACHE = None 33_ACCESS_TOKEN_FETCH_RETRIES = 6 34_ACCESS_TOKEN_FETCH_RETRIES_INTERVAL_S = 15 35 36_CHANGE_LABELS = { 37 -1: "improvement", 38 0: "none", 39 1: "low", 40 2: "medium", 41 3: "high", 42} 43 44_INCREASE_DECREASE = { 45 -1: "decrease", 46 0: "neutral", 47 1: "increase", 48} 49 50 51def _jwt_token(): 52 github_app_key = open( 53 os.path.join( 54 os.environ["KOKORO_KEYSTORE_DIR"], "73836_grpc_checks_private_key" 55 ), 56 "rb", 57 ).read() 58 return jwt.encode( 59 { 60 "iat": int(time.time()), 61 "exp": int(time.time() + 60 * 10), # expire in 10 minutes 62 "iss": _GITHUB_APP_ID, 63 }, 64 github_app_key, 65 algorithm="RS256", 66 ) 67 68 69def _access_token(): 70 global _ACCESS_TOKEN_CACHE 71 if _ACCESS_TOKEN_CACHE == None or _ACCESS_TOKEN_CACHE["exp"] < time.time(): 72 for i in range(_ACCESS_TOKEN_FETCH_RETRIES): 73 resp = requests.post( 74 url="https://api.github.com/app/installations/%s/access_tokens" 75 % _INSTALLATION_ID, 76 headers={ 77 "Authorization": "Bearer %s" % _jwt_token(), 78 "Accept": "application/vnd.github.machine-man-preview+json", 79 }, 80 ) 81 82 try: 83 _ACCESS_TOKEN_CACHE = { 84 "token": resp.json()["token"], 85 "exp": time.time() + 60, 86 } 87 break 88 except (KeyError, ValueError): 89 traceback.print_exc() 90 print("HTTP Status %d %s" % (resp.status_code, resp.reason)) 91 print("Fetch access token from Github API failed:") 92 print(resp.text) 93 if i != _ACCESS_TOKEN_FETCH_RETRIES - 1: 94 print( 95 "Retrying after %.2f second." 96 % _ACCESS_TOKEN_FETCH_RETRIES_INTERVAL_S 97 ) 98 time.sleep(_ACCESS_TOKEN_FETCH_RETRIES_INTERVAL_S) 99 else: 100 print("error: Unable to fetch access token, exiting...") 101 sys.exit(0) 102 103 return _ACCESS_TOKEN_CACHE["token"] 104 105 106def _call(url, method="GET", json=None): 107 if not url.startswith("https://"): 108 url = _GITHUB_API_PREFIX + url 109 headers = { 110 "Authorization": "Bearer %s" % _access_token(), 111 "Accept": "application/vnd.github.antiope-preview+json", 112 } 113 return requests.request(method=method, url=url, headers=headers, json=json) 114 115 116def _latest_commit(): 117 resp = _call( 118 "/repos/%s/pulls/%s/commits" 119 % (_GITHUB_REPO, os.environ["KOKORO_GITHUB_PULL_REQUEST_NUMBER"]) 120 ) 121 return resp.json()[-1] 122 123 124def check_on_pr(name, summary, success=True): 125 """Create/Update a check on current pull request. 126 127 The check runs are aggregated by their name, so newer check will update the 128 older check with the same name. 129 130 Requires environment variable 'KOKORO_GITHUB_PULL_REQUEST_NUMBER' to indicate which pull request 131 should be updated. 132 133 Args: 134 name: The name of the check. 135 summary: A str in Markdown to be used as the detail information of the check. 136 success: A bool indicates whether the check is succeed or not. 137 """ 138 if "KOKORO_GIT_COMMIT" not in os.environ: 139 print("Missing KOKORO_GIT_COMMIT env var: not checking") 140 return 141 if "KOKORO_KEYSTORE_DIR" not in os.environ: 142 print("Missing KOKORO_KEYSTORE_DIR env var: not checking") 143 return 144 if "KOKORO_GITHUB_PULL_REQUEST_NUMBER" not in os.environ: 145 print("Missing KOKORO_GITHUB_PULL_REQUEST_NUMBER env var: not checking") 146 return 147 MAX_SUMMARY_LEN = 65400 148 if len(summary) > MAX_SUMMARY_LEN: 149 # Drop some hints to the log should someone come looking for what really happened! 150 print("Clipping too long summary") 151 print(summary) 152 summary = summary[:MAX_SUMMARY_LEN] + "\n\n\n... CLIPPED (too long)" 153 completion_time = ( 154 str(datetime.datetime.utcnow().replace(microsecond=0).isoformat()) + "Z" 155 ) 156 resp = _call( 157 "/repos/%s/check-runs" % _GITHUB_REPO, 158 method="POST", 159 json={ 160 "name": name, 161 "head_sha": os.environ["KOKORO_GIT_COMMIT"], 162 "status": "completed", 163 "completed_at": completion_time, 164 "conclusion": "success" if success else "failure", 165 "output": { 166 "title": name, 167 "summary": summary, 168 }, 169 }, 170 ) 171 print( 172 "Result of Creating/Updating Check on PR:", 173 json.dumps(resp.json(), indent=2), 174 ) 175 176 177def label_significance_on_pr(name, change, labels=_CHANGE_LABELS): 178 """Add a label to the PR indicating the significance of the check. 179 180 Requires environment variable 'KOKORO_GITHUB_PULL_REQUEST_NUMBER' to indicate which pull request 181 should be updated. 182 183 Args: 184 name: The name of the label. 185 value: A str in Markdown to be used as the detail information of the label. 186 """ 187 if change < min(list(labels.keys())): 188 change = min(list(labels.keys())) 189 if change > max(list(labels.keys())): 190 change = max(list(labels.keys())) 191 value = labels[change] 192 if "KOKORO_GIT_COMMIT" not in os.environ: 193 print("Missing KOKORO_GIT_COMMIT env var: not checking") 194 return 195 if "KOKORO_KEYSTORE_DIR" not in os.environ: 196 print("Missing KOKORO_KEYSTORE_DIR env var: not checking") 197 return 198 if "KOKORO_GITHUB_PULL_REQUEST_NUMBER" not in os.environ: 199 print("Missing KOKORO_GITHUB_PULL_REQUEST_NUMBER env var: not checking") 200 return 201 existing = _call( 202 "/repos/%s/issues/%s/labels" 203 % (_GITHUB_REPO, os.environ["KOKORO_GITHUB_PULL_REQUEST_NUMBER"]), 204 method="GET", 205 ).json() 206 print("Result of fetching labels on PR:", existing) 207 new = [x["name"] for x in existing if not x["name"].startswith(name + "/")] 208 new.append(name + "/" + value) 209 resp = _call( 210 "/repos/%s/issues/%s/labels" 211 % (_GITHUB_REPO, os.environ["KOKORO_GITHUB_PULL_REQUEST_NUMBER"]), 212 method="PUT", 213 json=new, 214 ) 215 print("Result of setting labels on PR:", resp.text) 216 217 218def label_increase_decrease_on_pr(name, change, significant): 219 if change <= -significant: 220 label_significance_on_pr(name, -1, _INCREASE_DECREASE) 221 elif change >= significant: 222 label_significance_on_pr(name, 1, _INCREASE_DECREASE) 223 else: 224 label_significance_on_pr(name, 0, _INCREASE_DECREASE) 225