Add Github Action to label issues and PRs (#118)

This commit is contained in:
Maxwell G
2023-07-25 10:20:26 -05:00
committed by GitHub
parent 09fcd1be2d
commit 0c56c51999
3 changed files with 197 additions and 0 deletions

46
.github/workflows/labeler.yml vendored Normal file
View File

@@ -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
hacking/pr_labeler/label.py Normal file
View File

@@ -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()

View File

@@ -0,0 +1,3 @@
codeowners
pygithub
typer