@@ -643,16 +643,16 @@ def test_push_without_pr_commits_does_not_warn(self):
643643 self .assertNotIn ("::warning" , output )
644644
645645 @staticmethod
646- def _fake_git_and_cli (head2_resolves : bool ):
646+ def _fake_git_and_cli (resolves : bool ):
647647 """subprocess.run stand-in: answers rev-parse and the CLI alike."""
648648 commands : list [list [str ]] = []
649649
650650 def run (command , ** _kwargs ):
651651 commands .append (command )
652652 if command [:2 ] == ["git" , "rev-parse" ]:
653653 return MagicMock (
654- returncode = 0 if head2_resolves else 1 ,
655- stdout = "abc123\n " if head2_resolves else "" ,
654+ returncode = 0 if resolves else 1 ,
655+ stdout = "abc123\n " if resolves else "" ,
656656 )
657657 check = command [3 ].lstrip ("-" ).replace ("-" , "_" )
658658 return MagicMock (returncode = 0 , stdout = json_output (make_check (check )))
@@ -661,21 +661,24 @@ def run(command, **_kwargs):
661661
662662 def test_pr_author_checks_read_the_branch_tip (self ):
663663 """On refs/pull/N/merge HEAD's author is GitHub, not the contributor."""
664- run , commands = self ._fake_git_and_cli (head2_resolves = True )
664+ run , commands = self ._fake_git_and_cli (resolves = True )
665665 with (
666666 patch ("main.MESSAGE_ENABLED" , False ),
667667 patch ("main.BRANCH_ENABLED" , True ),
668668 patch ("main.AUTHOR_NAME_ENABLED" , True ),
669669 patch ("main.AUTHOR_EMAIL_ENABLED" , True ),
670- patch ("main.is_pr_event" , return_value = True ),
670+ patch .dict (os .environ , {"GITHUB_EVENT_NAME" : "pull_request" }),
671+ patch ("main.get_pr_head_sha" , return_value = None ),
671672 patch ("main.subprocess.run" , side_effect = run ),
672673 ):
673674 rc , results , output = self ._run_capturing_stdout ()
674675 self .assertEqual (rc , 0 )
675676 self .assertEqual (
676677 [s .label for s in results ], ["Branch" , "Author name" , "Author email" ]
677678 )
678- self .assertIn (["git" , "rev-parse" , "--verify" , "--quiet" , "HEAD^2" ], commands )
679+ self .assertIn (
680+ ["git" , "rev-parse" , "--verify" , "--quiet" , "HEAD^2^{commit}" ], commands
681+ )
679682 self .assertIn (
680683 ["commit-check" , "--format" , "json" , "--author-name" , "--rev" , "HEAD^2" ],
681684 commands ,
@@ -688,28 +691,104 @@ def test_pr_author_checks_read_the_branch_tip(self):
688691 self .assertIn (["commit-check" , "--format" , "json" , "--branch" ], commands )
689692 self .assertNotIn ("::warning" , output )
690693
691- def test_pr_author_checks_fall_back_to_head_on_shallow_clone (self ):
692- run , commands = self ._fake_git_and_cli (head2_resolves = False )
694+ def test_pr_author_checks_prefer_the_payload_head_sha (self ):
695+ """pull_request.head.sha names the tip for either PR event type."""
696+ run , commands = self ._fake_git_and_cli (resolves = True )
693697 with (
694698 patch ("main.MESSAGE_ENABLED" , False ),
695699 patch ("main.BRANCH_ENABLED" , False ),
696700 patch ("main.AUTHOR_NAME_ENABLED" , True ),
697701 patch ("main.AUTHOR_EMAIL_ENABLED" , False ),
698- patch ("main.is_pr_event" , return_value = True ),
702+ patch .dict (os .environ , {"GITHUB_EVENT_NAME" : "pull_request_target" }),
703+ patch ("main.get_pr_head_sha" , return_value = "deadbeefcafe" ),
699704 patch ("main.subprocess.run" , side_effect = run ),
700705 ):
701706 rc , results , output = self ._run_capturing_stdout ()
702707 self .assertEqual (rc , 0 )
703- self .assertIn (["commit-check" , "--format" , "json" , "--author-name" ], commands )
704- self .assertFalse ([c for c in commands if "--rev" in c ], commands )
708+ self .assertEqual ([s .status for s in results ], ["pass" ])
709+ self .assertIn (
710+ ["git" , "rev-parse" , "--verify" , "--quiet" , "deadbeefcafe^{commit}" ],
711+ commands ,
712+ )
713+ self .assertIn (
714+ [
715+ "commit-check" ,
716+ "--format" ,
717+ "json" ,
718+ "--author-name" ,
719+ "--rev" ,
720+ "deadbeefcafe" ,
721+ ],
722+ commands ,
723+ )
724+ self .assertNotIn ("::warning" , output )
725+
726+ def test_pr_author_checks_are_skipped_on_a_shallow_clone (self ):
727+ """HEAD's author is GitHub's merge commit: skip rather than grade it."""
728+ run , commands = self ._fake_git_and_cli (resolves = False )
729+ with (
730+ patch ("main.MESSAGE_ENABLED" , False ),
731+ patch ("main.BRANCH_ENABLED" , True ),
732+ patch ("main.AUTHOR_NAME_ENABLED" , True ),
733+ patch ("main.AUTHOR_EMAIL_ENABLED" , True ),
734+ patch .dict (os .environ , {"GITHUB_EVENT_NAME" : "pull_request" }),
735+ patch ("main.get_pr_head_sha" , return_value = None ),
736+ patch ("main.subprocess.run" , side_effect = run ),
737+ ):
738+ rc , results , output = self ._run_capturing_stdout ()
739+ self .assertEqual (rc , 0 )
740+ self .assertEqual (
741+ [(s .label , s .status ) for s in results ],
742+ [("Author name" , "skip" ), ("Author email" , "skip" ), ("Branch" , "pass" )],
743+ )
744+ self .assertEqual (
745+ results [0 ].checks ,
746+ [
747+ {
748+ "rule_id" : "CC101" ,
749+ "check" : "author_name" ,
750+ "status" : "skip" ,
751+ "value" : "" ,
752+ "error" : "" ,
753+ "suggest" : "" ,
754+ "docs_url" : "" ,
755+ }
756+ ],
757+ )
758+ self .assertEqual (results [1 ].checks [0 ]["rule_id" ], "CC102" )
759+ self .assertFalse ([c for c in commands if "--author-name" in c ], commands )
760+ self .assertFalse ([c for c in commands if "--author-email" in c ], commands )
761+ self .assertIn (["commit-check" , "--format" , "json" , "--branch" ], commands )
705762 warning = [ln for ln in output .splitlines () if ln .startswith ("::warning" )]
706763 self .assertEqual (len (warning ), 1 , output )
707764 self .assertTrue (warning [0 ].startswith ("::warning title=commit-check::" ))
708- self .assertIn ("Could not resolve HEAD^2 " , warning [0 ])
765+ self .assertIn ("Could not resolve the pull request's head commit " , warning [0 ])
709766 self .assertIn ("is actions/checkout using fetch-depth: 0?" , warning [0 ])
767+ self .assertIn ("they were skipped" , warning [0 ])
768+
769+ def test_pull_request_target_never_uses_head2 (self ):
770+ """On pull_request_target HEAD is the base branch; HEAD^2 is unrelated."""
771+ run , commands = self ._fake_git_and_cli (resolves = True )
772+ with (
773+ patch ("main.MESSAGE_ENABLED" , False ),
774+ patch ("main.BRANCH_ENABLED" , False ),
775+ patch ("main.AUTHOR_NAME_ENABLED" , True ),
776+ patch ("main.AUTHOR_EMAIL_ENABLED" , False ),
777+ patch .dict (os .environ , {"GITHUB_EVENT_NAME" : "pull_request_target" }),
778+ patch ("main.get_pr_head_sha" , return_value = None ),
779+ patch ("main.subprocess.run" , side_effect = run ),
780+ ):
781+ rc , results , output = self ._run_capturing_stdout ()
782+ self .assertEqual (rc , 0 )
783+ self .assertEqual (
784+ [(s .label , s .status ) for s in results ], [("Author name" , "skip" )]
785+ )
786+ self .assertFalse ([c for c in commands if "HEAD^2^{commit}" in c ], commands )
787+ self .assertFalse ([c for c in commands if c [0 ] == "commit-check" ], commands )
788+ self .assertIn ("::warning title=commit-check::" , output )
710789
711790 def test_push_author_checks_never_pass_rev (self ):
712- run , commands = self ._fake_git_and_cli (head2_resolves = True )
791+ run , commands = self ._fake_git_and_cli (resolves = True )
713792 with (
714793 patch ("main.MESSAGE_ENABLED" , False ),
715794 patch ("main.BRANCH_ENABLED" , False ),
@@ -725,25 +804,101 @@ def test_push_author_checks_never_pass_rev(self):
725804
726805
727806class TestPrHeadRev (unittest .TestCase ):
728- def test_resolving_head2_returns_the_revision (self ):
729- with patch (
730- "main.subprocess.run" , return_value = MagicMock (returncode = 0 )
731- ) as mock_run :
807+ def test_payload_head_sha_wins_when_the_clone_has_it (self ):
808+ with (
809+ patch ("main.get_pr_head_sha" , return_value = "abc123" ),
810+ patch (
811+ "main.subprocess.run" , return_value = MagicMock (returncode = 0 )
812+ ) as mock_run ,
813+ ):
814+ self .assertEqual (main .pr_head_rev (), "abc123" )
815+ self .assertEqual (
816+ mock_run .call_args [0 ][0 ],
817+ ["git" , "rev-parse" , "--verify" , "--quiet" , "abc123^{commit}" ],
818+ )
819+
820+ def test_pull_request_falls_back_to_head2 (self ):
821+ with (
822+ patch .dict (os .environ , {"GITHUB_EVENT_NAME" : "pull_request" }),
823+ patch ("main.get_pr_head_sha" , return_value = None ),
824+ patch (
825+ "main.subprocess.run" , return_value = MagicMock (returncode = 0 )
826+ ) as mock_run ,
827+ ):
732828 self .assertEqual (main .pr_head_rev (), "HEAD^2" )
733829 self .assertEqual (
734830 mock_run .call_args [0 ][0 ],
735- ["git" , "rev-parse" , "--verify" , "--quiet" , "HEAD^2" ],
831+ ["git" , "rev-parse" , "--verify" , "--quiet" , "HEAD^2^{commit} " ],
736832 )
737833
834+ def test_pull_request_target_does_not_fall_back_to_head2 (self ):
835+ with (
836+ patch .dict (os .environ , {"GITHUB_EVENT_NAME" : "pull_request_target" }),
837+ patch ("main.get_pr_head_sha" , return_value = None ),
838+ patch (
839+ "main.subprocess.run" , return_value = MagicMock (returncode = 0 )
840+ ) as mock_run ,
841+ ):
842+ self .assertIsNone (main .pr_head_rev ())
843+ mock_run .assert_not_called ()
844+
845+ def test_unfetched_payload_sha_on_pull_request_target_returns_none (self ):
846+ with (
847+ patch .dict (os .environ , {"GITHUB_EVENT_NAME" : "pull_request_target" }),
848+ patch ("main.get_pr_head_sha" , return_value = "abc123" ),
849+ patch ("main.subprocess.run" , return_value = MagicMock (returncode = 1 )),
850+ ):
851+ self .assertIsNone (main .pr_head_rev ())
852+
738853 def test_shallow_clone_returns_none (self ):
739- with patch ("main.subprocess.run" , return_value = MagicMock (returncode = 1 )):
854+ with (
855+ patch .dict (os .environ , {"GITHUB_EVENT_NAME" : "pull_request" }),
856+ patch ("main.get_pr_head_sha" , return_value = None ),
857+ patch ("main.subprocess.run" , return_value = MagicMock (returncode = 1 )),
858+ ):
740859 self .assertIsNone (main .pr_head_rev ())
741860
742861 def test_missing_git_returns_none (self ):
743- with patch ("main.subprocess.run" , side_effect = OSError ("no git" )):
862+ with (
863+ patch .dict (os .environ , {"GITHUB_EVENT_NAME" : "pull_request" }),
864+ patch ("main.get_pr_head_sha" , return_value = None ),
865+ patch ("main.subprocess.run" , side_effect = OSError ("no git" )),
866+ ):
744867 self .assertIsNone (main .pr_head_rev ())
745868
746869
870+ class TestGetPrHeadSha (unittest .TestCase ):
871+ def test_reads_the_head_sha_from_the_event (self ):
872+ with tempfile .NamedTemporaryFile ("w" , suffix = ".json" , delete = False ) as f :
873+ json .dump ({"pull_request" : {"head" : {"sha" : "abc123" }}}, f )
874+ event_path = f .name
875+ try :
876+ with patch .dict (
877+ os .environ ,
878+ {
879+ "GITHUB_EVENT_NAME" : "pull_request_target" ,
880+ "GITHUB_EVENT_PATH" : event_path ,
881+ },
882+ ):
883+ self .assertEqual (main .get_pr_head_sha (), "abc123" )
884+ finally :
885+ os .unlink (event_path )
886+
887+ def test_not_a_pr_event_returns_none (self ):
888+ with patch .dict (os .environ , {"GITHUB_EVENT_NAME" : "push" }):
889+ self .assertIsNone (main .get_pr_head_sha ())
890+
891+ def test_unreadable_event_returns_none (self ):
892+ with patch .dict (
893+ os .environ ,
894+ {
895+ "GITHUB_EVENT_NAME" : "pull_request" ,
896+ "GITHUB_EVENT_PATH" : "/nonexistent.json" ,
897+ },
898+ ):
899+ self .assertIsNone (main .get_pr_head_sha ())
900+
901+
747902class TestCommitCheckVersionPin (unittest .TestCase ):
748903 """The warn rendering is inert against an engine that never emits it.
749904
0 commit comments