diff --git a/.github/workflows/persubmit.yml b/.github/workflows/persubmit.yml index 244383cff9807..51b2cbad4c3da 100644 --- a/.github/workflows/persubmit.yml +++ b/.github/workflows/persubmit.yml @@ -57,6 +57,21 @@ jobs: ti diagnose ti test -vr2 -t2 + check_previous_run: + name: Checks the Workflow Run of the Previous Commit + runs-on: ubuntu-latest + if: ${{ contains(github.event.pull_request.labels.*.name, 'skip ci') || github.event.sender.login == 'taichi-gardener' }} + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Check the previous run + run: | + PR=${{ github.event.pull_request.number }} + SHA=${{ github.event.pull_request.head.sha }} + python misc/ci_check_previous_run.py --pr ${PR} --sha ${SHA} + code_format: name: Code Format runs-on: ubuntu-latest diff --git a/misc/ci_check_previous_run.py b/misc/ci_check_previous_run.py new file mode 100644 index 0000000000000..7b4a8cb1a5b99 --- /dev/null +++ b/misc/ci_check_previous_run.py @@ -0,0 +1,133 @@ +import argparse +import json +import logging +import time +import urllib.request as ur +import sys + +API_PREFIX = 'https://api.github.com/repos/taichi-dev/taichi' +SHA = 'sha' + + +def make_api_url(p): + return f'{API_PREFIX}/{p}' + + +def send_request(url): + logging.debug(f'request={url}') + return ur.urlopen(url) + + +def get_commits(pr): + url = make_api_url(f'pulls/{pr}/commits') + f = send_request(url) + return json.loads(f.read()) + + +def locate_previous_commit_sha(commits, head_sha): + """ + Returns the previous commit of |head_sha| in PR's |commits|. + """ + assert commits[-1][SHA] == head_sha + if len(commits) < 2: + return None + return commits[-2][SHA] + + +def get_workflow_runs(page_id): + # https://docs.github.com/en/rest/reference/actions#list-workflow-runs-for-a-repository + url = make_api_url(f'actions/runs?page={page_id}') + f = send_request(url) + return json.loads(f.read()) + + +def is_desired_workflow(run_json): + """ + Checks if this run is for the "Presubmit Checks" workflow. + """ + # Each workflow has a fixed ID. + # For the "Persubmit Checks" workflow, it is: + # https://api.github.com/repos/taichi-dev/taichi/actions/workflows/1291024 + DESIRED_ID = 1291024 + return run_json['workflow_id'] == DESIRED_ID + + +def locate_workflow_run_id(sha): + done = False + page_id = 0 + while not done: + # Note that the REST API to get runs paginates the results. + runs = get_workflow_runs(page_id)['workflow_runs'] + for r in runs: + if r['head_sha'] == sha and is_desired_workflow(r): + return r['id'] + page_id += 1 + return '' + + +def get_status_of_run(run_id): + """ + Waits for run identified by |run_id| to complete and returns its status. + """ + url = make_api_url(f'actions/runs/{run_id}') + start = time.time() + retries = 0 + MAX_TIMEOUT = 60 * 60 # 1 hour + while True: + f = send_request(url) + j = json.loads(f.read()) + # https://developer.github.com/v3/checks/runs/#create-a-check-run + if j['status'] == 'completed': + c = j['conclusion'] + logging.debug(f'run={run_id} conclusion={c}') + return c == 'success' + + if time.time() - start > MAX_TIMEOUT: + return False + retries += 1 + logging.info( + f'Waiting to get the status of run={run_id} (url={url}). retries={retries}' + ) + time.sleep(15) + return False + + +def get_cmd_args(): + parser = argparse.ArgumentParser() + parser.add_argument('--pr', help='PR number') + parser.add_argument('--sha', help='Head commit SHA in the PR') + return parser.parse_args() + + +def main(): + logging.basicConfig(format='%(asctime)s %(levelname)s:%(message)s', + level=logging.DEBUG, + datefmt='%Y-%m-%d %H:%M:%S') + args = get_cmd_args() + + pr = args.pr + commits = get_commits(pr) + num_commits = len(commits) + logging.debug(f'\nPR={pr} #commits={num_commits}') + + head_sha = args.sha + prev_sha = locate_previous_commit_sha(commits, head_sha) + logging.debug(f'SHA: head={head_sha} prev={prev_sha}') + if prev_sha is None: + logging.info(f'First commit in PR={pr}, no previous run to check') + # First commit in the PR + return 0 + + run_id = locate_workflow_run_id(prev_sha) + if not run_id: + logging.warning(f'Could not find the workflow run for SHA={prev_sha}') + return 0 + + logging.info(f'Prev commit: SHA={prev_sha} workflow_run_id={run_id}') + run_ok = get_status_of_run(run_id) + logging.info(f'workflow_run_id={run_id} ok={run_ok}') + return 0 if run_ok else 1 + + +if __name__ == '__main__': + sys.exit(main())