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(os.environ['KOKORO_KEYSTORE_DIR'], 54 '73836_grpc_checks_private_key'), 'rb').read() 55 return jwt.encode( 56 { 57 'iat': int(time.time()), 58 'exp': int(time.time() + 60 * 10), # expire in 10 minutes 59 'iss': _GITHUB_APP_ID, 60 }, 61 github_app_key, 62 algorithm='RS256') 63 64 65def _access_token(): 66 global _ACCESS_TOKEN_CACHE 67 if _ACCESS_TOKEN_CACHE == None or _ACCESS_TOKEN_CACHE['exp'] < time.time(): 68 for i in range(_ACCESS_TOKEN_FETCH_RETRIES): 69 resp = requests.post( 70 url='https://api.github.com/app/installations/%s/access_tokens' 71 % _INSTALLATION_ID, 72 headers={ 73 'Authorization': 'Bearer %s' % _jwt_token(), 74 'Accept': 'application/vnd.github.machine-man-preview+json', 75 }) 76 77 try: 78 _ACCESS_TOKEN_CACHE = { 79 'token': resp.json()['token'], 80 'exp': time.time() + 60 81 } 82 break 83 except (KeyError, ValueError): 84 traceback.print_exc() 85 print('HTTP Status %d %s' % (resp.status_code, resp.reason)) 86 print("Fetch access token from Github API failed:") 87 print(resp.text) 88 if i != _ACCESS_TOKEN_FETCH_RETRIES - 1: 89 print('Retrying after %.2f second.' % 90 _ACCESS_TOKEN_FETCH_RETRIES_INTERVAL_S) 91 time.sleep(_ACCESS_TOKEN_FETCH_RETRIES_INTERVAL_S) 92 else: 93 print("error: Unable to fetch access token, exiting...") 94 sys.exit(0) 95 96 return _ACCESS_TOKEN_CACHE['token'] 97 98 99def _call(url, method='GET', json=None): 100 if not url.startswith('https://'): 101 url = _GITHUB_API_PREFIX + url 102 headers = { 103 'Authorization': 'Bearer %s' % _access_token(), 104 'Accept': 'application/vnd.github.antiope-preview+json', 105 } 106 return requests.request(method=method, url=url, headers=headers, json=json) 107 108 109def _latest_commit(): 110 resp = _call( 111 '/repos/%s/pulls/%s/commits' % 112 (_GITHUB_REPO, os.environ['KOKORO_GITHUB_PULL_REQUEST_NUMBER'])) 113 return resp.json()[-1] 114 115 116def check_on_pr(name, summary, success=True): 117 """Create/Update a check on current pull request. 118 119 The check runs are aggregated by their name, so newer check will update the 120 older check with the same name. 121 122 Requires environment variable 'KOKORO_GITHUB_PULL_REQUEST_NUMBER' to indicate which pull request 123 should be updated. 124 125 Args: 126 name: The name of the check. 127 summary: A str in Markdown to be used as the detail information of the check. 128 success: A bool indicates whether the check is succeed or not. 129 """ 130 if 'KOKORO_GIT_COMMIT' not in os.environ: 131 print('Missing KOKORO_GIT_COMMIT env var: not checking') 132 return 133 if 'KOKORO_KEYSTORE_DIR' not in os.environ: 134 print('Missing KOKORO_KEYSTORE_DIR env var: not checking') 135 return 136 if 'KOKORO_GITHUB_PULL_REQUEST_NUMBER' not in os.environ: 137 print('Missing KOKORO_GITHUB_PULL_REQUEST_NUMBER env var: not checking') 138 return 139 MAX_SUMMARY_LEN = 65400 140 if len(summary) > MAX_SUMMARY_LEN: 141 # Drop some hints to the log should someone come looking for what really happened! 142 print('Clipping too long summary') 143 print(summary) 144 summary = summary[:MAX_SUMMARY_LEN] + '\n\n\n... CLIPPED (too long)' 145 completion_time = str( 146 datetime.datetime.utcnow().replace(microsecond=0).isoformat()) + 'Z' 147 resp = _call('/repos/%s/check-runs' % _GITHUB_REPO, 148 method='POST', 149 json={ 150 'name': name, 151 'head_sha': os.environ['KOKORO_GIT_COMMIT'], 152 'status': 'completed', 153 'completed_at': completion_time, 154 'conclusion': 'success' if success else 'failure', 155 'output': { 156 'title': name, 157 'summary': summary, 158 } 159 }) 160 print('Result of Creating/Updating Check on PR:', 161 json.dumps(resp.json(), indent=2)) 162 163 164def label_significance_on_pr(name, change, labels=_CHANGE_LABELS): 165 """Add a label to the PR indicating the significance of the check. 166 167 Requires environment variable 'KOKORO_GITHUB_PULL_REQUEST_NUMBER' to indicate which pull request 168 should be updated. 169 170 Args: 171 name: The name of the label. 172 value: A str in Markdown to be used as the detail information of the label. 173 """ 174 if change < min(list(labels.keys())): 175 change = min(list(labels.keys())) 176 if change > max(list(labels.keys())): 177 change = max(list(labels.keys())) 178 value = labels[change] 179 if 'KOKORO_GIT_COMMIT' not in os.environ: 180 print('Missing KOKORO_GIT_COMMIT env var: not checking') 181 return 182 if 'KOKORO_KEYSTORE_DIR' not in os.environ: 183 print('Missing KOKORO_KEYSTORE_DIR env var: not checking') 184 return 185 if 'KOKORO_GITHUB_PULL_REQUEST_NUMBER' not in os.environ: 186 print('Missing KOKORO_GITHUB_PULL_REQUEST_NUMBER env var: not checking') 187 return 188 existing = _call( 189 '/repos/%s/issues/%s/labels' % 190 (_GITHUB_REPO, os.environ['KOKORO_GITHUB_PULL_REQUEST_NUMBER']), 191 method='GET').json() 192 print('Result of fetching labels on PR:', existing) 193 new = [x['name'] for x in existing if not x['name'].startswith(name + '/')] 194 new.append(name + '/' + value) 195 resp = _call( 196 '/repos/%s/issues/%s/labels' % 197 (_GITHUB_REPO, os.environ['KOKORO_GITHUB_PULL_REQUEST_NUMBER']), 198 method='PUT', 199 json=new) 200 print('Result of setting labels on PR:', resp.text) 201 202 203def label_increase_decrease_on_pr(name, change, significant): 204 if change <= -significant: 205 label_significance_on_pr(name, -1, _INCREASE_DECREASE) 206 elif change >= significant: 207 label_significance_on_pr(name, 1, _INCREASE_DECREASE) 208 else: 209 label_significance_on_pr(name, 0, _INCREASE_DECREASE) 210