From f45858836c3c75616f6428ae3433b5b382554ff5 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Wed, 29 Jul 2026 23:49:42 +0300 Subject: [PATCH 1/5] test: add coverage for add_pr_comments PyGithub interaction paths The add_pr_comments function had zero test coverage for its core PyGithub interaction logic. Only the disabled and fork-pr early-return paths were tested. Add 8 new test cases covering: - Creating a new comment when none exists (success and failure paths) - Skipping update when comment is already up-to-date - Updating the last comment and deleting stale older ones - Missing GITHUB_TOKEN - GithubException(403) with warning output - GithubException(non-403) with error output - Generic Exception handling --- main_test.py | 176 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) diff --git a/main_test.py b/main_test.py index 1f5ef70..61caefa 100644 --- a/main_test.py +++ b/main_test.py @@ -691,6 +691,182 @@ def test_fork_pr_writes_job_summary_hint(self): self.assertIn("read-only", content) self.assertIn("fork-pr-comments", content) + # --- PyGithub interaction tests --- + + def _mock_github_chain(self): + """Set up mocked PyGithub objects for comment interaction tests.""" + mock_github = MagicMock() + mock_repo = MagicMock() + mock_issue = MagicMock() + mock_github.get_repo.return_value = mock_repo + mock_repo.get_issue.return_value = mock_issue + return mock_github, mock_repo, mock_issue + + def _patch_github_env(self, token="fake-token", repo="test/repo"): + return patch.dict( + os.environ, + {"GITHUB_TOKEN": token, "GITHUB_REPOSITORY": repo}, + ) + + def test_new_comment_created_when_failure(self): + """No existing comment: creates a new comment, returns 1 when result has content.""" + mock_github, _, mock_issue = self._mock_github_chain() + mock_issue.get_comments.return_value = [] + with ( + patch("main.PR_COMMENTS_ENABLED", True), + patch("main.is_fork_pr", return_value=False), + patch("github.Github", return_value=mock_github), + patch("main.get_pr_number", return_value=42), + patch("main.read_result_file", return_value="bad commit message"), + self._patch_github_env(), + patch("builtins.print"), + ): + rc = main.add_pr_comments() + self.assertEqual(rc, 1) + mock_issue.create_comment.assert_called_once() + body = mock_issue.create_comment.call_args[1]["body"] + self.assertIn(main.FAILURE_TITLE, body) + self.assertIn("bad commit message", body) + + def test_new_comment_created_when_success(self): + """No existing comment: creates a new success comment, returns 0.""" + mock_github, _, mock_issue = self._mock_github_chain() + mock_issue.get_comments.return_value = [] + with ( + patch("main.PR_COMMENTS_ENABLED", True), + patch("main.is_fork_pr", return_value=False), + patch("github.Github", return_value=mock_github), + patch("main.get_pr_number", return_value=42), + patch("main.read_result_file", return_value=None), + self._patch_github_env(), + patch("builtins.print"), + ): + rc = main.add_pr_comments() + self.assertEqual(rc, 0) + mock_issue.create_comment.assert_called_once() + body = mock_issue.create_comment.call_args[1]["body"] + self.assertEqual(body, main.SUCCESS_TITLE) + + def test_comment_up_to_date_skips(self): + """Existing comment with same body: no edit/create, returns 0.""" + mock_github, _, mock_issue = self._mock_github_chain() + existing = MagicMock() + existing.body = main.SUCCESS_TITLE + mock_issue.get_comments.return_value = [existing] + with ( + patch("main.PR_COMMENTS_ENABLED", True), + patch("main.is_fork_pr", return_value=False), + patch("github.Github", return_value=mock_github), + patch("main.get_pr_number", return_value=42), + patch("main.read_result_file", return_value=None), + self._patch_github_env(), + patch("builtins.print"), + ): + rc = main.add_pr_comments() + self.assertEqual(rc, 0) + existing.edit.assert_not_called() + existing.delete.assert_not_called() + mock_issue.create_comment.assert_not_called() + + def test_comment_updated_old_deleted(self): + """Multiple existing comments with stale body: update last, delete rest.""" + mock_github, _, mock_issue = self._mock_github_chain() + old_comment = MagicMock() + old_comment.body = main.FAILURE_TITLE + "\n```\nold message\n```" + last_comment = MagicMock() + last_comment.body = main.FAILURE_TITLE + "\n```\nolder message\n```" + mock_issue.get_comments.return_value = [old_comment, last_comment] + with ( + patch("main.PR_COMMENTS_ENABLED", True), + patch("main.is_fork_pr", return_value=False), + patch("github.Github", return_value=mock_github), + patch("main.get_pr_number", return_value=42), + patch("main.read_result_file", return_value="new error"), + self._patch_github_env(), + patch("builtins.print"), + ): + rc = main.add_pr_comments() + self.assertEqual(rc, 1) + last_comment.edit.assert_called_once() + old_comment.delete.assert_called_once() + mock_issue.create_comment.assert_not_called() + edited_body = last_comment.edit.call_args[0][0] + self.assertIn("new error", edited_body) + + def test_missing_token_returns_zero(self): + """GITHUB_TOKEN not set: catches ValueError, returns 0.""" + with ( + patch("main.PR_COMMENTS_ENABLED", True), + patch("main.is_fork_pr", return_value=False), + patch("main.get_pr_number", return_value=42), + patch.dict(os.environ, {"GITHUB_REPOSITORY": "test/repo"}), + patch("os.getenv", return_value=None), + patch("builtins.print"), + ): + rc = main.add_pr_comments() + self.assertEqual(rc, 0) + + def test_403_forbidden_returns_warning(self): + """GithubException(403): prints warning, returns 0.""" + from github import GithubException + + mock_github, mock_repo, _ = self._mock_github_chain() + mock_repo.get_issue.side_effect = GithubException( + 403, {"message": "Resource not accessible by integration"} + ) + with ( + patch("main.PR_COMMENTS_ENABLED", True), + patch("main.is_fork_pr", return_value=False), + patch("github.Github", return_value=mock_github), + patch("main.get_pr_number", return_value=42), + self._patch_github_env(), + patch("builtins.print") as mock_print, + ): + rc = main.add_pr_comments() + self.assertEqual(rc, 0) + warning_text = mock_print.call_args_list[-1][0][0] + self.assertIn("warning", warning_text) + self.assertIn("403", warning_text) + + def test_github_exception_returns_zero(self): + """GithubException(non-403): prints error, returns 0.""" + from github import GithubException + + mock_github, mock_repo, _ = self._mock_github_chain() + mock_repo.get_issue.side_effect = GithubException( + 500, {"message": "Internal Server Error"} + ) + with ( + patch("main.PR_COMMENTS_ENABLED", True), + patch("main.is_fork_pr", return_value=False), + patch("github.Github", return_value=mock_github), + patch("main.get_pr_number", return_value=42), + self._patch_github_env(), + patch("builtins.print") as mock_print, + ): + rc = main.add_pr_comments() + self.assertEqual(rc, 0) + error_text = mock_print.call_args_list[-1][0][0] + self.assertIn("Error posting PR comment", error_text) + self.assertIn("500", error_text) + + def test_generic_exception_returns_zero(self): + """Generic Exception: prints error, returns 0.""" + mock_github, mock_repo, _ = self._mock_github_chain() + mock_repo.get_issue.side_effect = ValueError("unexpected error") + with ( + patch("main.PR_COMMENTS_ENABLED", True), + patch("main.is_fork_pr", return_value=False), + patch("github.Github", return_value=mock_github), + patch("main.get_pr_number", return_value=42), + self._patch_github_env(), + patch("builtins.print") as mock_print, + ): + rc = main.add_pr_comments() + self.assertEqual(rc, 0) + error_text = mock_print.call_args_list[-1][0][0] + self.assertIn("Error posting PR comment", error_text) + class TestIsForkPrWithReadonlyToken(unittest.TestCase): def test_fork_pr_with_pull_request_event(self): From cf3191bb68ccf6e22f2b42137e086fb76640e413 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Wed, 29 Jul 2026 23:52:47 +0300 Subject: [PATCH 2/5] ci: add coverage workflow and Codecov badge --- .coverage | Bin 0 -> 53248 bytes .github/workflows/coverage.yml | 30 ++++++++++++++++++++++++++++++ README.md | 1 + 3 files changed, 31 insertions(+) create mode 100644 .coverage create mode 100644 .github/workflows/coverage.yml diff --git a/.coverage b/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..59643fd9380bbdd85b66b1188b0cbc4da2606159 GIT binary patch literal 53248 zcmeI)|7#m%90%~bq-%O<%2mn;wT?bNIJ&STU6|sx!B$t^@5#}%cD*glh<_4k&6Tuee=X1|pzI4qPGFVsMN0VOep69vG z^LoA|O`3PloVG(Lnx5}ip*X5dYr3wT5JJ;5gKkrFOSTN{j3p1~w?42xYS+-pU;H>_ zzNh8J-`C8Krk*xSxs}|XlV9duG|o@jnW9mq6WAaC0SG`~&j@tS%Ta#btJ)3970MMVqV3y`<*$p2a=jQOh_4pPwNM?PLZrRoatd)M z&R!6{Y)W6cbs4BA4%v-Dw(!|THj|&3(QnA8PL^M%pUI)x(1CtcPBp2Zy5~1I`kHUK z^;H=Zh1IKJ-J>|yLRDMea~4BSv}`x3rW@Fy?YTm($$BT0jU6jkWVB=>yusQ~Sr@oM z7gWypS_3-$%sb?algF8x6rAfNto@99Y04bM>~@?Qk$>EA+T1vlLtQxZM&%rpH}vAl zK6Kmc(3buB4mD>&&Mm5PbzJ*WNA?=ODEc{zQ8iqzD7X^T0}f7(=Uib~ zJc?Q^;WeWn!?}fO9O})3V`eeIuzVh9!iy79q>r_?aj+DFsI|P>a9@jhOdx51&|Hut zYj)f^50!xHQ1>oN-&&F7_^g^19(PBgu2V6_^UL$2x=ybPR4cvRYUY+DE(KK!;7u8R9(=ff2E!oKnREBi+U&LLDQd_R7D zIu(Wdc{RQ;mS?P_{BoO2xKzh#sNv3yGEm}`Re4`=YBf4(g)+1qDL6?KOw_Ti zSb?bfl1|2JX%rZVt!|G>74rt2N#;;ho5Di+L4AjtxLViz$lnt5~Ag@=G3009U<00Izz00bZa0SG_<0uazn>iZ0%o84M3 z+#AzQ+_`t>C-3gxuV4M{`>}sF4Q=b2KknQ;ctrp3>c+3XeS7z-pFjBL_{6is_S`d{ zT#fbgwXfe&@BeRV<|arX009U<00Izz00bZa0SG_<0ub1P0%K!_5xxJfP4B_lp-K>d z00bZa0SG_<0uX=z1Rwwb2=oN_{eQgv?{Q!U0uX=z1Rwwb2tWV=5P$##Ah0I|`2YX$ f`hQP0Eh+^82tWV=5P$##AOHafKmY;|=n4D>LsWhv literal 0 HcmV?d00001 diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..c92e353 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,30 @@ +name: Coverage + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7.0.1 + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + - name: Install dependencies + run: | + pip install -r requirements.txt + pip install pytest pytest-cov + - name: Run tests with coverage + run: | + pytest --cov=main --cov-report=xml --cov-report=term-missing main_test.py + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v7 + with: + files: ./coverage.xml + flags: unittests + name: codecov-umbrella + fail_ci_if_error: true diff --git a/README.md b/README.md index 6d8a7bc..8ff5f05 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ [![GitHub marketplace](https://img.shields.io/badge/Marketplace-commit--check--action-blue)](https://github.com/marketplace/actions/commit-check-action) [![commit-check](https://img.shields.io/badge/commit--check-enabled-brightgreen?logo=Git&logoColor=white&color=%232c9ccd)](https://github.com/commit-check/commit-check) [![slsa-badge](https://slsa.dev/images/gh-badge-level3.svg?color=blue)](https://github.com/commit-check/commit-check-action/blob/a2873ca0482dd505c93fb51861c953e82fd0a186/action.yml#L59-L69) +[![codecov](https://codecov.io/gh/commit-check/commit-check-action/graph/badge.svg?token=G3R0LFO0YF)](https://codecov.io/gh/commit-check/commit-check-action) A GitHub Action for checking commit message formatting, branch naming, committer name, email, commit signoff, and more. From 7f913118b44f39c6549a7da7bd4621923bb3c549 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Wed, 29 Jul 2026 23:53:03 +0300 Subject: [PATCH 3/5] chore: add .gitignore, remove accidentally committed .coverage --- .coverage | Bin 53248 -> 0 bytes .gitignore | 6 +++--- 2 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 .coverage diff --git a/.coverage b/.coverage deleted file mode 100644 index 59643fd9380bbdd85b66b1188b0cbc4da2606159..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53248 zcmeI)|7#m%90%~bq-%O<%2mn;wT?bNIJ&STU6|sx!B$t^@5#}%cD*glh<_4k&6Tuee=X1|pzI4qPGFVsMN0VOep69vG z^LoA|O`3PloVG(Lnx5}ip*X5dYr3wT5JJ;5gKkrFOSTN{j3p1~w?42xYS+-pU;H>_ zzNh8J-`C8Krk*xSxs}|XlV9duG|o@jnW9mq6WAaC0SG`~&j@tS%Ta#btJ)3970MMVqV3y`<*$p2a=jQOh_4pPwNM?PLZrRoatd)M z&R!6{Y)W6cbs4BA4%v-Dw(!|THj|&3(QnA8PL^M%pUI)x(1CtcPBp2Zy5~1I`kHUK z^;H=Zh1IKJ-J>|yLRDMea~4BSv}`x3rW@Fy?YTm($$BT0jU6jkWVB=>yusQ~Sr@oM z7gWypS_3-$%sb?algF8x6rAfNto@99Y04bM>~@?Qk$>EA+T1vlLtQxZM&%rpH}vAl zK6Kmc(3buB4mD>&&Mm5PbzJ*WNA?=ODEc{zQ8iqzD7X^T0}f7(=Uib~ zJc?Q^;WeWn!?}fO9O})3V`eeIuzVh9!iy79q>r_?aj+DFsI|P>a9@jhOdx51&|Hut zYj)f^50!xHQ1>oN-&&F7_^g^19(PBgu2V6_^UL$2x=ybPR4cvRYUY+DE(KK!;7u8R9(=ff2E!oKnREBi+U&LLDQd_R7D zIu(Wdc{RQ;mS?P_{BoO2xKzh#sNv3yGEm}`Re4`=YBf4(g)+1qDL6?KOw_Ti zSb?bfl1|2JX%rZVt!|G>74rt2N#;;ho5Di+L4AjtxLViz$lnt5~Ag@=G3009U<00Izz00bZa0SG_<0uazn>iZ0%o84M3 z+#AzQ+_`t>C-3gxuV4M{`>}sF4Q=b2KknQ;ctrp3>c+3XeS7z-pFjBL_{6is_S`d{ zT#fbgwXfe&@BeRV<|arX009U<00Izz00bZa0SG_<0ub1P0%K!_5xxJfP4B_lp-K>d z00bZa0SG_<0uX=z1Rwwb2=oN_{eQgv?{Q!U0uX=z1Rwwb2tWV=5P$##Ah0I|`2YX$ f`hQP0Eh+^82tWV=5P$##AOHafKmY;|=n4D>LsWhv diff --git a/.gitignore b/.gitignore index ab3375b..4bd91fe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -venv/ -.venv/ +.coverage +.coverage.* +*.pyc __pycache__/ -result.txt From 4c3c694030fa918dcd757da3d3fc25bdfb5468f0 Mon Sep 17 00:00:00 2001 From: Xianpeng Shen Date: Wed, 29 Jul 2026 23:56:53 +0300 Subject: [PATCH 4/5] chore: apply suggestions from code review --- .github/workflows/coverage.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index c92e353..9922b90 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -6,6 +6,9 @@ on: pull_request: branches: [main] +permissions: + contents: read + jobs: coverage: runs-on: ubuntu-latest From e69fedd890515c030c076630616ee801891d07ac Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Thu, 30 Jul 2026 00:47:45 +0300 Subject: [PATCH 5/5] chore: lock GitHub Actions to commit SHAs for supply-chain security --- .github/workflows/commit-check.yml | 2 +- .github/workflows/coverage.yml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/commit-check.yml b/.github/workflows/commit-check.yml index 8a5f8da..9b8b30b 100644 --- a/.github/workflows/commit-check.yml +++ b/.github/workflows/commit-check.yml @@ -16,7 +16,7 @@ jobs: contents: read pull-requests: write steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 # Required for merge-base checks - uses: ./ # self test diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 9922b90..e35bfa7 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -13,8 +13,8 @@ jobs: coverage: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7.0.1 - - uses: actions/setup-python@v5 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.13" - name: Install dependencies @@ -25,9 +25,9 @@ jobs: run: | pytest --cov=main --cov-report=xml --cov-report=term-missing main_test.py - name: Upload coverage to Codecov - uses: codecov/codecov-action@v7 + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: files: ./coverage.xml flags: unittests name: codecov-umbrella - fail_ci_if_error: true + fail_ci_if_error: ${{ github.event_name == 'push' }}