Skip to content

Commit

Permalink
Don't post a duplicate comment
Browse files Browse the repository at this point in the history
  • Loading branch information
JordonPhillips committed Nov 19, 2024
1 parent ec7cee5 commit 86715bf
Show file tree
Hide file tree
Showing 3 changed files with 179 additions and 68 deletions.
1 change: 1 addition & 0 deletions .changes/tool/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
CHANGES_DIR = Path(__file__).absolute().parent.parent
NEXT_RELEASE_DIR = CHANGES_DIR / "next-release"
RELEASES_DIR = CHANGES_DIR / "releases"
REPO_ROOT = CHANGES_DIR.parent


class ChangeType(Enum):
Expand Down
80 changes: 12 additions & 68 deletions .changes/tool/amend.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
import json
import os
import subprocess
from http.client import HTTPResponse
from pathlib import Path
from urllib import request
from urllib.error import HTTPError

from . import CHANGES_DIR, Change
from . import REPO_ROOT, Change
from .github import post_review_comment

REPO_ROOT = CHANGES_DIR.parent
DEFAULT_REPO = "smithy-lang/smithy"


Expand Down Expand Up @@ -45,11 +41,19 @@ def amend(

if review_comment:
print("Posting amended change as a review comment.")
comment = (
"Staged changelog entries should have an associated pull request "
"set. Commit this suggestion to associate this changelog entry "
"with this PR.\n\n"
f"```suggestion\n{change.write().strip()}\n```"
)
post_review_comment(
change_file=change_file,
change=change,
repository=repository,
pr_number=pr_number,
comment=comment,
file=change_file,
start_line=1,
end_line=len(change_file.read_text().splitlines()),
)
else:
print("Writing amended change to disk.")
Expand Down Expand Up @@ -79,63 +83,3 @@ def get_new_changes(base: str | None) -> dict[Path, Change]:
print(f"Discovered newly staged changelog entry: {file}")
new_changes[file] = Change.read(file)
return new_changes


# Manually making a request without using some sort of official client or even a less
# crappy http client in order to avoid having to take a dependency.
def post_review_comment(
change_file: Path, change: Change, repository: str, pr_number: str
):
base_url = os.environ.get("GITHUB_API_URL", "https://api.github.com")
github_token = os.environ.get("GITHUB_TOKEN")
if not github_token:
raise ValueError(
"The GITHUB_TOKEN environment variable must be set to post review comments."
)

commit_sha = os.environ.get("TARGET_SHA")
if not commit_sha:
raise ValueError(
"The TARGET_SHA environment variable must be set to post review comments."
)

comment = (
"Staged changelog entries should have an associated pull request set. Commit "
"this suggestion to associate this changelog entry with this PR.\n\n"
f"```suggestion\n{change.write().strip()}\n```"
)

# https://docs.github.com/en/rest/pulls/comments?apiVersion=2022-11-28#create-a-review-comment-for-a-pull-request
request_body = json.dumps(
{
"body": comment,
"commit_id": commit_sha,
"path": str(change_file.relative_to(REPO_ROOT)),
"start_line": 1,
"start_side": "RIGHT",
"line": len(change_file.read_text().splitlines()),
"side": "RIGHT",
},
indent=2,
)
resolved_url = f"{base_url}/repos/{repository}/pulls/{pr_number}/comments"

print(
f"Sending review comment request to {resolved_url} with body:\n\n{request_body}"
)
req = request.Request(
url=resolved_url,
headers={
"Accept": "application/vnd.github+json",
"Authorization": f"Bearer {github_token}",
"X-GitHub-Api-Version": "2022-11-28",
},
data=request_body.encode("utf-8"),
)

try:
response: HTTPResponse = request.urlopen(req)
except HTTPError as e:
formatted_body = json.dumps(json.loads(e.read().decode("utf-8")), indent=4)
raise Exception(f"Failed to post:\n\n{formatted_body}") from e
print(f"Received response: {response}")
166 changes: 166 additions & 0 deletions .changes/tool/github.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
import json
import os
import re
import time
from http.client import HTTPResponse
from pathlib import Path
from typing import Generator, Literal, NotRequired, Required, TypedDict
from urllib import request
from urllib.error import HTTPError

from . import REPO_ROOT

GITHUB_API_URL = os.environ.get("GITHUB_API_URL", "https://api.github.com")
GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN")
NEXT_PAGE = re.compile(r'(?<=<)([\S]*)(?=>; rel="Next")')


def post_review_comment(
repository: str,
pr_number: str,
comment: str,
file: Path,
start_line: int | None = None,
end_line: int | None = None,
allow_duplicate: bool = False,
) -> None:
_assert_token()

commit_sha = os.environ.get("TARGET_SHA")
if not commit_sha:
raise ValueError(
"The TARGET_SHA environment variable must be set to post review comments."
)

path = str(file.relative_to(REPO_ROOT))

if not allow_duplicate:
for existing_comment in get_review_comments(repository, pr_number):
if existing_comment["path"] == path and existing_comment["body"] == comment:
print("Review comment already posted, skipping duplicate.")
return

request_body: CreateReviewCommentParams = {
"body": comment,
"commit_id": commit_sha,
"path": path,
}

if start_line is not None:
request_body["side"] = "RIGHT"
if end_line is not None:
request_body["start_side"] = "RIGHT"
request_body["start_line"] = start_line
request_body["line"] = end_line
else:
request_body["line"] = start_line
elif end_line is not None:
raise ValueError("If end_line is set, start_line must also be set.")

body = json.dumps(
request_body,
indent=2,
)
resolved_url = f"{GITHUB_API_URL}/repos/{repository}/pulls/{pr_number}/comments"

print(f"Sending review comment request to {resolved_url} with body:\n\n{body}")
req = request.Request(
url=resolved_url,
headers={
"Accept": "application/vnd.github+json",
"Authorization": f"Bearer {GITHUB_TOKEN}",
"X-GitHub-Api-Version": "2022-11-28",
},
data=body.encode("utf-8"),
)

try:
response: HTTPResponse = request.urlopen(req)
except HTTPError as e:
formatted_body = json.dumps(json.loads(e.read().decode("utf-8")), indent=4)
raise Exception(f"Failed to post comment:\n\n{formatted_body}") from e
print(f"Received response: {response}")


type ReviewSide = Literal["LEFT"] | Literal["RIGHT"]


# https://docs.github.com/en/rest/pulls/comments?apiVersion=2022-11-28#create-a-review-comment-for-a-pull-request
class CreateReviewCommentParams(TypedDict):
body: Required[str]
commit_id: Required[str]
path: Required[str]
start_line: NotRequired[int]
line: NotRequired[int]
start_side: NotRequired[ReviewSide]
side: NotRequired[ReviewSide]
subject_type: NotRequired[Literal["line"] | Literal["file"]]


class User(TypedDict):
login: Required[str]


class ReviewComment(TypedDict):
user: Required[User]
path: Required[str]
body: str


# Use a generator so that we don't necessarily have to request everything if the
# iterator finds what it wants.
def get_review_comments(
repository: str,
pr_number: str,
) -> Generator[ReviewComment]:
_assert_token()
url = f"{GITHUB_API_URL}/repos/{repository}/pulls/{pr_number}/comments?per_page=1"
for response in _paginate(url):
parsed_body = json.loads(response.read())
for comment in parsed_body:
yield comment


def _paginate(url: str) -> Generator[HTTPResponse]:
headers = {
"Accept": "application/vnd.github+json",
"Authorization": f"Bearer {GITHUB_TOKEN}",
"X-GitHub-Api-Version": "2022-11-28",
}

print(f"Paginating GitHub api: {url}")
while True:
req = request.Request(url=url, headers=headers)

try:
response: HTTPResponse = request.urlopen(req)
except HTTPError as e:
raise GitHubAPIError(e) from e

print(f"Received response: {response}")
yield response

if match := NEXT_PAGE.match(response.headers.get("link", "")):
url = match.group()
print("Waiting one minute before requesting the next page.")
time.sleep(60)
else:
print("Done paginating.")
break


class GitHubAPIError(Exception):
def __init__(self, response: HTTPError) -> None:
formatted_body = json.dumps(
json.loads(response.read().decode("utf-8")), indent=2
)
super().__init__(f"GitHub API request failed:\n\n{formatted_body}")


def _assert_token():
if not GITHUB_TOKEN:
raise ValueError(
"The GITHUB_TOKEN environment variable must be set to post review comments."
)

0 comments on commit 86715bf

Please sign in to comment.