forked from ansible/ansible-documentation
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Github Action to label issues and PRs (ansible#118)
- Loading branch information
Showing
3 changed files
with
197 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
--- | ||
# Copyright (C) 2023 Maxwell G <maxwell@gtmx.me> | ||
# SPDX-License-Identifier: GPL-3.0-or-later | ||
|
||
"on": | ||
pull_request_target: | ||
issues: | ||
types: | ||
- opened | ||
|
||
name: "Triage Issues and PRs" | ||
|
||
permissions: | ||
issues: write | ||
pull-requests: write | ||
|
||
jobs: | ||
label_prs: | ||
runs-on: ubuntu-latest | ||
name: "Label Issue/PR" | ||
steps: | ||
- name: Print event information | ||
run: | | ||
echo "${{ toJSON(github.event) }}" | ||
- name: Checkout parent repository | ||
uses: actions/checkout@v3 | ||
- name: Install Python 3.11 | ||
uses: actions/setup-python@v4 | ||
with: | ||
python-version: "3.11" | ||
- name: Setup venv | ||
run: | | ||
python -m venv venv | ||
./venv/bin/pip install -r hacking/pr_labeler/requirements.txt | ||
- name: "Run the issue labeler" | ||
if: "github.event.issue" | ||
env: | ||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
run: | ||
./venv/bin/python hacking/pr_labeler/label.py issue ${{ github.event.issue.number }} | ||
- name: "Run the PR labeler" | ||
if: "! github.event.issue" | ||
env: | ||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
run: | ||
./venv/bin/python hacking/pr_labeler/label.py pr ${{ github.event.number }} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
# Copyright (C) 2023 Maxwell G <maxwell@gtmx.me> | ||
# SPDX-License-Identifier: GPL-3.0-or-later | ||
|
||
from __future__ import annotations | ||
|
||
import dataclasses | ||
import os | ||
from collections.abc import Collection | ||
from functools import cache | ||
from pathlib import Path | ||
from typing import Union | ||
|
||
import github | ||
import github.Auth | ||
import github.Issue | ||
import github.PullRequest | ||
import github.Repository | ||
import typer | ||
from codeowners import CodeOwners, OwnerTuple | ||
|
||
OWNER = "ansible" | ||
REPO = "ansible-documentation" | ||
LABELS_BY_CODEOWNER: dict[OwnerTuple, list[str]] = { | ||
("TEAM", "@ansible/steering-committee"): ["sc_approval"], | ||
} | ||
HERE = Path(__file__).resolve().parent | ||
ROOT = HERE.parent.parent | ||
CODEOWNERS = (ROOT / ".github/CODEOWNERS").read_text("utf-8") | ||
|
||
IssueOrPrCtx = Union["IssueLabelerCtx", "PRLabelerCtx"] | ||
IssueOrPr = Union["github.Issue.Issue", "github.PullRequest.PullRequest"] | ||
|
||
|
||
def get_repo(authed: bool = True) -> tuple[github.Github, github.Repository.Repository]: | ||
gclient = github.Github( | ||
auth=github.Auth.Token(os.environ["GITHUB_TOKEN"]) if authed else None, | ||
) | ||
repo = gclient.get_repo(f"{OWNER}/{REPO}") | ||
return gclient, repo | ||
|
||
|
||
@dataclasses.dataclass(frozen=True) | ||
class LabelerCtx: | ||
client: github.Github | ||
repo: github.Repository.Repository | ||
dry_run: bool | ||
|
||
@property | ||
def member(self) -> IssueOrPr: | ||
raise NotImplementedError | ||
|
||
|
||
@dataclasses.dataclass(frozen=True) | ||
class IssueLabelerCtx(LabelerCtx): | ||
issue: github.Issue.Issue | ||
|
||
@property | ||
def member(self) -> IssueOrPr: | ||
return self.issue | ||
|
||
|
||
@dataclasses.dataclass(frozen=True) | ||
class PRLabelerCtx(LabelerCtx): | ||
pr: github.PullRequest.PullRequest | ||
|
||
@property | ||
def member(self) -> IssueOrPr: | ||
return self.pr | ||
|
||
|
||
@cache | ||
def get_previously_labeled(ctx: IssueOrPrCtx) -> frozenset[str]: | ||
previously_labeled: set[str] = set() | ||
events = ( | ||
ctx.issue.get_events() | ||
if isinstance(ctx, IssueLabelerCtx) | ||
else ctx.pr.get_issue_events() | ||
) | ||
for event in events: | ||
if event.event in ("labeled", "unlabeled"): | ||
assert event.label | ||
previously_labeled.add(event.label.name) | ||
return frozenset(previously_labeled) | ||
|
||
|
||
def handle_codeowner_labels(ctx: PRLabelerCtx) -> None: | ||
labels = LABELS_BY_CODEOWNER.copy() | ||
owners = CodeOwners(CODEOWNERS) | ||
files = ctx.pr.get_files() | ||
for file in files: | ||
for owner in owners.of(file.filename): | ||
if labels_to_add := labels.pop(owner, None): | ||
add_label_if_new(ctx, labels_to_add) | ||
if not labels: | ||
return | ||
|
||
|
||
def add_label_if_new(ctx: IssueOrPrCtx, labels: Collection[str] | str) -> None: | ||
""" | ||
Add a label to a PR if it wasn't added in the past | ||
""" | ||
labels = {labels} if isinstance(labels, str) else labels | ||
previously_labeled = get_previously_labeled(ctx) | ||
labels = set(labels) - previously_labeled | ||
if not labels: | ||
return | ||
print(f"Adding labels to {ctx.member.number}:", *map(repr, labels)) | ||
if not ctx.dry_run: | ||
ctx.member.add_to_labels(*labels) | ||
|
||
|
||
APP = typer.Typer() | ||
|
||
|
||
@APP.callback() | ||
def cb(): | ||
""" | ||
Basic triager for ansible/ansible-documentation | ||
""" | ||
|
||
|
||
@APP.command(name="pr") | ||
def process_pr(pr_number: int, dry_run: bool = False) -> None: | ||
gclient, repo = get_repo(authed=not dry_run) | ||
pr = repo.get_pull(pr_number) | ||
if pr.state != "open": | ||
print("Refusing to process closed ticket") | ||
return | ||
ctx = PRLabelerCtx(client=gclient, repo=repo, pr=pr, dry_run=dry_run) | ||
|
||
handle_codeowner_labels(ctx) | ||
add_label_if_new(ctx, "needs_triage") | ||
|
||
|
||
@APP.command(name="issue") | ||
def process_issue(issue_number: int, dry_run: bool = False) -> None: | ||
gclient, repo = get_repo(authed=not dry_run) | ||
issue = repo.get_issue(issue_number) | ||
if issue.state != "open": | ||
print("Refusing to process closed ticket") | ||
return | ||
ctx = IssueLabelerCtx(client=gclient, repo=repo, issue=issue, dry_run=dry_run) | ||
|
||
add_label_if_new(ctx, "needs_triage") | ||
|
||
|
||
if __name__ == "__main__": | ||
APP() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
codeowners | ||
pygithub | ||
typer |