Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/commit-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +19 to 21

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== workflow files =="
git ls-files '.github/workflows/*.yml' '.github/workflows/*.yaml' || true

echo "== relevant checkout lines =="
sed -n '1,120p' .github/workflows/commit-check.yml 2>/dev/null || true
printf '\n--- coverage ---\n'
sed -n '1,140p' .github/workflows/coverage.yml 2>/dev/null || true

echo "== persist-credentials occurrences =="
rg -n "persist-credentials|actions/checkout|pull-requests: write|permissions:" .github/workflows || true

Repository: commit-check/commit-check-action

Length of output: 3120


🌐 Web query:

actions/checkout persist-credentials default true README

💡 Result:

In the actions/checkout GitHub Action, the persist-credentials input defaults to true [1][2]. When set to true, the action configures the authentication token or SSH key with the local Git configuration [1][2]. This enables your workflow steps to run authenticated Git commands, such as git fetch or git push, without needing to manually handle credentials [1][2][3]. Security and Usage Details: - Removal: The credentials are removed during the post-job cleanup process [1][2]. - Improved Storage: As of version v6.0.0, the action was updated to store these credentials in a separate file under $RUNNER_TEMP instead of directly in the.git/config file, improving security [3][4]. - Opt-out: You can disable this behavior by setting persist-credentials: false in your workflow step [1][2].

Citations:


Disable persisted checkout credentials in both workflows.

Both workflow steps leave the GitHub token as the default configured Git credentials for later repository-controlled steps. Set persist-credentials: false unless auth is explicitly required by subsequent steps.

  • .github/workflows/commit-check.yml#L19-L21: add persist-credentials: false; this job also has pull-requests: write.
  • .github/workflows/coverage.yml#L16-L19: add persist-credentials: false.
🧰 Tools
🪛 zizmor (1.28.0)

[warning] 19-21: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

📍 Affects 2 files
  • .github/workflows/commit-check.yml#L19-L21 (this comment)
  • .github/workflows/coverage.yml#L16-L19
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/commit-check.yml around lines 19 - 21, Disable persisted
checkout credentials by adding persist-credentials: false to the
actions/checkout step in .github/workflows/commit-check.yml at lines 19-21 and
.github/workflows/coverage.yml at lines 16-19; no other workflow behavior should
change.

Source: Linters/SAST tools

- uses: ./ # self test
Expand Down
33 changes: 33 additions & 0 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Coverage

on:
push:
branches: [main]
pull_request:
branches: [main]

permissions:
contents: read

jobs:
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # 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@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
files: ./coverage.xml
flags: unittests
name: codecov-umbrella
fail_ci_if_error: ${{ github.event_name == 'push' }}
6 changes: 3 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
venv/
.venv/
.coverage
.coverage.*
*.pyc
__pycache__/
Comment on lines +1 to 4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Ignore the generated XML coverage report.

The workflow creates coverage.xml at line 26, but this ignore file does not exclude it, so local coverage runs can leave an untracked generated artifact.

Proposed fix
 .coverage
 .coverage.*
+coverage.xml
 *.pyc
 __pycache__/
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.coverage
.coverage.*
*.pyc
__pycache__/
.coverage
.coverage.*
coverage.xml
*.pyc
__pycache__/
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.gitignore around lines 1 - 4, Add coverage.xml to the coverage-related
entries in .gitignore so the generated XML report is excluded from version
control alongside the existing coverage artifacts.

result.txt
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
176 changes: 176 additions & 0 deletions main_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment on lines +796 to +807

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make the missing-token test prove the intended branch.

Mocking every os.getenv lookup and checking only rc == 0 allows unrelated failures to satisfy this test. Set GITHUB_TOKEN to an empty value while preserving GITHUB_REPOSITORY, then assert the output mentions GITHUB_TOKEN.

Proposed test adjustment
-            patch.dict(os.environ, {"GITHUB_REPOSITORY": "test/repo"}),
-            patch("os.getenv", return_value=None),
-            patch("builtins.print"),
+            patch.dict(
+                os.environ,
+                {"GITHUB_TOKEN": "", "GITHUB_REPOSITORY": "test/repo"},
+            ),
+            patch("builtins.print") as mock_print,
...
         self.assertEqual(rc, 0)
+        error_text = mock_print.call_args_list[-1][0][0]
+        self.assertIn("GITHUB_TOKEN", error_text)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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_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_TOKEN": "", "GITHUB_REPOSITORY": "test/repo"},
),
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("GITHUB_TOKEN", error_text)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@main_test.py` around lines 796 - 807, Update test_missing_token_returns_zero
to preserve GITHUB_REPOSITORY while setting GITHUB_TOKEN to an empty value
instead of mocking every os.getenv call. Capture the mocked builtins.print
output and assert it mentions GITHUB_TOKEN, while retaining the rc == 0
assertion to verify the intended missing-token branch.


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)
Comment on lines +853 to +868

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the generic exception context.

The test should verify that "unexpected error" is included in the printed message; otherwise it only proves that some generic error prefix was emitted.

Proposed assertion
         self.assertIn("Error posting PR comment", error_text)
+        self.assertIn("unexpected error", error_text)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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)
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)
self.assertIn("unexpected error", error_text)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@main_test.py` around lines 853 - 868, Update
test_generic_exception_returns_zero to assert that the captured final print
message includes the ValueError detail "unexpected error" in addition to the
existing "Error posting PR comment" context. Keep the current return-code and
exception setup unchanged.



class TestIsForkPrWithReadonlyToken(unittest.TestCase):
def test_fork_pr_with_pull_request_event(self):
Expand Down
Loading