circuitpython/tools/ci_changes_per_commit.py

240 lines
6.6 KiB
Python

#! /usr/bin/env python3
# SPDX-FileCopyrightText: 2021 microDev
#
# SPDX-License-Identifier: MIT
# GraphQL Query
QUERY_COMMITS = """
query ($owner: String!, $name: String!, $pullNumber: Int!, $commitsPerPage: Int!, $beforeCommit: String) {
repository(owner: $owner, name: $name) {
pullRequest(number: $pullNumber) {
commits(last: $commitsPerPage, before: $beforeCommit) {
totalCount
pageInfo {
startCursor
hasPreviousPage
}
nodes {
commit {
checkSuites(first: 100) {
nodes {
conclusion
workflowRun {
workflow {
name
}
}
id
}
totalCount
}
oid
}
}
}
}
}
}
"""
QUERY_CHECK_RUNS = """
query ($checkSuiteID: ID!,
$afterFailedRun: String, $afterIncompleteRun: String,
$includeFailedRuns: Boolean!, $includeIncompleteRuns: Boolean!) {
node(id: $checkSuiteID) {
... on CheckSuite {
failedRuns: checkRuns(
first: 100
after: $afterFailedRun
filterBy: {checkType: LATEST, conclusions: [ACTION_REQUIRED, TIMED_OUT, CANCELLED, FAILURE, NEUTRAL, STARTUP_FAILURE]}
) @include(if: $includeFailedRuns) {
nodes {
name
}
pageInfo {
endCursor
hasNextPage
}
}
incompleteRuns: checkRuns(
first: 100
after: $afterIncompleteRun
filterBy: {checkType: LATEST, statuses: [QUEUED, IN_PROGRESS, WAITING, PENDING, REQUESTED]}
) @include(if: $includeIncompleteRuns) {
nodes {
name
}
pageInfo {
endCursor
hasNextPage
}
}
}
}
}
"""
import os
import re
import json
import requests
query_variables_commits = {
"owner": "",
"name": "",
"pullNumber": int(os.environ["PULL"]),
"commitsPerPage": 20,
"beforeCommit": None,
}
query_variables_check_runs = {
"checkSuiteID": "",
"afterFailedRun": None,
"afterIncompleteRun": None,
"includeFailedRuns": True,
"includeIncompleteRuns": True,
}
headers = {"Authorization": f"Bearer {os.environ['GITHUB_TOKEN']}"}
class Query:
def __init__(self, query, variables={}, headers={}):
self.query = query
self.variables = variables
self.headers = headers
def paginate(self, page_info, name):
has_page = page_info["hasNextPage" if name.startswith("after") else "hasPreviousPage"]
if has_page:
self.variables[name] = page_info[
"endCursor" if name.startswith("after") else "startCursor"
]
return has_page
def fetch(self):
request = requests.post(
"https://api.github.com/graphql",
json={"query": self.query, "variables": self.variables},
headers=self.headers,
)
if request.status_code == 200:
return request.json()
else:
print(request.json())
raise Exception("Query Failed: {}".format(request.status_code))
def set_output(name, value):
if "GITHUB_OUTPUT" in os.environ:
with open(os.environ["GITHUB_OUTPUT"], "at") as f:
print(f"{name}={value}", file=f)
else:
print(f"Would set GitHub actions output {name} to '{value}'")
def get_commit_depth_and_check_suite(query_commits):
commit_depth = 0
while True:
commits = query_commits.fetch()["data"]["repository"]["pullRequest"]["commits"]
if commits["totalCount"] > 0:
nodes = commits["nodes"]
nodes.reverse()
if nodes[0]["commit"]["oid"] == os.environ["EXCLUDE_COMMIT"]:
nodes.pop(0)
for commit in nodes:
commit_depth += 1
commit = commit["commit"]
commit_sha = commit["oid"]
check_suites = commit["checkSuites"]
if check_suites["totalCount"] > 0:
for check_suite in check_suites["nodes"]:
if check_suite["workflowRun"]["workflow"]["name"] == "Build CI":
return [
{"sha": commit_sha, "depth": commit_depth},
check_suite["id"]
if check_suite["conclusion"] != "SUCCESS"
else None,
]
if not query_commits.paginate(commits["pageInfo"], "beforeCommit"):
return [None, None]
def get_bad_check_runs(query_check_runs):
bad_runs = {}
more_pages = True
run_types = ["failed", "incomplete"]
have_dependent_jobs = ["scheduler", "mpy-cross", "tests"]
while more_pages:
check_runs = query_check_runs.fetch()["data"]["node"]
more_pages = False
for run_type in run_types:
run_type_camel = run_type.capitalize() + "Run"
run_type = run_type + "Runs"
for check_run in check_runs[run_type]["nodes"]:
name = check_run["name"]
if any([name.startswith(job) for job in have_dependent_jobs]):
return {}
if name.startswith("ports"):
matrix_job = name.rsplit(" (", 1)[1][:-1]
bad_runs.setdefault("ports", []).append(matrix_job)
else:
bad_runs[name] = True
if query_check_runs.paginate(
check_runs[run_type]["pageInfo"], "after" + run_type_camel
):
query_check_runs.variables["include" + run_type_camel] = True
more_pages = True
return bad_runs
def set_commit(commit):
set_output("commit_sha", commit["sha"])
set_output("commit_depth", commit["depth"])
def main():
query_commits = Query(QUERY_COMMITS, query_variables_commits, headers)
query_commits.variables["owner"], query_commits.variables["name"] = os.environ["REPO"].split(
"/"
)
commit, check_suite = get_commit_depth_and_check_suite(query_commits)
if not check_suite:
if commit:
set_commit(commit)
else:
print("Abort: No check suite found")
quit()
query_check_runs = Query(QUERY_CHECK_RUNS, query_variables_check_runs, headers)
query_check_runs.variables["checkSuiteID"] = check_suite
check_runs = get_bad_check_runs(query_check_runs)
if not check_runs:
print("Abort: No check runs found")
quit()
set_commit(commit)
set_output("check_runs", json.dumps(check_runs))
if __name__ == "__main__":
main()