From a6e2f0c795735dc83218fb4cdb0f513dcf561bd8 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Date: Tue, 1 Oct 2024 22:33:15 +0100 Subject: [PATCH 1/5] Add a automatic PR-revert utility for failing buildbots Signed-off-by: Pablo Galindo --- buildbothammer/README.md | 63 +++ buildbothammer/pyproject.toml | 16 + buildbothammer/src/buildbothammer/__init__.py | 363 ++++++++++++++++++ buildbothammer/src/buildbothammer/__main__.py | 6 + 4 files changed, 448 insertions(+) create mode 100644 buildbothammer/README.md create mode 100644 buildbothammer/pyproject.toml create mode 100644 buildbothammer/src/buildbothammer/__init__.py create mode 100644 buildbothammer/src/buildbothammer/__main__.py diff --git a/buildbothammer/README.md b/buildbothammer/README.md new file mode 100644 index 00000000..68427915 --- /dev/null +++ b/buildbothammer/README.md @@ -0,0 +1,63 @@ +# BuildbotHammer + +This app automatically creates revert Pull Requests for failing builds in the +CPython project. It monitors Buildbot for failing builds, identifies the +commit that caused the failure, and creates a revert PR on GitHub. It also sends +notifications to Discord. + +## Prerequisites + +- Python 3.11+ +- Git +- A GitHub account with fork of the CPython repository +- A Discord server with webhook set up (for notifications) + +## Installation + +1. Clone this repository: + ``` + git clone https://github.com/python/buildmasterconf.git + cd buildmasterconf/buildbot-hammer + ``` + +2. Install the required Python packages: + ``` + pip install -e . + ``` + +3. Set up environment variables: + ``` + export GITHUB_TOKEN="your-github-personal-access-token" + export DISCORD_WEBHOOK_URL="your-discord-webhook-url" + ``` + +4. Update the script with your GitHub username: + Open the script and replace `FORK_OWNER` variable with your GitHub username. + +5. (Optional) Update the `REPO_CLONE_DIR` path if you want to use a different location for the local CPython clone. + +## Usage + +Run the script with: + +``` +python -m buildbothammer +``` + +The script will: +1. Check Buildbot for failing builds +2. For each failing build, it will: + - Identify the commit that caused the failure + - Create a new branch in your fork + - Revert the problematic commit + - Create a Pull Request to the main CPython repository + - Send a notification to the configured Discord channel + +## Configuration + +- `BUILDBOT_API`: The Buildbot API endpoint (default: "http://buildbot.python.org/api/v2") +- `BUILDBOT_URL`: The Buildbot URL for generating links (default: "http://buildbot.python.org/#/") +- `REPO_OWNER`: The owner of the main repository (default: "python") +- `REPO_NAME`: The name of the repository (default: "cpython") +- `FORK_OWNER`: Your GitHub username (default: "$REPO_OWNER") +- `REPO_CLONE_DIR`: The directory for the local clone of the repository \ No newline at end of file diff --git a/buildbothammer/pyproject.toml b/buildbothammer/pyproject.toml new file mode 100644 index 00000000..31f7a62b --- /dev/null +++ b/buildbothammer/pyproject.toml @@ -0,0 +1,16 @@ +[project] +name = "buildbothammer" +version = "0.1.0" +description = "Automatic revert failing CPython PRs" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "requests>=2.32.3", + "pygithub>=2.4.0", + "filelock>=3.16.1", + "aiohttp", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/buildbothammer/src/buildbothammer/__init__.py b/buildbothammer/src/buildbothammer/__init__.py new file mode 100644 index 00000000..508d9f6a --- /dev/null +++ b/buildbothammer/src/buildbothammer/__init__.py @@ -0,0 +1,363 @@ +import aiohttp +import asyncio +import json +import os +import logging +from pathlib import Path +import subprocess +from filelock import FileLock +from github import Github +from github.GithubException import GithubException + +# Set up logging +logging.basicConfig( + level=logging.DEBUG, format="%(asctime)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + +BUILDBOT_API = "http://buildbot.python.org/api/v2" +BUILDBOT_URL = "http://buildbot.python.org/#/" + +DISCORD_WEBHOOK_URL = os.environ.get("DISCORD_WEBHOOK_URL") +if not DISCORD_WEBHOOK_URL: + logger.warning( + "DISCORD_WEBHOOK_URL environment variable is not set. Discord notifications will be disabled." + ) + +GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN") +if not GITHUB_TOKEN: + raise EnvironmentError("GITHUB_TOKEN environment variable is not set") + +N_BUILDS = 200 + +REPO_OWNER = "python" +REPO_NAME = "cpython" +FORK_OWNER = os.getenv("FORK_OWNER", REPO_OWNER) +REPO_CLONE_PATH = os.getenv("REPO_CLONE_PATH") +if not REPO_CLONE_PATH: + raise EnvironmentError("REPO_CLONE_PATH environment variable is not set") +REPO_CLONE_DIR = Path(REPO_CLONE_PATH) +LOCK_FILE = REPO_CLONE_DIR.parent / f"{REPO_NAME}.lock" +MIN_CONSECUTIVE_FAILURES = 3 + + +class BuildbotError(Exception): + """Exception raised for errors in the Buildbot API.""" + + pass + + +class GitHubError(Exception): + """Exception raised for errors in GitHub operations.""" + + pass + + +def generate_build_status_graph(builds): + status_map = { + 0: "🟩", # Success + 2: "🟥", # Failure + None: "⬜", # Not completed + 1: "🟧", # Warnings or unstable + } + return "".join(status_map.get(build["results"], "⬜") for build in builds) + + +async def send_discord_notification(session, message): + if not DISCORD_WEBHOOK_URL: + logger.warning("Discord notification not sent: DISCORD_WEBHOOK_URL is not set") + return + + payload = {"content": message} + + try: + async with session.post(DISCORD_WEBHOOK_URL, json=payload) as response: + if response.status == 204: + logger.info("Successfully sent Discord notification") + else: + logger.error( + f"Failed to send Discord notification. Status: {response.status}" + ) + except Exception as e: + logger.error(f"Error sending Discord notification: {str(e)}") + + +async def fetch(session, url): + try: + async with session.get(url) as response: + response.raise_for_status() + content_type = response.headers.get("Content-Type", "") + if "application/json" in content_type: + return await response.json() + elif "text/plain" in content_type: + text = await response.text() + return json.loads(text) + else: + raise ValueError(f"Unexpected content type: {content_type}") + except aiohttp.ClientResponseError as e: + raise BuildbotError(f"HTTP error {e.status} while fetching {url}: {e.message}") + except json.JSONDecodeError: + raise BuildbotError(f"Failed to decode JSON from {url}") + + +async def get_builder_builds(session, builder, limit=N_BUILDS): + builds_url = f"{BUILDBOT_API}/builders/{builder['builderid']}/builds?limit={limit}&&order=-complete_at" + builds = await fetch(session, builds_url) + status_graph = generate_build_status_graph(builds["builds"]) + return builder, builds["builds"], status_graph + + +def is_failing_builder(builds): + failing_streak = 0 + first_failing_build = None + for build in builds: + if not build["complete"]: + continue + if build["results"] == 2: # 2 indicates a failed build in Buildbot + failing_streak += 1 + first_failing_build = build + continue + elif build["results"] == 0: # 0 indicates a successful build in Buildbot + if failing_streak >= MIN_CONSECUTIVE_FAILURES: + return True, first_failing_build + return False, None + failing_streak = 0 + return False, None + + +async def get_failing_builders(session, limit=N_BUILDS): + logger.info("Fetching failing builders") + builders_url = f"{BUILDBOT_API}/builders" + builders = await fetch(session, builders_url) + builders = builders["builders"] + + relevant_builders = [ + b for b in builders if "3.x" in b["tags"] and "stable" in b["tags"] + ] + + builder_builds = await asyncio.gather( + *[get_builder_builds(session, builder, limit) for builder in relevant_builders], + return_exceptions=True, + ) + + failing_builders = [] + all_builders_status = [] + for result in builder_builds: + if isinstance(result, Exception): + logger.error(f"Error fetching builder builds: {result}") + continue + builder, builds, status_graph = result + is_failing, last_failing_build = is_failing_builder(builds) + if last_failing_build: + status_graph = list(status_graph) + status_graph[builds.index(last_failing_build)] = "🟪" + status_graph = "".join(status_graph) + print(f"{builder['name']}\n{status_graph}\n") + all_builders_status.append((builder["name"], status_graph)) + if is_failing: + logger.info( + f"Found failing builder: {builder['name']} with last failing build {last_failing_build['buildid']}" + ) + failing_builders.append((builder, last_failing_build)) + else: + logger.debug(f"Builder {builder['name']} is not failing") + + logger.info(f"Total failing builders found: {len(failing_builders)}") + return failing_builders, all_builders_status + + +async def get_change_request(session, build): + logger.info(f"Fetching change request for build: {build['buildid']}") + changes_url = f"{BUILDBOT_API}/builds/{build['buildid']}/changes" + changes = await fetch(session, changes_url) + + if len(changes["changes"]) == 1: + logger.debug(f"Found change request for build {build['buildid']}") + return changes["changes"][0] + else: + logger.debug(f"No single change request found for build {build['buildid']}") + return None + + +def run_command(command, cwd=None, check=True, capture_output=False): + logger.debug(f"Running command: {' '.join(command)}") + subprocess.run( + command, cwd=cwd, capture_output=capture_output, text=True, check=check + ) + + +def ensure_repo_clone(): + if not REPO_CLONE_DIR.exists(): + logger.info(f"Cloning repository to {REPO_CLONE_DIR}") + clone_url = f"https://{GITHUB_TOKEN}@github.com/{REPO_OWNER}/{REPO_NAME}.git" + run_command(["git", "clone", clone_url, str(REPO_CLONE_DIR)]) + else: + logger.info("Updating existing repository clone") + run_command(["git", "fetch", "--all"], cwd=str(REPO_CLONE_DIR)) + run_command(["git", "reset", "--hard", "origin/main"], cwd=str(REPO_CLONE_DIR)) + + +def create_revert_pr(commit_sha, builder, failing_build): + logger.info(f"Creating revert PR for commit: {commit_sha}") + g = Github(GITHUB_TOKEN) + + try: + main_repo = g.get_repo(f"{REPO_OWNER}/{REPO_NAME}") + + with FileLock(LOCK_FILE): + ensure_repo_clone() + + run_command( + ["git", "config", "user.name", "Your Name"], cwd=str(REPO_CLONE_DIR) + ) + run_command( + ["git", "config", "user.email", "your.email@example.com"], + cwd=str(REPO_CLONE_DIR), + ) + + branch_name = f"revert-{commit_sha[:7]}" + run_command(["git", "checkout", "-b", branch_name], cwd=str(REPO_CLONE_DIR)) + logger.info(f"Created and checked out new branch: {branch_name}") + + run_command( + ["git", "revert", "--no-edit", commit_sha], cwd=str(REPO_CLONE_DIR) + ) + logger.info(f"Successfully reverted commit {commit_sha}") + + run_command( + [ + "git", + "push", + "-f", + f"https://{GITHUB_TOKEN}@github.com/{FORK_OWNER}/{REPO_NAME}.git", + branch_name, + ], + cwd=str(REPO_CLONE_DIR), + ) + logger.info("Pushed changes to fork") + + commit_to_revert = main_repo.get_commit(commit_sha) + original_commit_message = commit_to_revert.commit.message.split("\n")[0] + + commit_to_revert = main_repo.get_commit(commit_sha) + original_commit_message = commit_to_revert.commit.message.split("\n")[0] + author = ( + commit_to_revert.author.login if commit_to_revert.author else "Unknown" + ) + commit_date = commit_to_revert.commit.author.date.strftime( + "%Y-%m-%d %H:%M:%S UTC" + ) + + pr_description = f""" +🔄 **Automatic Revert** + +This PR automatically reverts commit {commit_sha} due to a failing build. + +📊 **Build Information:** +- Builder: {builder['name']} +- Build Number: {failing_build['number']} +- Build URL: {BUILDBOT_URL}/builders/{failing_build['builderid']}/builds/{failing_build['buildid']} + +💻 **Reverted Commit Details:** +- SHA: `{commit_sha}` +- Author: {author} +- Date: {commit_date} +- Message: "{original_commit_message}" + +🛠 **Next Steps:** +1. Investigate the cause of the build failure. +2. If the revert is necessary, merge this PR. +3. If the revert is not necessary, close this PR and fix the original issue. + +⚠️ Please review this revert carefully before merging. + +cc @{author} - Your attention may be needed on this revert. +""" + pr = main_repo.create_pull( + title=f"🔄 Revert: {original_commit_message}", + body=pr_description, + head=f"{FORK_OWNER}:{branch_name}", + base="main", + ) + + logger.info(f"Created revert PR: {pr.html_url}") + + discord_message = f""" +🚨 **Automatic Revert PR Created** + +A build failure has triggered an automatic revert. Details: + +🔗 **PR Link:** {pr.html_url} +🏗 **Failed Build:** {BUILDBOT_URL}/builders/{failing_build['builderid']}/builds/{failing_build['buildid']} +🔄 **Reverted Commit:** `{commit_sha}` +✍️ **Original Author:** {author} +📅 **Commit Date:** {commit_date} +💬 **Commit Message:** "{original_commit_message}" + +Please review and take appropriate action! +""" + + return pr.html_url, discord_message + except GithubException as e: + raise GitHubError(f"GitHub API error: {e.status}, {e.data}") + except Exception as e: + raise GitHubError(f"Unexpected error while creating revert PR: {str(e)}") + finally: + run_command( + ["git", "revert", "--abort"], + cwd=str(REPO_CLONE_DIR), + check=False, + capture_output=True, + ) + run_command(["git", "checkout", "main"], cwd=str(REPO_CLONE_DIR)) + run_command(["git", "branch", "-D", branch_name], cwd=str(REPO_CLONE_DIR)) + + +async def process_builder(session, builder, first_failing_build): + try: + change_request = await get_change_request(session, first_failing_build) + + if change_request and "sourcestamp" in change_request: + commit_sha = change_request["sourcestamp"]["revision"] + pr_url, discord_message = create_revert_pr( + commit_sha, builder, first_failing_build + ) + if pr_url: + logger.info(f"Created revert PR for commit {commit_sha}: {pr_url}") + await send_discord_notification(session, discord_message) + else: + logger.error(f"Failed to create revert PR for commit {commit_sha}") + else: + logger.warning( + f"No suitable change request found for builder: {builder['name']}" + ) + except (BuildbotError, GitHubError) as e: + logger.error(f"Error processing builder {builder['name']}: {str(e)}") + raise + + +async def main(): + logger.info("Starting the Async Buildbot and GitHub Revert Script") + + async with aiohttp.ClientSession() as session: + try: + failing_builders, all_builders_status = await get_failing_builders(session) + + results = await asyncio.gather( + *[ + process_builder(session, builder, first_failing_build) + for builder, first_failing_build in failing_builders + ], + return_exceptions=True, + ) + for result in results: + if isinstance(result, Exception): + logger.error(f"Error in processing a builder: {result}") + except Exception as e: + logger.error(f"An error occurred in the main execution: {str(e)}") + finally: + logger.info("Script execution completed") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/buildbothammer/src/buildbothammer/__main__.py b/buildbothammer/src/buildbothammer/__main__.py new file mode 100644 index 00000000..870d3aa8 --- /dev/null +++ b/buildbothammer/src/buildbothammer/__main__.py @@ -0,0 +1,6 @@ +import asyncio + +from . import main + +if __name__ == "__main__": + asyncio.run(main()) From bc97a6f73bbb89248fb1f0c8adc83857fe8fdfa8 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Date: Thu, 10 Oct 2024 23:31:51 +0100 Subject: [PATCH 2/5] Do not create a PR if one exists Signed-off-by: Pablo Galindo --- buildbothammer/README.md | 33 +------------------ buildbothammer/src/buildbothammer/__init__.py | 21 ++++++++++-- 2 files changed, 20 insertions(+), 34 deletions(-) diff --git a/buildbothammer/README.md b/buildbothammer/README.md index 68427915..e31e8e2f 100644 --- a/buildbothammer/README.md +++ b/buildbothammer/README.md @@ -5,37 +5,6 @@ CPython project. It monitors Buildbot for failing builds, identifies the commit that caused the failure, and creates a revert PR on GitHub. It also sends notifications to Discord. -## Prerequisites - -- Python 3.11+ -- Git -- A GitHub account with fork of the CPython repository -- A Discord server with webhook set up (for notifications) - -## Installation - -1. Clone this repository: - ``` - git clone https://github.com/python/buildmasterconf.git - cd buildmasterconf/buildbot-hammer - ``` - -2. Install the required Python packages: - ``` - pip install -e . - ``` - -3. Set up environment variables: - ``` - export GITHUB_TOKEN="your-github-personal-access-token" - export DISCORD_WEBHOOK_URL="your-discord-webhook-url" - ``` - -4. Update the script with your GitHub username: - Open the script and replace `FORK_OWNER` variable with your GitHub username. - -5. (Optional) Update the `REPO_CLONE_DIR` path if you want to use a different location for the local CPython clone. - ## Usage Run the script with: @@ -60,4 +29,4 @@ The script will: - `REPO_OWNER`: The owner of the main repository (default: "python") - `REPO_NAME`: The name of the repository (default: "cpython") - `FORK_OWNER`: Your GitHub username (default: "$REPO_OWNER") -- `REPO_CLONE_DIR`: The directory for the local clone of the repository \ No newline at end of file +- `REPO_CLONE_PATH`: The directory for the local clone of the repository \ No newline at end of file diff --git a/buildbothammer/src/buildbothammer/__init__.py b/buildbothammer/src/buildbothammer/__init__.py index 508d9f6a..2ef65277 100644 --- a/buildbothammer/src/buildbothammer/__init__.py +++ b/buildbothammer/src/buildbothammer/__init__.py @@ -197,6 +197,12 @@ def ensure_repo_clone(): run_command(["git", "reset", "--hard", "origin/main"], cwd=str(REPO_CLONE_DIR)) +def check_existing_pr(repo, branch_name): + logger.info(f"Checking for existing PR for branch: {branch_name}") + existing_prs = repo.get_pulls(state='open', head=f"{FORK_OWNER}:{branch_name}") + return next(existing_prs, None) + + def create_revert_pr(commit_sha, builder, failing_build): logger.info(f"Creating revert PR for commit: {commit_sha}") g = Github(GITHUB_TOKEN) @@ -204,6 +210,18 @@ def create_revert_pr(commit_sha, builder, failing_build): try: main_repo = g.get_repo(f"{REPO_OWNER}/{REPO_NAME}") + branch_name = f"revert-{commit_sha[:7]}" + + # Check for existing PR + existing_pr = check_existing_pr(main_repo, branch_name) + if existing_pr: + logger.info(f"Existing PR found: {existing_pr.html_url}") + return None, None + + with FileLock(LOCK_FILE): + ensure_repo_clone() + + with FileLock(LOCK_FILE): ensure_repo_clone() @@ -215,7 +233,6 @@ def create_revert_pr(commit_sha, builder, failing_build): cwd=str(REPO_CLONE_DIR), ) - branch_name = f"revert-{commit_sha[:7]}" run_command(["git", "checkout", "-b", branch_name], cwd=str(REPO_CLONE_DIR)) logger.info(f"Created and checked out new branch: {branch_name}") @@ -322,7 +339,7 @@ async def process_builder(session, builder, first_failing_build): pr_url, discord_message = create_revert_pr( commit_sha, builder, first_failing_build ) - if pr_url: + if pr_url and discord_message: logger.info(f"Created revert PR for commit {commit_sha}: {pr_url}") await send_discord_notification(session, discord_message) else: From 20276e01186a5309600592b2815f2210da466456 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Date: Thu, 10 Oct 2024 23:45:26 +0100 Subject: [PATCH 3/5] Use task groups --- buildbothammer/src/buildbothammer/__init__.py | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/buildbothammer/src/buildbothammer/__init__.py b/buildbothammer/src/buildbothammer/__init__.py index 2ef65277..2846d5b9 100644 --- a/buildbothammer/src/buildbothammer/__init__.py +++ b/buildbothammer/src/buildbothammer/__init__.py @@ -135,32 +135,32 @@ async def get_failing_builders(session, limit=N_BUILDS): b for b in builders if "3.x" in b["tags"] and "stable" in b["tags"] ] - builder_builds = await asyncio.gather( - *[get_builder_builds(session, builder, limit) for builder in relevant_builders], - return_exceptions=True, - ) - failing_builders = [] all_builders_status = [] - for result in builder_builds: - if isinstance(result, Exception): - logger.error(f"Error fetching builder builds: {result}") - continue - builder, builds, status_graph = result - is_failing, last_failing_build = is_failing_builder(builds) - if last_failing_build: - status_graph = list(status_graph) - status_graph[builds.index(last_failing_build)] = "🟪" - status_graph = "".join(status_graph) - print(f"{builder['name']}\n{status_graph}\n") - all_builders_status.append((builder["name"], status_graph)) - if is_failing: - logger.info( - f"Found failing builder: {builder['name']} with last failing build {last_failing_build['buildid']}" - ) - failing_builders.append((builder, last_failing_build)) - else: - logger.debug(f"Builder {builder['name']} is not failing") + + async def process_builder(builder): + try: + builder, builds, status_graph = await get_builder_builds(session, builder, limit) + is_failing, last_failing_build = is_failing_builder(builds) + if last_failing_build: + status_graph = list(status_graph) + status_graph[builds.index(last_failing_build)] = "🟪" + status_graph = "".join(status_graph) + print(f"{builder['name']}\n{status_graph}\n") + all_builders_status.append((builder["name"], status_graph)) + if is_failing: + logger.info( + f"Found failing builder: {builder['name']} with last failing build {last_failing_build['buildid']}" + ) + failing_builders.append((builder, last_failing_build)) + else: + logger.debug(f"Builder {builder['name']} is not failing") + except Exception as e: + logger.error(f"Error processing builder {builder['name']}: {str(e)}") + + async with asyncio.TaskGroup() as tg: + for builder in relevant_builders: + tg.create_task(process_builder(builder)) logger.info(f"Total failing builders found: {len(failing_builders)}") return failing_builders, all_builders_status From 58194ac4b09e2a6b5f3aed3865cf13273d01504d Mon Sep 17 00:00:00 2001 From: Pablo Galindo Date: Thu, 10 Oct 2024 23:46:51 +0100 Subject: [PATCH 4/5] Small fixes --- buildbothammer/src/buildbothammer/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildbothammer/src/buildbothammer/__init__.py b/buildbothammer/src/buildbothammer/__init__.py index 2846d5b9..f3bdc06b 100644 --- a/buildbothammer/src/buildbothammer/__init__.py +++ b/buildbothammer/src/buildbothammer/__init__.py @@ -373,7 +373,7 @@ async def main(): except Exception as e: logger.error(f"An error occurred in the main execution: {str(e)}") finally: - logger.info("Script execution completed") + logger.info("Buildbothammer execution completed") if __name__ == "__main__": From 94ff7b47e28f028cdac65df81da95953619e44ca Mon Sep 17 00:00:00 2001 From: Pablo Galindo Date: Thu, 10 Oct 2024 23:49:03 +0100 Subject: [PATCH 5/5] Small fixes --- buildbothammer/src/buildbothammer/__init__.py | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/buildbothammer/src/buildbothammer/__init__.py b/buildbothammer/src/buildbothammer/__init__.py index f3bdc06b..540cfe4f 100644 --- a/buildbothammer/src/buildbothammer/__init__.py +++ b/buildbothammer/src/buildbothammer/__init__.py @@ -140,7 +140,9 @@ async def get_failing_builders(session, limit=N_BUILDS): async def process_builder(builder): try: - builder, builds, status_graph = await get_builder_builds(session, builder, limit) + builder, builds, status_graph = await get_builder_builds( + session, builder, limit + ) is_failing, last_failing_build = is_failing_builder(builds) if last_failing_build: status_graph = list(status_graph) @@ -199,7 +201,7 @@ def ensure_repo_clone(): def check_existing_pr(repo, branch_name): logger.info(f"Checking for existing PR for branch: {branch_name}") - existing_prs = repo.get_pulls(state='open', head=f"{FORK_OWNER}:{branch_name}") + existing_prs = repo.get_pulls(state="open", head=f"{FORK_OWNER}:{branch_name}") return next(existing_prs, None) @@ -211,7 +213,7 @@ def create_revert_pr(commit_sha, builder, failing_build): main_repo = g.get_repo(f"{REPO_OWNER}/{REPO_NAME}") branch_name = f"revert-{commit_sha[:7]}" - + # Check for existing PR existing_pr = check_existing_pr(main_repo, branch_name) if existing_pr: @@ -221,7 +223,6 @@ def create_revert_pr(commit_sha, builder, failing_build): with FileLock(LOCK_FILE): ensure_repo_clone() - with FileLock(LOCK_FILE): ensure_repo_clone() @@ -360,20 +361,24 @@ async def main(): try: failing_builders, all_builders_status = await get_failing_builders(session) - results = await asyncio.gather( - *[ - process_builder(session, builder, first_failing_build) - for builder, first_failing_build in failing_builders - ], - return_exceptions=True, - ) - for result in results: - if isinstance(result, Exception): - logger.error(f"Error in processing a builder: {result}") + async def process_failing_builder(builder, first_failing_build): + try: + await process_builder(session, builder, first_failing_build) + except Exception as e: + logger.error( + f"Error processing builder {builder['name']}: {str(e)}" + ) + + async with asyncio.TaskGroup() as tg: + for builder, first_failing_build in failing_builders: + tg.create_task( + process_failing_builder(builder, first_failing_build) + ) + except Exception as e: logger.error(f"An error occurred in the main execution: {str(e)}") finally: - logger.info("Buildbothammer execution completed") + logger.info("BuildbotHammer execution completed") if __name__ == "__main__":