88variables 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
1820GITHUB_BASE_REF multiple ways:
1921
20221. Ensure that the committer and author do not match any bad patterns (e.g.,
5052GOOD = "good"
5153BAD = "bad"
5254
55+ GIT_REMOTE_PR_HEAD_NAME = "prHead"
56+
5357NACP = "bot:notacherrypick"
5458
5559GITHUB_WORKSPACE = os .environ .get ('GITHUB_WORKSPACE' )
56- GITHUB_SHA = os .environ .get ('GITHUB_SHA' )
5760GITHUB_BASE_REF = os .environ .get ('GITHUB_BASE_REF' )
61+ GITHUB_HEAD_REF = os .environ .get ('GITHUB_HEAD_REF' )
5862GITHUB_TOKEN = os .environ .get ('GITHUB_TOKEN' )
5963GITHUB_REPOSITORY = os .environ .get ('GITHUB_REPOSITORY' )
6064GITHUB_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
6370if (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 \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+
88142"""
89143The results dictionary is in the following format:
90144
@@ -242,15 +296,17 @@ def _is_entirely_submodule_updates(repo, commit):
242296#----------------------------------------------------------------------------
243297
244298def 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):
292348If "bot:notacherrypick" is in the PR description, then disable the
293349cherry-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
335383def 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 ("\n Test passed: everything was good!" )
0 commit comments