Skip to content

Commit 4b0ce7e

Browse files
committed
git-commit-checks: comment on PR with error(s)
This adds comments onto the pull request containing what caused the commit checks to fail, if any, and suggests fixes to the user. If no errors are raised, no comment is made. GitHub says there's a limit of 65536 characters on comments. If the bot's comment is over that limit, it will truncate the comment to fit, and add a message explaining where the remaining errors can be found. Unfortunately, the GitHub API doesn't seem to provide a job's unique ID for linking to a job run (this is different than an action run: ".../runs/..." vs ".../actions/runs/...", respectively), so we can't directly link to the error messages printed to the console. Additionally, to create this link, two new environment variables are used: GITHUB_RUN_ID and GITHUB_SERVER_URL. Because we need the PR object twice, check_github_pr_description() was also changed to have the PR object passed into it; the PR object is gotten with a new function, get_github_pr(). The GitHub action configuration was changed to run on pull_request_target, instead of pull_request. This allows the action to be run in the context of the base of the PR, rather than in the context of the merge commit. Therefore, the action is run even if the PR has merge conflicts. Because of how the branch contexts are different between pull_request and pull_request_target, other parts of the Python script had to change to reflect these differences. Signed-off-by: Joe Downs <joe@dwns.dev>
1 parent f0101dc commit 4b0ce7e

File tree

2 files changed

+82
-27
lines changed

2 files changed

+82
-27
lines changed

.github/workflows/git-commit-checks.py

Lines changed: 76 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@
88
variables that are available in the Github Action environment. Specifically:
99
1010
* GITHUB_WORKSPACE: directory where the git clone is located
11-
* GITHUB_SHA: the git commit SHA of the artificial Github PR test merge commit
1211
* GITHUB_BASE_REF: the git ref for the base branch
12+
* GITHUB_HEAD_REF: the git commit ref of the head branch
1313
* GITHUB_TOKEN: token authorizing Github API usage
1414
* GITHUB_REPOSITORY: "org/repo" name of the Github repository of this PR
1515
* GITHUB_REF: string that includes this Github PR number
16+
* GITHUB_RUN_ID: unique ID for each workflow run
17+
* GITHUB_SERVER_URL: the URL of the GitHub server
1618
17-
This script tests each git commit between (and not including) GITHUB_SHA and
19+
This script tests each git commit between (and not including) GITHUB_HEAD_REF and
1820
GITHUB_BASE_REF multiple ways:
1921
2022
1. Ensure that the committer and author do not match any bad patterns (e.g.,
@@ -53,19 +55,25 @@
5355
NACP = "bot:notacherrypick"
5456

5557
GITHUB_WORKSPACE = os.environ.get('GITHUB_WORKSPACE')
56-
GITHUB_SHA = os.environ.get('GITHUB_SHA')
5758
GITHUB_BASE_REF = os.environ.get('GITHUB_BASE_REF')
59+
GITHUB_HEAD_REF = os.environ.get('GITHUB_HEAD_REF')
5860
GITHUB_TOKEN = os.environ.get('GITHUB_TOKEN')
5961
GITHUB_REPOSITORY = os.environ.get('GITHUB_REPOSITORY')
6062
GITHUB_REF = os.environ.get('GITHUB_REF')
63+
GITHUB_RUN_ID = os.environ.get('GITHUB_RUN_ID')
64+
GITHUB_SERVER_URL = os.environ.get('GITHUB_SERVER_URL')
65+
PR_NUM = os.environ.get('PR_NUM')
6166

6267
# Sanity check
6368
if (GITHUB_WORKSPACE is None or
64-
GITHUB_SHA is None or
6569
GITHUB_BASE_REF is None or
70+
GITHUB_HEAD_REF is None or
6671
GITHUB_TOKEN is None or
6772
GITHUB_REPOSITORY is None or
68-
GITHUB_REF is None):
73+
GITHUB_REF is None or
74+
GITHUB_RUN_ID is None or
75+
GITHUB_SERVER_URL is None or
76+
PR_NUM is None):
6977
print("Error: this script is designed to run as a Github Action")
7078
exit(1)
7179

@@ -85,6 +93,50 @@ def make_commit_message(repo, hash):
8593

8694
#----------------------------------------------------------------------------
8795

