Skip to content

Commit

Permalink
Add Github Action to label issues and PRs (ansible#118)
Browse files Browse the repository at this point in the history
  • Loading branch information
gotmax23 committed Jul 25, 2023
1 parent 897b949 commit 68d58f5
Show file tree
Hide file tree
Showing 3 changed files with 197 additions and 0 deletions.
46 changes: 46 additions & 0 deletions .github/workflows/labeler.yml
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 }}
148 changes: 148 additions & 0 deletions hacking/pr_labeler/label.py
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()
3 changes: 3 additions & 0 deletions hacking/pr_labeler/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
codeowners
pygithub
typer

0 comments on commit 68d58f5

Please sign in to comment.