rerun-failed-checks.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. #!/usr/bin/env python3
  2. # Usage: python3 rerun-failed-checks.py -t <github token> -r <repo> -b <branch>
  3. #
  4. # Description: This script will fetch the latest commit from a branch, and check the status of all check runs of the commit.
  5. # If any check run is not successful, it will trigger a rerun of the failed jobs.
  6. #
  7. # Default branch is master, default repo is emqx/emqx
  8. #
  9. # Limitation: only works for upstream repo, not for forked.
  10. import requests
  11. import http.client
  12. import json
  13. import os
  14. import sys
  15. import time
  16. import math
  17. from optparse import OptionParser
  18. job_black_list = [
  19. 'windows',
  20. 'publish_artifacts',
  21. 'stale'
  22. ]
  23. def fetch_latest_commit(token: str, repo: str, branch: str):
  24. url = f'https://api.github.com/repos/{repo}/commits/{branch}'
  25. headers = {'Accept': 'application/vnd.github+json',
  26. 'Authorization': f'Bearer {token}',
  27. 'X-GitHub-Api-Version': '2022-11-28',
  28. 'User-Agent': 'python3'
  29. }
  30. r = requests.get(url, headers=headers)
  31. if r.status_code == 200:
  32. res = r.json()
  33. return res
  34. else:
  35. print(
  36. f'Failed to fetch latest commit from {branch} branch, code: {r.status_code}')
  37. sys.exit(1)
  38. '''
  39. fetch check runs of a commit.
  40. @note, only works for public repos
  41. '''
  42. def fetch_check_runs(token: str, repo: str, ref: str):
  43. all_checks = []
  44. page = 1
  45. total_pages = 1
  46. per_page = 100
  47. failed_checks = []
  48. while page <= total_pages:
  49. print(f'Fetching check runs for page {page} of {total_pages} pages')
  50. url = f'https://api.github.com/repos/{repo}/commits/{ref}/check-runs?per_page={per_page}&page={page}'
  51. headers = {'Accept': 'application/vnd.github.v3+json',
  52. 'Authorization': f'Bearer {token}'
  53. }
  54. r = requests.get(url, headers=headers)
  55. if r.status_code == 200:
  56. resp = r.json()
  57. all_checks.extend(resp['check_runs'])
  58. page += 1
  59. if 'total_count' in resp and resp['total_count'] > per_page:
  60. total_pages = math.ceil(resp['total_count'] / per_page)
  61. else:
  62. print(f'Failed to fetch check runs {r.status_code}')
  63. sys.exit(1)
  64. for crun in all_checks:
  65. if crun['status'] == 'completed' and crun['conclusion'] != 'success':
  66. print('Failed check: ', crun['name'])
  67. failed_checks.append(
  68. {'id': crun['id'], 'name': crun['name'], 'url': crun['url']})
  69. else:
  70. # pretty print crun
  71. # print(json.dumps(crun, indent=4))
  72. print('successed:', crun['id'], crun['name'],
  73. crun['status'], crun['conclusion'])
  74. return failed_checks
  75. '''
  76. rerquest a check-run
  77. '''
  78. def trigger_build(failed_checks: list, repo: str, token: str):
  79. reruns = []
  80. for crun in failed_checks:
  81. if crun['name'].strip() in job_black_list:
  82. print(f'Skip black listed job {crun["name"]}')
  83. continue
  84. r = requests.get(crun['url'], headers={'Accept': 'application/vnd.github.v3+json',
  85. 'User-Agent': 'python3',
  86. 'Authorization': f'Bearer {token}'}
  87. )
  88. if r.status_code == 200:
  89. # url example: https://github.com/qzhuyan/emqx/actions/runs/4469557961/jobs/7852858687
  90. run_id = r.json()['details_url'].split('/')[-3]
  91. reruns.append(run_id)
  92. else:
  93. print(f'failed to fetch check run {crun["name"]}')
  94. # remove duplicates
  95. for run_id in set(reruns):
  96. url = f'https://api.github.com/repos/{repo}/actions/runs/{run_id}/rerun-failed-jobs'
  97. r = requests.post(url, headers={'Accept': 'application/vnd.github.v3+json',
  98. 'User-Agent': 'python3',
  99. 'Authorization': f'Bearer {token}'}
  100. )
  101. if r.status_code == 201:
  102. print(f'Successfully triggered build for {crun["name"]}')
  103. else:
  104. # Only complain but not exit.
  105. print(
  106. f'Failed to trigger rerun for {run_id}, {crun["name"]}: {r.status_code} : {r.text}')
  107. def main():
  108. parser = OptionParser()
  109. parser.add_option("-r", "--repo", dest="repo",
  110. help="github repo", default="emqx/emqx")
  111. parser.add_option("-t", "--token", dest="gh_token",
  112. help="github API token")
  113. parser.add_option("-b", "--branch", dest="branch", default='master',
  114. help="Branch that workflow runs on")
  115. (options, args) = parser.parse_args()
  116. # Get gh token from env var GITHUB_TOKEN if provided, else use the one from command line
  117. token = os.environ['GITHUB_TOKEN'] if 'GITHUB_TOKEN' in os.environ else options.gh_token
  118. target_commit = fetch_latest_commit(token, options.repo, options.branch)
  119. failed_checks = fetch_check_runs(token, options.repo, target_commit['sha'])
  120. trigger_build(failed_checks, options.repo, token)
  121. if __name__ == '__main__':
  122. main()