96+
"""
97+
Iterate through the BAD results, collect the error messages, and send a nicely
98+
formatted comment to the PR.
99+
100+
For the structure of the results dictionary, see comment for print_results()
101+
below.
102+
103+
"""
104+
def comment_on_pr(pr, results, repo):
105+
# If there are no BAD results, just return without posting a comment to the
106+
# GitHub PR.
107+
if len(results[BAD]) == 0:
108+
return
109+
110+
comment = "Hello! The Git Commit Checker CI bot found a few problems with this PR:"
111+
for hash, entry in results[BAD].items():
112+
comment += f"\n\n**{hash[:8]}: {make_commit_message(repo, hash)}**"
113+
for check_name, message in entry.items():
114+
if message is not None:
115+
comment += f"\n * *{check_name}: {message}*"
116+
comment_footer = "\n\nPlease fix these problems and, if necessary, force-push new commits back up to the PR branch. Thanks!"
117+
118+
# GitHub says that 65536 characters is the limit of comment messages, so
119+
# check if our comment is over that limit. If it is, truncate it to fit, and
120+
# add a message explaining with a link to the full error list.
121+
comment_char_limit = 65536
122+
if len(comment + comment_footer) >= comment_char_limit:
123+
run_url = f"{GITHUB_SERVER_URL}/{GITHUB_REPOSITORY}/actions/runs/{GITHUB_RUN_ID}?check_suite_focus=true"
124+
truncation_message = f"\n\n**Additional errors could not be shown...\n[Please click here for a full list of errors.]({run_url})**"
125+
# Cut the comment down so we can get the comment itself, and the new
126+
# message in.
127+
comment = comment[:(comment_char_limit - len(comment_footer + truncation_message))]
128+
# In case a newline gets split in half, remove the leftover '\' (if
129+
# there is one). (This is purely an aesthetics choice).
130+
comment = comment.rstrip("\\")
131+
comment += truncation_message
132+
133+
comment += comment_footer
134+
pr.create_issue_comment(comment)
135+
136+
return
137+
138+
#----------------------------------------------------------------------------
139+
88140
"""
89141
The results dictionary is in the following format:
90142
@@ -242,15 +294,15 @@ def _is_entirely_submodule_updates(repo, commit):
242294
#----------------------------------------------------------------------------
243295

244296
def check_all_commits(config, repo):
245-
# Get a list of commits that we'll be examining. Use the progromatic form
246-
# of "git log GITHUB_BASE_REF..GITHUB_SHA" (i.e., "git log ^GITHUB_BASE_REF
247-
# GITHUB_SHA") to do the heavy lifting to find that set of commits.
297+
# Get a list of commits that we'll be examining. Use the programmatic form
298+
# of "git log GITHUB_BASE_REF..GITHUB_HEAD_REF" (i.e., "git log
299+
# ^GITHUB_BASE_REF GITHUB_HEAD_REF") to do the heavy lifting to find that
300+
# set of commits. Because we're using pull_request_target, GITHUB_BASE_REF
301+
# is already checked out. GITHUB_HEAD_REF has never been checked out, so we
302+
# specify "origin/{GITHUB_HEAD_REF}".
248303
git_cli = git.cmd.Git(GITHUB_WORKSPACE)
249-
hashes = git_cli.log(f"--pretty=format:%h", f"origin/{GITHUB_BASE_REF}..{GITHUB_SHA}").splitlines()
250-
251-
# The first entry in the list will be the artificial Github merge commit for
252-
# this PR. We don't want to examine this commit.
253-
del hashes[0]
304+
hashes = git_cli.log(f"--pretty=format:%h",
305+
f"{GITHUB_BASE_REF}..origin/{GITHUB_HEAD_REF}").splitlines()
254306

255307
#------------------------------------------------------------------------
256308

@@ -292,15 +344,7 @@ def check_all_commits(config, repo):
292344
If "bot:notacherrypick" is in the PR description, then disable the
293345
cherry-pick message requirement.
294346
"""
295-
def check_github_pr_description(config):
296-
g = Github(GITHUB_TOKEN)
297-
repo = g.get_repo(GITHUB_REPOSITORY)
298-
299-
# Extract the PR number from GITHUB_REF
300-
match = re.search("/(\d+)/", GITHUB_REF)
301-
pr_num = int(match.group(1))
302-
pr = repo.get_pull(pr_num)
303-
347+
def check_github_pr_description(config, pr):
304348
if pr.body and NACP in pr.body:
305349
config['cherry pick required'] = False
306350

@@ -334,11 +378,17 @@ def load_config():
334378

335379
def main():
336380
config = load_config()
337-
check_github_pr_description(config)
338381

339-
repo = git.Repo(GITHUB_WORKSPACE)
340-
results, hashes = check_all_commits(config, repo)
341-
print_results(results, repo, hashes)
382+
g = Github(GITHUB_TOKEN)
383+
github_repo = g.get_repo(GITHUB_REPOSITORY)
384+
pr_num = int(PR_NUM)
385+
pr = github_repo.get_pull(pr_num)
386+
check_github_pr_description(config, pr)
387+
388+
local_repo = git.Repo(GITHUB_WORKSPACE)
389+
results, hashes = check_all_commits(config, local_repo)
390+
print_results(results, local_repo, hashes)
391+
comment_on_pr(pr, results, local_repo)
342392

343393
if len(results[BAD]) == 0:
344394
print("\nTest passed: everything was good!")

.github/workflows/git-commit-checks.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
name: GitHub Action CI
22

3+
# We're using pull_request_target here instead of just pull_request so that the
4+
# action runs in the context of the base of the pull request, rather than in the
5+
# context of the merge commit. For more detail about the differences, see:
6+
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target
37
on:
4-
pull_request:
8+
pull_request_target:
59
# We don't need this to be run on all types of PR behavior
610
# See https://docs.github.com/en/actions/reference/events-that-trigger-workflows#pull_request
711
types:
@@ -32,3 +36,4 @@ jobs:
3236
run: $GITHUB_WORKSPACE/.github/workflows/git-commit-checks.py
3337
env:
3438
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
39+
PR_NUM: ${{ github.event.number }}

0 commit comments

Comments
 (0)