pr_commenter.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2023 Google LLC
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the 'License');
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an 'AS IS' BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. import argparse
  16. import datetime
  17. import json
  18. import logging
  19. import os
  20. import api_diff_report
  21. import pytz
  22. import requests
  23. from requests.adapters import HTTPAdapter
  24. from requests.packages.urllib3.util.retry import Retry
  25. STAGES_PROGRESS = 'progress'
  26. STAGES_END = 'end'
  27. TITLE_PROGESS = '## ⏳  Detecting API diff in progress...\n'
  28. TITLE_END_DIFF = '## Apple API Diff Report\n'
  29. TITLE_END_NO_DIFF = '## ✅  No API diff detected\n'
  30. COMMENT_HIDDEN_IDENTIFIER = '\r\n<hidden value="diff-report"></hidden>\r\n'
  31. GITHUB_API_URL = 'https://api.github.com/repos/firebase/firebase-ios-sdk'
  32. PR_LABEL = 'public-api-change'
  33. def main():
  34. logging.getLogger().setLevel(logging.INFO)
  35. # Parse command-line arguments
  36. args = parse_cmdline_args()
  37. stage = args.stage
  38. token = args.token
  39. pr_number = args.pr_number
  40. commit = args.commit
  41. run_id = args.run_id
  42. report = ''
  43. comment_id = get_comment_id(token, pr_number, COMMENT_HIDDEN_IDENTIFIER)
  44. if stage == STAGES_PROGRESS:
  45. if comment_id:
  46. report = COMMENT_HIDDEN_IDENTIFIER
  47. report += generate_markdown_title(TITLE_PROGESS, commit, run_id)
  48. update_comment(token, comment_id, report)
  49. delete_label(token, pr_number, PR_LABEL)
  50. elif stage == STAGES_END:
  51. diff_report_file = os.path.join(
  52. os.path.expanduser(args.report), api_diff_report.API_DIFF_FILE_NAME
  53. )
  54. with open(diff_report_file, 'r') as file:
  55. report_content = file.read()
  56. if report_content: # Diff detected
  57. report = (
  58. COMMENT_HIDDEN_IDENTIFIER
  59. + generate_markdown_title(TITLE_END_DIFF, commit, run_id)
  60. + report_content
  61. )
  62. if comment_id:
  63. update_comment(token, comment_id, report)
  64. else:
  65. add_comment(token, pr_number, report)
  66. add_label(token, pr_number, PR_LABEL)
  67. else: # No diff
  68. if comment_id:
  69. report = COMMENT_HIDDEN_IDENTIFIER + generate_markdown_title(
  70. TITLE_END_NO_DIFF, commit, run_id
  71. )
  72. update_comment(token, comment_id, report)
  73. delete_label(token, pr_number, PR_LABEL)
  74. def generate_markdown_title(title, commit, run_id):
  75. pst_now = datetime.datetime.utcnow().astimezone(
  76. pytz.timezone('America/Los_Angeles')
  77. )
  78. return (
  79. title
  80. + 'Commit: %s\n' % commit
  81. + 'Last updated: %s \n' % pst_now.strftime('%a %b %e %H:%M %Z %G')
  82. + '**[View workflow logs & download artifacts]'
  83. + '(https://github.com/firebase/firebase-ios-sdk/actions/runs/%s)**\n\n'
  84. % run_id
  85. + '-----\n'
  86. )
  87. RETRIES = 3
  88. BACKOFF = 5
  89. RETRY_STATUS = (403, 500, 502, 504)
  90. TIMEOUT = 5
  91. def requests_retry_session(
  92. retries=RETRIES, backoff_factor=BACKOFF, status_forcelist=RETRY_STATUS
  93. ):
  94. session = requests.Session()
  95. retry = Retry(
  96. total=retries,
  97. read=retries,
  98. connect=retries,
  99. backoff_factor=backoff_factor,
  100. status_forcelist=status_forcelist,
  101. )
  102. adapter = HTTPAdapter(max_retries=retry)
  103. session.mount('http://', adapter)
  104. session.mount('https://', adapter)
  105. return session
  106. def get_comment_id(token, issue_number, comment_identifier):
  107. comments = list_comments(token, issue_number)
  108. for comment in comments:
  109. if comment_identifier in comment['body']:
  110. return comment['id']
  111. return None
  112. def list_comments(token, issue_number):
  113. """https://docs.github.com/en/rest/reference/issues#list-issue-comments"""
  114. url = f'{GITHUB_API_URL}/issues/{issue_number}/comments'
  115. headers = {
  116. 'Accept': 'application/vnd.github.v3+json',
  117. 'Authorization': f'token {token}',
  118. }
  119. with requests_retry_session().get(
  120. url, headers=headers, timeout=TIMEOUT
  121. ) as response:
  122. logging.info('list_comments: %s response: %s', url, response)
  123. return response.json()
  124. def add_comment(token, issue_number, comment):
  125. """https://docs.github.com/en/rest/reference/issues#create-an-issue-comment"""
  126. url = f'{GITHUB_API_URL}/issues/{issue_number}/comments'
  127. headers = {
  128. 'Accept': 'application/vnd.github.v3+json',
  129. 'Authorization': f'token {token}',
  130. }
  131. data = {'body': comment}
  132. with requests.post(
  133. url, headers=headers, data=json.dumps(data), timeout=TIMEOUT
  134. ) as response:
  135. logging.info('add_comment: %s response: %s', url, response)
  136. def update_comment(token, comment_id, comment):
  137. """https://docs.github.com/en/rest/reference/issues#update-an-issue-comment"""
  138. url = f'{GITHUB_API_URL}/issues/comments/{comment_id}'
  139. headers = {
  140. 'Accept': 'application/vnd.github.v3+json',
  141. 'Authorization': f'token {token}',
  142. }
  143. data = {'body': comment}
  144. with requests_retry_session().patch(
  145. url, headers=headers, data=json.dumps(data), timeout=TIMEOUT
  146. ) as response:
  147. logging.info('update_comment: %s response: %s', url, response)
  148. def delete_comment(token, comment_id):
  149. """https://docs.github.com/en/rest/reference/issues#delete-an-issue-comment"""
  150. url = f'{GITHUB_API_URL}/issues/comments/{comment_id}'
  151. headers = {
  152. 'Accept': 'application/vnd.github.v3+json',
  153. 'Authorization': f'token {token}',
  154. }
  155. with requests.delete(url, headers=headers, timeout=TIMEOUT) as response:
  156. logging.info('delete_comment: %s response: %s', url, response)
  157. def add_label(token, issue_number, label):
  158. """https://docs.github.com/en/rest/reference/issues#add-labels-to-an-issue"""
  159. url = f'{GITHUB_API_URL}/issues/{issue_number}/labels'
  160. headers = {
  161. 'Accept': 'application/vnd.github.v3+json',
  162. 'Authorization': f'token {token}',
  163. }
  164. data = [label]
  165. with requests.post(
  166. url, headers=headers, data=json.dumps(data), timeout=TIMEOUT
  167. ) as response:
  168. logging.info('add_label: %s response: %s', url, response)
  169. def delete_label(token, issue_number, label):
  170. """https://docs.github.com/en/rest/reference/issues#delete-a-label"""
  171. url = f'{GITHUB_API_URL}/issues/{issue_number}/labels/{label}'
  172. headers = {
  173. 'Accept': 'application/vnd.github.v3+json',
  174. 'Authorization': f'token {token}',
  175. }
  176. with requests.delete(url, headers=headers, timeout=TIMEOUT) as response:
  177. logging.info('delete_label: %s response: %s', url, response)
  178. def parse_cmdline_args():
  179. parser = argparse.ArgumentParser()
  180. parser.add_argument('-s', '--stage')
  181. parser.add_argument('-r', '--report')
  182. parser.add_argument('-t', '--token')
  183. parser.add_argument('-n', '--pr_number')
  184. parser.add_argument('-c', '--commit')
  185. parser.add_argument('-i', '--run_id')
  186. args = parser.parse_args()
  187. return args
  188. if __name__ == '__main__':
  189. main()