diff --git a/.github/workflows/pr_opened.yml b/.github/workflows/pr_opened.yml new file mode 100644 index 00000000..9ac1386f --- /dev/null +++ b/.github/workflows/pr_opened.yml @@ -0,0 +1,43 @@ +name: PR Opened +on: + pull_request_target: + types: [opened] + +permissions: + contents: read + pull-requests: write + issues: write + +jobs: + # based on the scikit-learn 1.3.1 PR labelers + labeler: + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v3 + with: + sparse-checkout: _build_tools + + - uses: actions/setup-python@v4 + with: + python-version: "3.10" + + - name: Install PyGithub + run: pip install -Uq PyGithub + + - uses: actions/create-github-app-token@v1 + id: app-token + with: + app-id: ${{ vars.PR_APP_ID }} + private-key: ${{ secrets.PR_APP_KEY }} + + - name: Label pull request + id: label-pr + run: _python build_tools/pr_labeler.py ${{ steps.app-token.outputs.token }} + env: + CONTEXT_GITHUB: ${{ toJson(github) }} + + - name: Write pull request comment + run: _python build_tools/pr_open_commenter.py ${{ steps.app-token.outputs.token }} ${{ steps.label-pr.outputs.title-labels }} ${{ steps.label-pr.outputs.title-labels-new }} ${{ steps.label-pr.outputs.content-labels }} ${{ steps.label-pr.outputs.content-labels-status }} + env: + CONTEXT_GITHUB: ${{ toJson(github) }} diff --git a/_build_tools/pr_labeler.py b/_build_tools/pr_labeler.py new file mode 100644 index 00000000..fe8bc6c4 --- /dev/null +++ b/_build_tools/pr_labeler.py @@ -0,0 +1,86 @@ +"""Labels PRs based on title and change list. + +Must be run in a github action with the pull_request_target event. + +Based on the scikit-learn v1.3.1 label_title_regex.py script. +""" + +import json +import os +import re +import sys + +from github import Github + +context_dict = json.loads(os.getenv("CONTEXT_GITHUB")) + +repo = context_dict["repository"] +g = Github(sys.argv[1]) +repo = g.get_repo(repo) +pr_number = context_dict["event"]["number"] +pr = repo.get_pull(number=pr_number) +labels = [label.name for label in pr.get_labels()] + +# title labels +title = pr.title + +title_regex_to_labels = [ + (r"\bENH\b", "enhancement"), + (r"\bMNT\b", "maintenance"), + (r"\bBUG\b", "bug"), + (r"\bDOC\b", "documentation"), + (r"\bREF\b", "refactor"), +] + +title_labels = [ + label for regex, label in title_regex_to_labels if re.search(regex, title) +] +title_labels_to_add = list(set(title_labels) - set(labels)) + +# content labels +paths = [file.filename for file in pr.get_files()] + +content_paths_to_labels = [ + ("tsml-eval/datasets/", "datasets"), + ("tsml-eval/estimators/", "estimators"), + ("tsml-eval/evaluation/", "evaluation"), + ("examples/", "examples"), + ("tsml-eval/experiments/", "experiments"), + ("tsml-eval/publications/", "publications"), + ("results/", "results"), + ("tsml-eval/testing/", "testing"), +] + +present_content_labels = [ + label for _, label in content_paths_to_labels if label in labels +] + +content_labels = [ + label + for package, label in content_paths_to_labels + if any([package in path for path in paths]) +] +content_labels = list(set(content_labels)) + +content_labels_to_add = content_labels +content_labels_status = "used" +if len(present_content_labels) > 0: + content_labels_to_add = [] + content_labels_status = "ignored" +if len(content_labels) > 3: + content_labels_to_add = [] + content_labels_status = ( + "large" if content_labels_status != "ignored" else "ignored+large" + ) + +# add to PR +if title_labels_to_add or content_labels_to_add: + pr.add_to_labels(*title_labels_to_add + content_labels_to_add) + +with open(os.environ["GITHUB_OUTPUT"], "a") as fh: + print(f"title-labels={title_labels}".replace(" ", ""), file=fh) # noqa: T201 + print( # noqa: T201 + f"title-labels-new={title_labels_to_add}".replace(" ", ""), file=fh + ) + print(f"content-labels={content_labels}".replace(" ", ""), file=fh) # noqa: T201 + print(f"content-labels-status={content_labels_status}", file=fh) # noqa: T201 diff --git a/_build_tools/pr_open_commenter.py b/_build_tools/pr_open_commenter.py new file mode 100644 index 00000000..36e73cd0 --- /dev/null +++ b/_build_tools/pr_open_commenter.py @@ -0,0 +1,98 @@ +"""Writes a comment on PR opening. + +Includes output from the labeler action. +""" + +import json +import os +import sys + +from github import Github + +context_dict = json.loads(os.getenv("CONTEXT_GITHUB")) + +repo = context_dict["repository"] +g = Github(sys.argv[1]) +repo = g.get_repo(repo) +pr_number = context_dict["event"]["number"] +pr = repo.get_pull(number=pr_number) + +print(sys.argv[2:]) # noqa +title_labels = sys.argv[2][1:-1].split(",") +title_labels_new = sys.argv[3][1:-1].split(",") +content_labels = sys.argv[4][1:-1].split(",") +content_labels_status = sys.argv[5] + +labels = [(label.name, label.color) for label in repo.get_labels()] +title_labels = [ + "$\\color{#%s}{\\textsf{%s}}$" % (color, label) + for label, color in labels + if label in title_labels +] +title_labels_new = [ + "$\\color{#%s}{\\textsf{%s}}$" % (color, label) + for label, color in labels + if label in title_labels_new +] +content_labels = [ + "$\\color{#%s}{\\textsf{%s}}$" % (color, label) + for label, color in labels + if label in content_labels +] + +title_labels_str = "" +if len(title_labels) == 0: + title_labels_str = "I did not find any labels to add based on the title." +elif len(title_labels_new) != 0: + arr_str = str(title_labels_new).strip("[]").replace("'", "") + title_labels_str = ( + "I have added the following labels to this PR based on the title: " + f"**[ {arr_str} ]**." + ) + if len(title_labels) != len(title_labels_new): + arr_str = ( + str(set(title_labels) - set(title_labels_new)).strip("[]").replace("'", "") + ) + title_labels_str += ( + f" The following labels were already present: **[ {arr_str} ]**" + ) + +content_labels_str = "" +if len(content_labels) != 0: + if content_labels_status == "used": + arr_str = str(content_labels).strip("[]").replace("'", "") + content_labels_str = ( + "I have added the following labels to this PR based on " + f"the changes made: **[ {arr_str} ]**. Feel free " + "to change these if they do not properly represent the PR." + ) + elif content_labels_status == "ignored": + arr_str = str(content_labels).strip("[]").replace("'", "") + content_labels_str = ( + "I would have added the following labels to this PR " + f"based on the changes made: **[ {arr_str} ]**, " + "however some package labels are already present." + ) + elif content_labels_status == "large": + content_labels_str = ( + "This PR changes too many different packages (>3) for " + "automatic addition of labels, please manually add package " + "labels if relevant." + ) +elif title_labels_str == "": + content_labels_str = ( + "I did not find any labels to add that did not already " + "exist. If the content of your PR changes, make sure to " + "update the labels accordingly." + ) + +pr.create_issue_comment( + f""" +## Thank you for contributing to `tsml-eval`! + +{title_labels_str} +{content_labels_str} + +The [Checks](https://github.com/aeon-toolkit/aeon/pull/{pr_number}/checks) tab will show the status of our automated tests. You can click on individual test runs in the tab or "Details" in the panel below to see more information if there is a failure. + """ # noqa +)