-
Notifications
You must be signed in to change notification settings - Fork 88
ci: automatically assign new reviewers #620
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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:]] | ||
|
|
||
| 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: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| 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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||
| 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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| 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 |
There was a problem hiding this comment.
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?