Skip to content
100 changes: 35 additions & 65 deletions templates/_templating_scripting.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
"""
import argparse
import contextlib
from enum import StrEnum
from hashlib import sha256
import json
from pathlib import Path
import re
Expand All @@ -24,8 +26,14 @@
BOTS = ["dependabot[bot]", "app/dependabot", "pre-commit-ci[bot]", "app/pre-commit-ci"]

_MAGIC_PREFIX = "@scitools-templating: please"
MAGIC_NO_PROMPT = re.compile(rf"{_MAGIC_PREFIX} no share prompt")
MAGIC_NO_NOTIFY = re.compile(rf"{_MAGIC_PREFIX} no update notification on: ([\w-]+)")
MAGIC_NO_PROMPT = re.compile(rf"{_MAGIC_PREFIX} no share prompt", re.IGNORECASE)
MAGIC_NO_NOTIFY = re.compile(rf"{_MAGIC_PREFIX} no update notification on: ([\w-]+)", re.IGNORECASE)


class ReviewType(StrEnum):
APPROVE = "approve"
COMMENT = "comment"
REQUEST_CHANGES = "request-changes"


def git_command(command: str) -> str:
Expand Down Expand Up @@ -220,16 +228,9 @@ def split_github_url(url: str) -> tuple[str, str, str]:
_, org, repo, _, ref = urlparse(url).path.split("/")
return org, repo, ref

def url_to_short_ref(url: str) -> str:
org, repo, ref = split_github_url(url)
return f"{org}/{repo}#{ref}"

pr_url = gh_json(f"pr view {pr_number}", "url")["url"]
pr_short_ref = url_to_short_ref(pr_url)
pr_repo = split_github_url(pr_url)[1]

author = gh_json(f"pr view {pr_number}", "author")["author"]["login"]

changed_files = gh_json(f"pr view {pr_number}", "files")["files"]
changed_paths = [Path(file["path"]) for file in changed_files]

Expand All @@ -249,70 +250,38 @@ def get_all_authors() -> set[str]:
for commit_author in get_commit_authors(commit)
)

def post_review(review_body: str, review_type: ReviewType) -> None:
with NamedTemporaryFile("w") as file_write:
file_write.write(review_body)
file_write.flush()
gh_command = shlex.split(
f"gh pr review {pr_number} --{review_type.value} "
f"--body-file {file_write.name}"
)
run(gh_command, check=True)

human_authors = get_all_authors() - set(BOTS)
if human_authors == set():
review_body = (
review_text = (
f"### [Templating]({SCITOOLS_URL}/.github/blob/main/templates)\n\n"
"Version numbers are not typically covered by templating. It is "
"expected that this PR is 100% about advancing version numbers, "
"which would not require any templating follow-up. **Please double-"
"check for any other changes that might be suitable for "
"templating**."
)
with NamedTemporaryFile("w") as file_write:
file_write.write(review_body)
file_write.flush()
gh_command = shlex.split(
f"gh pr review {pr_number} --comment --body-file {file_write.name}"
)
run(gh_command, check=True)
post_review(review_text, ReviewType.COMMENT)
return

def create_issue(title: str, body: str) -> None:
assignee = author

# Check that an issue with this title isn't already on the .github repo.
existing_issues = gh_json(
"issue list --state all --repo SciTools/.github", "title"
)
if any(issue["title"] == title for issue in existing_issues):
return

if assignee in BOTS:
# if the author is a bot, we don't want to assign the issue to the bot
# so instead choose a human author from the latest commit
assignee = list(human_authors)[0]

with NamedTemporaryFile("w") as file_write:
file_write.write(body)
file_write.flush()
gh_command = shlex.split(
"gh issue create "
f'--title "{title}" '
f"--body-file {file_write.name} "
"--repo SciTools/.github "
f"--assignee {assignee}"
)
issue_url = check_output(gh_command).decode("utf-8").strip()
short_ref = url_to_short_ref(issue_url)
# GitHub renders the full text of a cross-ref when it is in a list.
review_body = f"- [ ] Please see: {short_ref}"
gh_command = shlex.split(
f'gh pr review {pr_number} --request-changes --body "{review_body}"'
)
run(gh_command, check=True)

issue_title = f"Share {pr_short_ref} changes via templating?"

templates_relative = TEMPLATES_DIR.relative_to(TEMPLATE_REPO_ROOT)
templates_url = f"{SCITOOLS_URL}/.github/tree/main/{templates_relative}"
body_intro = (
f"## [Templating]({SCITOOLS_URL}/.github/blob/main/templates/README.md)\n\n"
f"{pr_short_ref} (by @{author}) includes changes that may be worth "
f"This PR includes changes that may be worth "
"sharing via templating. For each file listed below, please "
"either:\n\n"
"- Action the suggestion via a pull request editing/adding the "
f"relevant file in the [templates directory]({templates_url}). [^1]\n"
f"relevant file in the [SciTools/.github `templates/` directory]({templates_url}). [^1]\n"
"- Dismiss the suggestion if the changes are not suitable for "
"templating."
Comment thread
trexfeathers marked this conversation as resolved.
Outdated
)
Expand All @@ -337,15 +306,20 @@ def create_issue(title: str, body: str) -> None:
ignored = str(changed_path) in ignore_dict[pr_repo]
if ignored:
continue

changed_hash = sha256(str(changed_path).encode()).hexdigest()
changed_url = f"{pr_url}/files#diff-{changed_hash}"
changed_link = f"[`{changed_path}`]({changed_url})"

if is_templated:
template_relative = template.relative_to(TEMPLATE_REPO_ROOT)
template_url = (
f"{SCITOOLS_URL}/.github/blob/main/{template_relative}"
)
template_link = f"[`{template_relative}`]({template_url})"
template_link = f"[`SciTools/.github/{template_relative}`]({template_url})"

templated_list.append(
f"- [ ] `{changed_path}`, templated by {template_link}"
f"- [ ] {changed_link}, templated by {template_link}"
)

else:
Expand All @@ -359,13 +333,9 @@ def create_issue(title: str, body: str) -> None:
if changed_parent in (
git_root,
git_root / "benchmarks",
git_root / "docs" / "src",
Comment thread
HGWright marked this conversation as resolved.
):
candidates_list.append(f"- [ ] `{changed_path}`")
if changed_path in (
git_root / "docs" / "src" / "conf.py",
git_root / "docs" / "src" / "Makefile",
):
candidates_list.append(f"- [ ] `{changed_path}`")
candidates_list.append(f"- [ ] {changed_link}")

if templated_list or candidates_list:
body_args = [body_intro]
Expand All @@ -385,8 +355,8 @@ def create_issue(title: str, body: str) -> None:
f"``{pattern_repo}``"
)

issue_body = "\n".join(body_args)
create_issue(issue_title, issue_body)
review_text= "\n".join(body_args)
post_review(review_text, ReviewType.REQUEST_CHANGES)


def check_dir(args: argparse.Namespace) -> None:
Expand Down
Loading