check_on_pr.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  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. from __future__ import print_function
  15. import datetime
  16. import json
  17. import os
  18. import sys
  19. import time
  20. import traceback
  21. import jwt
  22. import requests
  23. _GITHUB_API_PREFIX = 'https://api.github.com'
  24. _GITHUB_REPO = 'grpc/grpc'
  25. _GITHUB_APP_ID = 22338
  26. _INSTALLATION_ID = 519109
  27. _ACCESS_TOKEN_CACHE = None
  28. _ACCESS_TOKEN_FETCH_RETRIES = 6
  29. _ACCESS_TOKEN_FETCH_RETRIES_INTERVAL_S = 15
  30. _CHANGE_LABELS = {
  31. -1: 'improvement',
  32. 0: 'none',
  33. 1: 'low',
  34. 2: 'medium',
  35. 3: 'high',
  36. }
  37. def _jwt_token():
  38. github_app_key = open(
  39. os.path.join(os.environ['KOKORO_KEYSTORE_DIR'],
  40. '73836_grpc_checks_private_key'), 'rb').read()
  41. return jwt.encode(
  42. {
  43. 'iat': int(time.time()),
  44. 'exp': int(time.time() + 60 * 10), # expire in 10 minutes
  45. 'iss': _GITHUB_APP_ID,
  46. },
  47. github_app_key,
  48. algorithm='RS256')
  49. def _access_token():
  50. global _ACCESS_TOKEN_CACHE
  51. if _ACCESS_TOKEN_CACHE == None or _ACCESS_TOKEN_CACHE['exp'] < time.time():
  52. for i in range(_ACCESS_TOKEN_FETCH_RETRIES):
  53. resp = requests.post(
  54. url='https://api.github.com/app/installations/%s/access_tokens'
  55. % _INSTALLATION_ID,
  56. headers={
  57. 'Authorization': 'Bearer %s' % _jwt_token(),
  58. 'Accept': 'application/vnd.github.machine-man-preview+json',
  59. })
  60. try:
  61. _ACCESS_TOKEN_CACHE = {
  62. 'token': resp.json()['token'],
  63. 'exp': time.time() + 60
  64. }
  65. break
  66. except (KeyError, ValueError):
  67. traceback.print_exc()
  68. print('HTTP Status %d %s' % (resp.status_code, resp.reason))
  69. print("Fetch access token from Github API failed:")
  70. print(resp.text)
  71. if i != _ACCESS_TOKEN_FETCH_RETRIES - 1:
  72. print('Retrying after %.2f second.' %
  73. _ACCESS_TOKEN_FETCH_RETRIES_INTERVAL_S)
  74. time.sleep(_ACCESS_TOKEN_FETCH_RETRIES_INTERVAL_S)
  75. else:
  76. print("error: Unable to fetch access token, exiting...")
  77. sys.exit(0)
  78. return _ACCESS_TOKEN_CACHE['token']
  79. def _call(url, method='GET', json=None):
  80. if not url.startswith('https://'):
  81. url = _GITHUB_API_PREFIX + url
  82. headers = {
  83. 'Authorization': 'Bearer %s' % _access_token(),
  84. 'Accept': 'application/vnd.github.antiope-preview+json',
  85. }
  86. return requests.request(method=method, url=url, headers=headers, json=json)
  87. def _latest_commit():
  88. resp = _call(
  89. '/repos/%s/pulls/%s/commits' %
  90. (_GITHUB_REPO, os.environ['KOKORO_GITHUB_PULL_REQUEST_NUMBER']))
  91. return resp.json()[-1]
  92. def check_on_pr(name, summary, success=True):
  93. """Create/Update a check on current pull request.
  94. The check runs are aggregated by their name, so newer check will update the
  95. older check with the same name.
  96. Requires environment variable 'KOKORO_GITHUB_PULL_REQUEST_NUMBER' to indicate which pull request
  97. should be updated.
  98. Args:
  99. name: The name of the check.
  100. summary: A str in Markdown to be used as the detail information of the check.
  101. success: A bool indicates whether the check is succeed or not.
  102. """
  103. if 'KOKORO_GIT_COMMIT' not in os.environ:
  104. print('Missing KOKORO_GIT_COMMIT env var: not checking')
  105. return
  106. if 'KOKORO_KEYSTORE_DIR' not in os.environ:
  107. print('Missing KOKORO_KEYSTORE_DIR env var: not checking')
  108. return
  109. if 'KOKORO_GITHUB_PULL_REQUEST_NUMBER' not in os.environ:
  110. print('Missing KOKORO_GITHUB_PULL_REQUEST_NUMBER env var: not checking')
  111. return
  112. MAX_SUMMARY_LEN = 65400
  113. if len(summary) > MAX_SUMMARY_LEN:
  114. # Drop some hints to the log should someone come looking for what really happened!
  115. print('Clipping too long summary')
  116. print(summary)
  117. summary = summary[:MAX_SUMMARY_LEN] + '\n\n\n... CLIPPED (too long)'
  118. completion_time = str(
  119. datetime.datetime.utcnow().replace(microsecond=0).isoformat()) + 'Z'
  120. resp = _call('/repos/%s/check-runs' % _GITHUB_REPO,
  121. method='POST',
  122. json={
  123. 'name': name,
  124. 'head_sha': os.environ['KOKORO_GIT_COMMIT'],
  125. 'status': 'completed',
  126. 'completed_at': completion_time,
  127. 'conclusion': 'success' if success else 'failure',
  128. 'output': {
  129. 'title': name,
  130. 'summary': summary,
  131. }
  132. })
  133. print('Result of Creating/Updating Check on PR:',
  134. json.dumps(resp.json(), indent=2))
  135. def label_significance_on_pr(name, change):
  136. """Add a label to the PR indicating the significance of the check.
  137. Requires environment variable 'KOKORO_GITHUB_PULL_REQUEST_NUMBER' to indicate which pull request
  138. should be updated.
  139. Args:
  140. name: The name of the label.
  141. value: A str in Markdown to be used as the detail information of the label.
  142. """
  143. if change < min(list(_CHANGE_LABELS.keys())):
  144. change = min(list(_CHANGE_LABELS.keys()))
  145. if change > max(list(_CHANGE_LABELS.keys())):
  146. change = max(list(_CHANGE_LABELS.keys()))
  147. value = _CHANGE_LABELS[change]
  148. if 'KOKORO_GIT_COMMIT' not in os.environ:
  149. print('Missing KOKORO_GIT_COMMIT env var: not checking')
  150. return
  151. if 'KOKORO_KEYSTORE_DIR' not in os.environ:
  152. print('Missing KOKORO_KEYSTORE_DIR env var: not checking')
  153. return
  154. if 'KOKORO_GITHUB_PULL_REQUEST_NUMBER' not in os.environ:
  155. print('Missing KOKORO_GITHUB_PULL_REQUEST_NUMBER env var: not checking')
  156. return
  157. existing = _call(
  158. '/repos/%s/issues/%s/labels' %
  159. (_GITHUB_REPO, os.environ['KOKORO_GITHUB_PULL_REQUEST_NUMBER']),
  160. method='GET').json()
  161. print('Result of fetching labels on PR:', existing)
  162. new = [x['name'] for x in existing if not x['name'].startswith(name + '/')]
  163. new.append(name + '/' + value)
  164. resp = _call(
  165. '/repos/%s/issues/%s/labels' %
  166. (_GITHUB_REPO, os.environ['KOKORO_GITHUB_PULL_REQUEST_NUMBER']),
  167. method='PUT',
  168. json=new)
  169. print('Result of setting labels on PR:', resp.text)