Skip to content

Commit 91e58bf

Browse files
authored
Merge pull request #10786 from Joe-Downs/pr/testing-commit-checker
git-commit-checks: comment on PR with error(s)
2 parents 94bed42 + ef866a4 commit 91e58bf

File tree

2 files changed

+98
-28
lines changed

2 files changed

+98
-28
lines changed

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

Lines changed: 86 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.,
@@ -50,22 +52,30 @@
5052
GOOD = "good"
5153
BAD = "bad"
5254

55+
GIT_REMOTE_PR_HEAD_NAME = "prHead"
56+
5357
NACP = "bot:notacherrypick"
5458

5559
GITHUB_WORKSPACE = os.environ.get('GITHUB_WORKSPACE')
56-
GITHUB_SHA = os.environ.get('GITHUB_SHA')
5760
GITHUB_BASE_REF = os.environ.get('GITHUB_BASE_REF')
61+
GITHUB_HEAD_REF = os.environ.get('GITHUB_HEAD_REF')
5862
GITHUB_TOKEN = os.environ.get('GITHUB_TOKEN')
5963
GITHUB_REPOSITORY = os.environ.get('GITHUB_REPOSITORY')
6064
GITHUB_REF = os.environ.get('GITHUB_REF')
65+
GITHUB_RUN_ID = os.environ.get('GITHUB_RUN_ID')
66+
GITHUB_SERVER_URL = os.environ.get('GITHUB_SERVER_URL')
67+
PR_NUM = os.environ.get('PR_NUM')
6168

6269
# Sanity check
6370
if (GITHUB_WORKSPACE is None or
64-
GITHUB_SHA is None or
6571
GITHUB_BASE_REF is None or
72+
GITHUB_HEAD_REF is None or
6673
GITHUB_TOKEN is None or
6774
GITHUB_REPOSITORY is None or
68-
GITHUB_REF is None):
75+
GITHUB_REF is None or
76+
GITHUB_RUN_ID is None or
77+
GITHUB_SERVER_URL is None or
78+
PR_NUM is None):
6979
print("Error: this script is designed to run as a Github Action")
7080
exit(1)
7181

@@ -85,6 +95,50 @@ def make_commit_message(repo, hash):
8595

8696
#----------------------------------------------------------------------------
8797

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

244298
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.
299+
# Get a list of commits that we'll be examining. Use the programmatic form
300+
# of "git log GITHUB_BASE_REF..GITHUB_HEAD_REF" (i.e., "git log
301+
# ^GITHUB_BASE_REF GITHUB_HEAD_REF") to do the heavy lifting to find that
302+
# set of commits. Because we're using pull_request_target, GITHUB_BASE_REF
303+
# is already checked out, however, we specify "origin/{GITHUB_BASE_REF}", to
304+
# disambiguate the base ref from the head ref in case of duplicate ref
305+
# names. GITHUB_HEAD_REF has never been checked out, so we specify
306+
# "{GIT_REMOTE_PR_HEAD_NAME}/{GITHUB_HEAD_REF}".
248307
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]
308+
hashes = git_cli.log(f"--pretty=format:%h",
309+
f"origin/{GITHUB_BASE_REF}..{GIT_REMOTE_PR_HEAD_NAME}/{GITHUB_HEAD_REF}").splitlines()
254310

255311
#------------------------------------------------------------------------
256312

@@ -292,15 +348,7 @@ def check_all_commits(config, repo):
292348
If "bot:notacherrypick" is in the PR description, then disable the
293349
cherry-pick message requirement.
294350
"""
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-
351+
def check_github_pr_description(config, pr):
304352
if pr.body and NACP in pr.body:
305353
config['cherry pick required'] = False
306354

@@ -334,11 +382,23 @@ def load_config():
334382

335383
def main():
336384
config = load_config()
337-
check_github_pr_description(config)
338385

339-
repo = git.Repo(GITHUB_WORKSPACE)
340-
results, hashes = check_all_commits(config, repo)
341-
print_results(results, repo, hashes)
386+
g = Github(GITHUB_TOKEN)
387+
github_repo = g.get_repo(GITHUB_REPOSITORY)
388+
pr_num = int(PR_NUM)
389+
pr = github_repo.get_pull(pr_num)
390+
391+
check_github_pr_description(config, pr)
392+
393+
# Because we're using pull_request_target, we cloned the base repo and
394+
# therefore have to add the head repo as a remote.
395+
local_repo = git.Repo(GITHUB_WORKSPACE)
396+
head_remote = git.remote.Remote.create(local_repo, GIT_REMOTE_PR_HEAD_NAME, pr.head.repo.clone_url)
397+
head_remote.fetch()
398+
399+
results, hashes = check_all_commits(config, local_repo)
400+
print_results(results, local_repo, hashes)
401+
comment_on_pr(pr, results, local_repo)
342402

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

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
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:
812
- opened
913
- synchronize
1014
- edited
1115

16+
permissions: {} # none
17+
1218
jobs:
1319
ci:
20+
permissions:
21+
pull-requests: write
1422
name: Git commit checker
1523
runs-on: ubuntu-latest
1624
steps:
@@ -29,6 +37,8 @@ jobs:
2937
run: pip install gitpython PyGithub
3038

3139
- name: Check all git commits
32-
run: $GITHUB_WORKSPACE/.github/workflows/git-commit-checks.py
40+
run:
41+
$GITHUB_WORKSPACE/.github/workflows/git-commit-checks.py
3342
env:
3443
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
44+
PR_NUM: ${{ github.event.number }}

0 commit comments

Comments
 (0)