Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions .github/scripts/assign_reviewers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# 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:]]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should there be if owner.startswith("@") here?


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:
return

users_requested, teams_requested = pr.get_review_requests()
users_requested = list(users_requested)
if users_requested:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we check that there are at least 2 reviewers? In the case where there is a single one we could automatically assign a second one.
I don't think so, 1 seems enough, just making sure this is on purpose.

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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so we're breaking after finding only the single most common author, but we're also looking for 2 reviewers. It seems to me that we would like to get at least the last two commits from each file, which might do +1 to two people or +2 to a single one. Maybe even the last 4 commits?


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
]
try:
pr.create_review_request(top_owners)
except github.GithubException as e:
raise e


if __name__ == "__main__":
main()
28 changes: 28 additions & 0 deletions .github/scripts/codeowners_assignment
Original file line number Diff line number Diff line change
@@ -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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we can also add the pre-commit-hook file, or do you want to dispatch this one manually?


# 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
26 changes: 26 additions & 0 deletions .github/workflows/assign-reviewers.yaml
Original file line number Diff line number Diff line change
@@ -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
Loading