8
8
variables that are available in the Github Action environment. Specifically:
9
9
10
10
* GITHUB_WORKSPACE: directory where the git clone is located
11
- * GITHUB_SHA: the git commit SHA of the artificial Github PR test merge commit
12
11
* GITHUB_BASE_REF: the git ref for the base branch
12
+ * GITHUB_HEAD_REF: the git commit ref of the head branch
13
13
* GITHUB_TOKEN: token authorizing Github API usage
14
14
* GITHUB_REPOSITORY: "org/repo" name of the Github repository of this PR
15
15
* 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
16
18
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
18
20
GITHUB_BASE_REF multiple ways:
19
21
20
22
1. Ensure that the committer and author do not match any bad patterns (e.g.,
50
52
GOOD = "good"
51
53
BAD = "bad"
52
54
55
+ GIT_REMOTE_PR_HEAD_NAME = "prHead"
56
+
53
57
NACP = "bot:notacherrypick"
54
58
55
59
GITHUB_WORKSPACE = os .environ .get ('GITHUB_WORKSPACE' )
56
- GITHUB_SHA = os .environ .get ('GITHUB_SHA' )
57
60
GITHUB_BASE_REF = os .environ .get ('GITHUB_BASE_REF' )
61
+ GITHUB_HEAD_REF = os .environ .get ('GITHUB_HEAD_REF' )
58
62
GITHUB_TOKEN = os .environ .get ('GITHUB_TOKEN' )
59
63
GITHUB_REPOSITORY = os .environ .get ('GITHUB_REPOSITORY' )
60
64
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' )
61
68
62
69
# Sanity check
63
70
if (GITHUB_WORKSPACE is None or
64
- GITHUB_SHA is None or
65
71
GITHUB_BASE_REF is None or
72
+ GITHUB_HEAD_REF is None or
66
73
GITHUB_TOKEN is None or
67
74
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 ):
69
79
print ("Error: this script is designed to run as a Github Action" )
70
80
exit (1 )
71
81
@@ -85,6 +95,50 @@ def make_commit_message(repo, hash):
85
95
86
96
#----------------------------------------------------------------------------
87
97
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 \n Please 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
+
88
142
"""
89
143
The results dictionary is in the following format:
90
144
@@ -242,15 +296,17 @@ def _is_entirely_submodule_updates(repo, commit):
242
296
#----------------------------------------------------------------------------
243
297
244
298
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}".
248
307
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 ()
254
310
255
311
#------------------------------------------------------------------------
256
312
@@ -292,15 +348,7 @@ def check_all_commits(config, repo):
292
348
If "bot:notacherrypick" is in the PR description, then disable the
293
349
cherry-pick message requirement.
294
350
"""
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 ):
304
352
if pr .body and NACP in pr .body :
305
353
config ['cherry pick required' ] = False
306
354
@@ -334,11 +382,23 @@ def load_config():
334
382
335
383
def main ():
336
384
config = load_config ()
337
- check_github_pr_description (config )
338
385
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 )
342
402
343
403
if len (results [BAD ]) == 0 :
344
404
print ("\n Test passed: everything was good!" )
0 commit comments