mirror of
https://github.com/ansible/ansible-documentation.git
synced 2026-03-26 13:18:58 +07:00
Add Github Action to label issues and PRs (#118)
This commit is contained in:
46
.github/workflows/labeler.yml
vendored
Normal file
46
.github/workflows/labeler.yml
vendored
Normal 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
148
hacking/pr_labeler/label.py
Normal 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()
|
||||
3
hacking/pr_labeler/requirements.txt
Normal file
3
hacking/pr_labeler/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
codeowners
|
||||
pygithub
|
||||
typer
|
||||
Reference in New Issue
Block a user