From e8523b2d659e1e40a1eaf917cec509800e3bf651 Mon Sep 17 00:00:00 2001 From: sdiazlor Date: Tue, 7 Apr 2026 08:34:36 +0200 Subject: [PATCH 1/4] ci: add new workflow --- .github/workflows/assign-reviewers.yaml | 26 +++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/assign-reviewers.yaml diff --git a/.github/workflows/assign-reviewers.yaml b/.github/workflows/assign-reviewers.yaml new file mode 100644 index 00000000..4e20421c --- /dev/null +++ b/.github/workflows/assign-reviewers.yaml @@ -0,0 +1,26 @@ +name: Assign PR Reviewers +on: + pull_request_target: + branches: + - main + types: [ready_for_review] + +jobs: + assign_reviewers: + permissions: + pull-requests: write + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.11" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install PyGithub + - name: Run assignment script + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: python .github/scripts/assign_reviewers.py \ No newline at end of file From 23d4dfeb5dedd2f012da671a1aaf5a5800fcfb1e Mon Sep 17 00:00:00 2001 From: sdiazlor Date: Thu, 9 Apr 2026 00:42:44 +0200 Subject: [PATCH 2/4] feat: add assign reviewers script --- .github/scripts/assign_reviewers.py | 133 ++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 .github/scripts/assign_reviewers.py diff --git a/.github/scripts/assign_reviewers.py b/.github/scripts/assign_reviewers.py new file mode 100644 index 00000000..0da07fbb --- /dev/null +++ b/.github/scripts/assign_reviewers.py @@ -0,0 +1,133 @@ +# Copyright 2025 - Pruna AI GmbH. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import os +import re +from collections import Counter +from pathlib import Path + +import github +from github import Github + + +def pattern_to_regex(pattern): + """Turn a CODEOWNERS glob ``pattern`` into a regex for matching file paths.""" + if pattern.startswith("/"): + start_anchor = True + pattern = re.escape(pattern[1:]) + else: + start_anchor = False + pattern = re.escape(pattern) + pattern = pattern.replace(r"\*", "[^/]*") + if start_anchor: + pattern = r"^\/?" + pattern # Allow an optional leading slash after the start of the string + return pattern + + +def get_file_owners(file_path, codeowners_lines): + """Return owner logins for ``file_path`` using CODEOWNERS rules (last match wins).""" + for line in reversed(codeowners_lines): + line = line.split('#')[0].strip() + if not line: + continue + + parts = line.split() + pattern = parts[0] + # Can be empty, e.g. for dummy files with explicitly no owner + owners = [owner.removeprefix("@") for owner in parts[1:]] + + file_regex = pattern_to_regex(pattern) + if re.search(file_regex, file_path) is not None: + return owners # It can be empty + return [] + + +def get_dispatch_owners(codeowners_lines): + """Return fallback owners from the catch-all ``*`` CODEOWNERS rule.""" + for line in codeowners_lines: + line = line.split("#")[0].strip() + if not line: + continue + + parts = line.split() + pattern = parts[0] + owners = [owner.removeprefix("@") for owner in parts[1:]] + + if pattern == "*": + return owners + return [] + + +def main(): + """Load the PR event, skip if reviews exist or reviewers are already requested, then request recent owners.""" + script_dir = Path(__file__).parent.absolute() + with open(script_dir / "codeowners_assignment") as f: + codeowners_lines = f.readlines() + + g = Github(os.environ['GITHUB_TOKEN']) + repo = g.get_repo("PrunaAI/pruna") + with open(os.environ['GITHUB_EVENT_PATH']) as f: + event = json.load(f) + + pr_number = event['pull_request']['number'] + pr = repo.get_pull(pr_number) + pr_author = pr.user.login + + # Skipping exceptions + existing_reviews = list(pr.get_reviews()) + if existing_reviews: + print(f"Already has reviews: {[r.user.login for r in existing_reviews]}") + return + + users_requested, teams_requested = pr.get_review_requests() + users_requested = list(users_requested) + if users_requested: + print(f"Reviewers already requested: {users_requested}") + return + + # Counting recent owner matches + latest_owner_matches = Counter() + for file in pr.get_files(): + owners = set(get_file_owners(file.filename, codeowners_lines)) + owners.discard(pr_author) + if not owners: + continue + + commits = repo.get_commits(path=file.filename) + for commit in commits: + if commit.author is None: + continue + + login = commit.author.login + if login in owners: + latest_owner_matches[login] += file.changes + break + + top_owners = [owner for owner, _ in latest_owner_matches.most_common(2)] + + if not top_owners: + top_owners = [ + owner for owner in get_dispatch_owners(codeowners_lines) + if owner != pr_author + ] + print("Top owners", top_owners) + try: + pr.create_review_request(top_owners) + except github.GithubException as e: + print(f"Failed to request review for {top_owners}: {e}") + + +if __name__ == "__main__": + main() From 8ab605274b949b089fa348ea0dc4f342e745e644 Mon Sep 17 00:00:00 2001 From: sdiazlor Date: Thu, 9 Apr 2026 00:43:13 +0200 Subject: [PATCH 3/4] feat: add first version of codeowners --- .github/scripts/codeowners_assignment | 28 +++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/scripts/codeowners_assignment diff --git a/.github/scripts/codeowners_assignment b/.github/scripts/codeowners_assignment new file mode 100644 index 00000000..5192a46d --- /dev/null +++ b/.github/scripts/codeowners_assignment @@ -0,0 +1,28 @@ +# For more information on how to use this file, see: +# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#about-code-owners + +# If no one is pinged, in charge of dispatching reviewers +* @sdiazlor @minettekaum + +# CI and GitHub config +/.github/workflows/ @SaboniAmine @johannaSommer @gsprochette +/.github/scripts/ @sdiazlor +/.github/ISSUE_TEMPLATE/ @sdiazlor @minettekaum +/.github/PULL_REQUEST_TEMPLATE/ @minettekaum @sdiazlor + +# Dependencies and packaging +/pyproject.toml @gsprochette @begumcig + +# Docs +/docs/ @sdiazlor @minettekaum + +# Source +/src/pruna/algorithms/ @johannaSommer @begumcig @gsprochette @simlang @llcnt +/src/pruna/config/ @gsprochette @johannaSommer +/src/pruna/data/ @begumcig +/src/pruna/engine/ @gsprochette @johannaSommer +/src/pruna/evaluation/ @begumcig @davidberenstein1957 @johannaSommer +/src/pruna/logging/ @johannaSommer + +# Tests +/tests/ @johannaSommer @begumcig @gsprochette @simlang @llcnt From 916f7e0745991f0a9c5472c78af5f72c4accf18e Mon Sep 17 00:00:00 2001 From: sdiazlor Date: Thu, 9 Apr 2026 00:48:36 +0200 Subject: [PATCH 4/4] fix: align with linting --- .github/scripts/assign_reviewers.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/scripts/assign_reviewers.py b/.github/scripts/assign_reviewers.py index 0da07fbb..95d7c126 100644 --- a/.github/scripts/assign_reviewers.py +++ b/.github/scripts/assign_reviewers.py @@ -88,13 +88,11 @@ def main(): # Skipping exceptions existing_reviews = list(pr.get_reviews()) if existing_reviews: - print(f"Already has reviews: {[r.user.login for r in existing_reviews]}") return users_requested, teams_requested = pr.get_review_requests() users_requested = list(users_requested) if users_requested: - print(f"Reviewers already requested: {users_requested}") return # Counting recent owner matches @@ -122,11 +120,10 @@ def main(): owner for owner in get_dispatch_owners(codeowners_lines) if owner != pr_author ] - print("Top owners", top_owners) try: pr.create_review_request(top_owners) except github.GithubException as e: - print(f"Failed to request review for {top_owners}: {e}") + raise e if __name__ == "__main__":