|
| 1 | +"""Creates or updates an issue if the CI fails. This is useful to keep track of |
| 2 | +scheduled jobs that are failing repeatedly. |
| 3 | +
|
| 4 | +This script depends on: |
| 5 | +- `defusedxml` for safer parsing for xml |
| 6 | +- `PyGithub` for interacting with GitHub |
| 7 | +
|
| 8 | +The GitHub token only requires the `repo:public_repo` scope are described in |
| 9 | +https://docs.github.com/en/developers/apps/building-oauth-apps/scopes-for-oauth-apps#available-scopes. |
| 10 | +This scope allows the bot to create and edit its own issues. It is best to use a |
| 11 | +github account that does **not** have commit access to the public repo. |
| 12 | +""" |
| 13 | + |
| 14 | +from pathlib import Path |
| 15 | +import sys |
| 16 | +import argparse |
| 17 | + |
| 18 | +import defusedxml.ElementTree as ET |
| 19 | +from github import Github |
| 20 | + |
| 21 | +parser = argparse.ArgumentParser( |
| 22 | + description="Create or update issue from JUnit test results from pytest" |
| 23 | +) |
| 24 | +parser.add_argument( |
| 25 | + "bot_github_token", help="Github token for creating or updating an issue" |
| 26 | +) |
| 27 | +parser.add_argument("ci_name", help="Name of CI run instance") |
| 28 | +parser.add_argument("issue_repo", help="Repo to track issues") |
| 29 | +parser.add_argument("link_to_ci_run", help="URL to link to") |
| 30 | +parser.add_argument("junit_file", help="JUnit file") |
| 31 | + |
| 32 | +args = parser.parse_args() |
| 33 | +gh = Github(args.bot_github_token) |
| 34 | +issue_repo = gh.get_repo(args.issue_repo) |
| 35 | +title = f"⚠️ CI failed on {args.ci_name} ⚠️" |
| 36 | + |
| 37 | + |
| 38 | +def get_issue(): |
| 39 | + login = gh.get_user().login |
| 40 | + issues = gh.search_issues( |
| 41 | + f"repo:{args.issue_repo} {title} in:title state:open author:{login}" |
| 42 | + ) |
| 43 | + first_page = issues.get_page(0) |
| 44 | + # Return issue if it exist |
| 45 | + return first_page[0] if first_page else None |
| 46 | + |
| 47 | + |
| 48 | +def create_or_update_issue(body): |
| 49 | + # Interact with GitHub API to create issue |
| 50 | + header = f"**CI Failed on [{args.ci_name}]({args.link_to_ci_run})**" |
| 51 | + body_text = f"{header}\n{body}" |
| 52 | + issue = get_issue() |
| 53 | + |
| 54 | + if issue is None: |
| 55 | + # Create new issue |
| 56 | + issue = issue_repo.create_issue(title=title, body=body_text) |
| 57 | + print(f"Created issue in {args.issue_repo}#{issue.number}") |
| 58 | + sys.exit() |
| 59 | + else: |
| 60 | + # Update existing issue |
| 61 | + issue.edit(title=title, body=body_text) |
| 62 | + print(f"Updated issue in {args.issue_repo}#{issue.number}") |
| 63 | + sys.exit() |
| 64 | + |
| 65 | + |
| 66 | +junit_path = Path(args.junit_file) |
| 67 | +if not junit_path.exists(): |
| 68 | + body = "Unable to find junit file. Please see link for details." |
| 69 | + create_or_update_issue(body) |
| 70 | + sys.exit() |
| 71 | + |
| 72 | +# Find failures in junit file |
| 73 | +tree = ET.parse(args.junit_file) |
| 74 | +failure_cases = [] |
| 75 | + |
| 76 | +for item in tree.iter("testcase"): |
| 77 | + failure = item.find("failure") |
| 78 | + if failure is None: |
| 79 | + continue |
| 80 | + |
| 81 | + failure_cases.append( |
| 82 | + { |
| 83 | + "title": item.attrib["name"], |
| 84 | + "body": failure.text, |
| 85 | + } |
| 86 | + ) |
| 87 | + |
| 88 | +if not failure_cases: |
| 89 | + print("Test has no failures!") |
| 90 | + issue = get_issue() |
| 91 | + if issue is not None: |
| 92 | + print(f"Closing issue #{issue.number}") |
| 93 | + new_body = ( |
| 94 | + "## Closed issue because CI is no longer failing! ✅\n\n" |
| 95 | + f"[Successful run]({args.link_to_ci_run})\n\n" |
| 96 | + "## Previous failing issue\n\n" |
| 97 | + f"{issue.body}" |
| 98 | + ) |
| 99 | + issue.edit(state="closed", body=new_body) |
| 100 | + sys.exit() |
| 101 | + |
| 102 | +# Create content for issue |
| 103 | +issue_summary = ( |
| 104 | + "<details><summary>{title}</summary>\n\n```python\n{body}\n```\n</details>\n" |
| 105 | +) |
| 106 | +body_list = [issue_summary.format(**case) for case in failure_cases] |
| 107 | +body = "\n".join(body_list) |
| 108 | +create_or_update_issue(body) |
0 commit comments