pr_commenter.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  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 os
  16. import json
  17. import logging
  18. import requests
  19. import argparse
  20. import api_diff_report
  21. import datetime
  22. import pytz
  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(os.path.expanduser(args.report),
  52. api_diff_report.API_DIFF_FILE_NAME)
  53. with open(diff_report_file, 'r') as file:
  54. report_content = file.read()
  55. if report_content: # Diff detected
  56. report = COMMENT_HIDDEN_IDENTIFIER + generate_markdown_title(
  57. TITLE_END_DIFF, commit, run_id) + report_content
  58. if comment_id:
  59. update_comment(token, comment_id, report)
  60. else:
  61. add_comment(token, pr_number, report)
  62. add_label(token, pr_number, PR_LABEL)
  63. else: # No diff
  64. if comment_id:
  65. report = COMMENT_HIDDEN_IDENTIFIER + generate_markdown_title(
  66. TITLE_END_NO_DIFF, commit, run_id)
  67. update_comment(token, comment_id, report)
  68. delete_label(token, pr_number, PR_LABEL)
  69. def generate_markdown_title(title, commit, run_id):
  70. pst_now = datetime.datetime.utcnow().astimezone(
  71. pytz.timezone('America/Los_Angeles'))
  72. return (
  73. title + 'Commit: %s\n' % commit
  74. + 'Last updated: %s \n' % pst_now.strftime('%a %b %e %H:%M %Z %G')
  75. + '**[View workflow logs & download artifacts]'
  76. + '(https://github.com/firebase/firebase-ios-sdk/actions/runs/%s)**\n\n'
  77. % run_id + '-----\n')
  78. RETRIES = 3
  79. BACKOFF = 5
  80. RETRY_STATUS = (403, 500, 502, 504)
  81. TIMEOUT = 5
  82. def requests_retry_session(retries=RETRIES,
  83. backoff_factor=BACKOFF,
  84. status_forcelist=RETRY_STATUS):
  85. session = requests.Session()
  86. retry = Retry(total=retries,
  87. read=retries,
  88. connect=retries,
  89. backoff_factor=backoff_factor,
  90. status_forcelist=status_forcelist)
  91. adapter = HTTPAdapter(max_retries=retry)
  92. session.mount('http://', adapter)
  93. session.mount('https://', adapter)
  94. return session
  95. def get_comment_id(token, issue_number, comment_identifier):
  96. comments = list_comments(token, issue_number)
  97. for comment in comments:
  98. if comment_identifier in comment['body']:
  99. return comment['id']
  100. return None
  101. def list_comments(token, issue_number):
  102. """https://docs.github.com/en/rest/reference/issues#list-issue-comments"""
  103. url = f'{GITHUB_API_URL}/issues/{issue_number}/comments'
  104. headers = {
  105. 'Accept': 'application/vnd.github.v3+json',
  106. 'Authorization': f'token {token}'
  107. }
  108. with requests_retry_session().get(url, headers=headers,
  109. timeout=TIMEOUT) as response:
  110. logging.info("list_comments: %s response: %s", url, response)
  111. return response.json()
  112. def add_comment(token, issue_number, comment):
  113. """https://docs.github.com/en/rest/reference/issues#create-an-issue-comment"""
  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. data = {'body': comment}
  120. with requests.post(url,
  121. headers=headers,
  122. data=json.dumps(data),
  123. timeout=TIMEOUT) as response:
  124. logging.info("add_comment: %s response: %s", url, response)
  125. def update_comment(token, comment_id, comment):
  126. """https://docs.github.com/en/rest/reference/issues#update-an-issue-comment"""
  127. url = f'{GITHUB_API_URL}/issues/comments/{comment_id}'
  128. headers = {
  129. 'Accept': 'application/vnd.github.v3+json',
  130. 'Authorization': f'token {token}'
  131. }
  132. data = {'body': comment}
  133. with requests_retry_session().patch(url,
  134. headers=headers,
  135. data=json.dumps(data),
  136. timeout=TIMEOUT) as response:
  137. logging.info("update_comment: %s response: %s", url, response)
  138. def delete_comment(token, comment_id):
  139. """https://docs.github.com/en/rest/reference/issues#delete-an-issue-comment"""
  140. url = f'{GITHUB_API_URL}/issues/comments/{comment_id}'
  141. headers = {
  142. 'Accept': 'application/vnd.github.v3+json',
  143. 'Authorization': f'token {token}'
  144. }
  145. with requests.delete(url, headers=headers, timeout=TIMEOUT) as response:
  146. logging.info("delete_comment: %s response: %s", url, response)
  147. def add_label(token, issue_number, label):
  148. """https://docs.github.com/en/rest/reference/issues#add-labels-to-an-issue"""
  149. url = f'{GITHUB_API_URL}/issues/{issue_number}/labels'
  150. headers = {
  151. 'Accept': 'application/vnd.github.v3+json',
  152. 'Authorization': f'token {token}'
  153. }
  154. data = [label]
  155. with requests.post(url,
  156. headers=headers,
  157. data=json.dumps(data),
  158. timeout=TIMEOUT) as response:
  159. logging.info("add_label: %s response: %s", url, response)
  160. def delete_label(token, issue_number, label):
  161. """https://docs.github.com/en/rest/reference/issues#delete-a-label"""
  162. url = f'{GITHUB_API_URL}/issues/{issue_number}/labels/{label}'
  163. headers = {
  164. 'Accept': 'application/vnd.github.v3+json',
  165. 'Authorization': f'token {token}'
  166. }
  167. with requests.delete(url, headers=headers, timeout=TIMEOUT) as response:
  168. logging.info("delete_label: %s response: %s", url, response)
  169. def parse_cmdline_args():
  170. parser = argparse.ArgumentParser()
  171. parser.add_argument('-s', '--stage')
  172. parser.add_argument('-r', '--report')
  173. parser.add_argument('-t', '--token')
  174. parser.add_argument('-n', '--pr_number')
  175. parser.add_argument('-c', '--commit')
  176. parser.add_argument('-i', '--run_id')
  177. args = parser.parse_args()
  178. return args
  179. if __name__ == '__main__':
  180. main()