Skip to content

feat: render structured output for step log, job summary, and PR comments - #251

Merged
shenxianpeng merged 18 commits into
mainfrom
feature/structured-action-output
Aug 5, 2026
Merged

feat: render structured output for step log, job summary, and PR comments#251
shenxianpeng merged 18 commits into
mainfrom
feature/structured-action-output

Conversation

@shenxianpeng

@shenxianpeng shenxianpeng commented Aug 4, 2026

Copy link
Copy Markdown
Member

What

Rebuilds the action's output pipeline on top of commit-check --format json (core 2.13.0) and renders all three output surfaces from the structured check data instead of scraping plain text and stripping ANSI codes.

Why

Changes

Step log

  • Grouped sections (::group::) per check category (Commit message / Branch / Author)
  • One line per scope (PR title, Commit N/M, Branch, ...) with ✔/✖ and failure count
  • Each failing rule emits a ::error title=CCxxx <check>:: annotation with value / suggest / docs details

Job summary

  • # Commit Check Policy Report header with failure count
  • | Scope | Failed checks | Result | table with Markdown rule links
  • Collapsible <details> with full failure details

PR comment

  • Keeps the # Commit-Check ❌/✔️ prefix so the idempotent update/delete logic is unchanged
  • Same compact table + collapsible details

New output

  • result action output: structured JSON (status + per-scope checks), consumable via fromJSON(steps.<id>.outputs.result) — documented in README

Fix

  • add_pr_comments returned 0 when the existing comment was already up-to-date even on failure; now returns the correct status

Test plan

  • 76 unit tests pass (up from 64), including new renderer and GitHub API interaction coverage
  • End-to-end verified against real commit-check 2.13.0: PR with 3 commits (1 bad), bad PR title, bad branch, and all-pass scenarios
  • pre-commit (black, mypy, codespell) all green

Screenshots

The README screenshots for job summary / PR comments live in commit-check/.github and will need refreshing after this lands — happy to do that in a follow-up if you'd like.

Summary by CodeRabbit

  • New Features

    • Added structured JSON results with overall status and per-scope check details.
    • Checks now consistently cover pull request titles and individual commits.
    • Added readable grouped reports in workflow logs, job summaries, and pull request comments.
    • Added clearer failure summaries with counts and affected checks.
    • Added fallback handling for invalid check output.
    • Added a result output for use in downstream GitHub Actions.
  • Documentation

    • Documented the structured output, available fields, and downstream usage examples.

@shenxianpeng
shenxianpeng requested a review from a team as a code owner August 4, 2026 18:14
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@shenxianpeng, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 4 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 414ffc62-30da-4dc4-abee-b57dc527148a

📥 Commits

Reviewing files that changed from the base of the PR and between 6337ec1 and 30357af.

⛔ Files ignored due to path filters (1)
  • assets/logo.png is excluded by !**/*.png
📒 Files selected for processing (4)
  • README.md
  • action.yml
  • main.py
  • main_test.py
📝 Walkthrough

Walkthrough

The action now executes checks as structured JSON results grouped by scope. It renders these results in logs, job summaries, and pull request comments, publishes them through GITHUB_OUTPUT, and documents the new result output.

Changes

Structured results

Layer / File(s) Summary
Scoped check execution and aggregation
main.py, main_test.py
ScopeResult stores parsed checks, raw fallback output, status, and failures. Checks now run for pull request titles, individual commits, and other scopes.
Result rendering and publication
main.py, main_test.py
The action renders grouped logs and Markdown reports, writes job summaries and JSON outputs, and reports aggregate failures.
Pull request comment ownership
main.py, main_test.py
Comments use hidden markers. The action updates marked comments, deletes stale marked comments, and adopts only bot-authored legacy comments.
Action output contract and validation
action.yml, README.md, main_test.py
The public result output and its JSON fields are documented. Tests cover serialization, rendering, UTF-8 handling, dry-run behavior, and comment ownership.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant main
  participant run_commit_check
  participant render_step_log
  participant add_job_summary
  participant add_pr_comments
  participant GITHUB_OUTPUT
  main->>run_commit_check: execute checks and collect ScopeResult values
  run_commit_check-->>main: return exit code and scoped results
  main->>render_step_log: render grouped results
  main->>add_job_summary: write Markdown report
  main->>add_pr_comments: create or update report comment
  main->>GITHUB_OUTPUT: publish serialized result
Loading

Possibly related PRs

Suggested labels: enhancement

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies structured output rendering for the step log, job summary, and PR comments.
Linked Issues check ✅ Passed The changes satisfy the structured policy report and enhanced error output objectives, including JSON results, grouped failures, reports, comments, and tests [#210, #503].
Out of Scope Changes check ✅ Passed The README, action output, implementation, and tests directly support the linked reporting and error-output objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/structured-action-output

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Commit Check

All 22 checks passed

Show all 22 checks
Commit message
  ✔ PR title (feat: render structured output for step log, job summary,...)
  ✔ Commit 1/18 (feat: render structured output for step log, job summary,...)
  ✔ Commit 2/18 (fix: make stdout UTF-8 for Windows runners)
  ✔ Commit 3/18 (refactor: unify job summary and PR comment rendering)
  ✔ Commit 4/18 (refactor: split report title and status into separate lines)
  ✔ Commit 5/18 (feat: show passed checks in a collapsible section on success)
  ✔ Commit 6/18 (refactor: render passed checks like the step log in the d...)
  ✔ Commit 7/18 (feat: show checked values in the success details block)
  ✔ Commit 8/18 (refactor: truncate checked values at 60 chars in the deta...)
  ✔ Commit 9/18 (feat: show checked values in the failure table)
  ✔ Commit 10/18 (feat: show only failed values in the table and all checks...)
  ✔ Commit 11/18 (feat: add CodSpeed-style pass/fail stats to the report he...)
  ✔ Commit 12/18 (feat: reduce the report table to failed scopes)
  ✔ Commit 13/18 (docs: document the report output spec and pin it with gol...)
  ✔ Commit 14/18 (fix: wire the result output through action.yml)
  ✔ Commit 15/18 (feat: rework the report format and identify comments by a...)
  ✔ Commit 16/18 (fix: count things checked, not rule evaluations)
  ✔ Commit 17/18 (fix: print the failure reason once and keep the log tree ...)
  ✔ Commit 18/18 (fix: show the failing value in full and render one tree, ...)
Branch
  ✔ Branch (feature/structured-action-output)
Author
  ✔ Author name (Xianpeng Shen)
  ✔ Author email (xianpeng.shen@gmail.com)

commit-check 2.13.1 · Rules reference

@shenxianpeng
shenxianpeng force-pushed the feature/structured-action-output branch 2 times, most recently from 6761fa5 to 82f1f1d Compare August 4, 2026 18:21
@shenxianpeng shenxianpeng changed the title feat: structured output for step log, job summary, and PR comments feat: render structured output for step log, job summary, and PR comments Aug 4, 2026
@shenxianpeng
shenxianpeng force-pushed the feature/structured-action-output branch from 4c02c9d to 1ab3be0 Compare August 4, 2026 18:23
…ents

Collect results via commit-check --format json and render all three
output surfaces from the structured data instead of scraping plain text:

- step log: grouped sections with ::error annotations per rule ID
- job summary: policy report table with rule links and collapsible details
- PR comment: compact table with rule links and collapsible details
- new result output exposing structured JSON for downstream jobs
Emoji and check marks (✔ ✖ ❌) cannot be encoded by the default cp1252
codec on Windows, crashing the action. Reconfigure stdout/stderr to
UTF-8, matching commit-check core's _reconfigure_io.
Use a single report renderer for both surfaces with a plain '# Commit
Check' title (dropping 'Policy Report') and identical content on success
and failure. PR comment matching also accepts the old hyphenated title
so existing comments are still updated rather than duplicated.
The report now opens with a plain '# Commit Check' title line followed
by the status line ('✅ All checks passed (N scopes)' or the failure
count), keeping the PR comment and job summary identical.
The success report now includes a '<details>' block listing every check
that passed per scope, so users can confirm which rules were actually
evaluated without leaving the summary or PR comment.
Replace the passed-checks table with a fenced block mirroring the
step log layout (group name followed by indented ✔ scope lines), so the
success report reads the same as the action log.
Each scope line now shows the concrete value that was checked (PR
title, commit subject, branch name, author name/email), truncated to
one line, so the details block reads like the step log.
@shenxianpeng
shenxianpeng force-pushed the feature/structured-action-output branch from c5bd88b to 775fc7f Compare August 4, 2026 23:42
Long commit subjects wrapped inside the fenced details block, misaligning
the scope lines. Trimming the value at 60 characters keeps the full line
(prefix + value + parentheses) short enough to stay on one line.
The scope table now carries a 'Checked value' column (mirroring the
success details), so a failing report answers what exactly was checked
without expanding the failure details.
@shenxianpeng
shenxianpeng force-pushed the feature/structured-action-output branch from 3fa0c58 to 532e527 Compare August 4, 2026 23:54
The report table now fills the checked-value column only for failed
scopes so failures stand out, and the collapsible details block lists
every scope's value in step-log style (✔/✖) with the failure reason and
suggestion under each failing rule, unifying the pass and fail layouts.
The status line now reads '❌ 2 failures · ✅ 4 passed (6 scopes)' on
failure and '✅ 11 passed (11 scopes)' on success, mirroring the compact
emoji-plus-count style of CodSpeed reports so the pass ratio is visible
without scanning the table.
@shenxianpeng
shenxianpeng force-pushed the feature/structured-action-output branch from 79571e9 to bbba5cd Compare August 5, 2026 00:04
Passing scopes no longer pad the failure table with dash rows; the
table lists only the failed scopes with their checked value and rule
links, while the collapsible details block keeps the full pass/fail
picture in step-log style.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
main.py (2)

208-221: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not merge stderr into the JSON stream.

stderr=subprocess.STDOUT sends every stderr line into the same buffer that json.loads parses. Under the previous text-based pipeline this was harmless. Now a single stray stderr line, for example a Python DeprecationWarning, a config-file warning, or git stderr forwarded by commit-check, makes the document invalid. json.loads then raises, check_scope returns ScopeResult(raw_text=raw), and ScopeResult.status reports fail. The action fails the workflow and posts a failure report even when every rule passed.

Capture stderr separately and parse only stdout. Keep stderr for the fallback text so unexpected output is still visible.

🐛 Proposed fix
     result = subprocess.run(
         command,
         input=input_text,
         stdout=subprocess.PIPE,
-        stderr=subprocess.STDOUT,
+        stderr=subprocess.PIPE,
         text=True,
         encoding="utf-8",
         check=False,
     )
-    raw = result.stdout or ""
+    raw = result.stdout or ""
     try:
         return result.returncode, json.loads(raw), raw
     except json.JSONDecodeError:
-        return result.returncode, None, raw
+        combined = "\n".join(part for part in (raw, result.stderr or "") if part.strip())
+        return result.returncode, None, combined
🤖 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.py` around lines 208 - 221, Update the subprocess.run invocation to
capture stderr separately instead of merging it into stdout, then parse only
stdout as JSON. Preserve stderr alongside stdout in the raw fallback text
returned by this execution flow so unexpected diagnostics remain visible, while
keeping the existing return code and successful JSON behavior unchanged.

671-689: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Authorization Bypass (CWE-284)

Reachability: External

Reachability path
● Entry
  main_test.py:49
  pass_scope
│
▼
● Sink
  main.py

Filter matching comments to ones authored by this action.

matching_comments currently selects any pull request comment whose body starts with # Commit Check or # Commit-Check, with no author check. The later edit() and delete() calls can therefore overwrite or remove comments from another user if the action has issues: write. Restrict these matches to comments authored by the action token identity before performing destructive updates.

🤖 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.py` around lines 671 - 689, Update the matching_comments comprehension
near pull_request.get_comments() to require both the existing title prefix and
that the comment author matches the action token identity. Preserve support for
both REPORT_TITLE and the legacy "# Commit-Check" prefix, and ensure only those
action-authored comments reach the later edit() and delete() calls.
🧹 Nitpick comments (8)
main.py (4)

408-472: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Merge _markdown_details and _markdown_passed_details.

The two functions render the same block. _markdown_details is a strict superset: it adds the failure lines. _markdown_passed_details duplicates the wrapper, the grouping loop, and the ✔ label (value) line. Any future change to the details layout must be applied twice.

Also note lines 420-421: the failure branch computes value and suffix and then discards both, so the checked value is never shown for a failing scope in the details block.

Keep one function and let _markdown_details handle the all-pass case, which it already does correctly.

♻️ Proposed fix
-def _markdown_passed_details(results: list[ScopeResult]) -> str:
-    """Render the collapsible section listing passed checks per scope.
-
-    Mirrors the step log layout (group name followed by indented ✔ scope
-    lines) inside a fenced block so it reads like the action log. Each
-    scope line also shows the concrete value that was checked.
-    """
-    lines = ["<details>", "<summary>Show details</summary>", "", "```text"]
-    for group_name, scopes in _grouped(results):
-        lines.append(group_name)
-        for scope in scopes:
-            value = _scope_value(scope)
-            suffix = f" ({value})" if value else ""
-            lines.append(f"  ✔ {scope.label}{suffix}")
-    lines.extend(["```", "", "</details>"])
-    return "\n".join(lines)

Then in render_report, call _markdown_details(results) on the success path too.

🤖 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.py` around lines 408 - 472, Remove the duplicate
_markdown_passed_details function and use _markdown_details for both success and
failure report rendering. Update render_report’s success path to call
_markdown_details(results), and in _markdown_details preserve the computed value
suffix for failing scopes by including it on the failure label as well.

508-520: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

include_footer has no false caller.

render_job_summary and render_pr_comment both call render_report(results, include_footer=True), so the two wrappers are byte-identical and the parameter is never exercised with False. The tests confirm this by asserting the two bodies are equal. Either drop the parameter or drop one wrapper.

🤖 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.py` around lines 508 - 520, Remove the unused include_footer parameter
from render_report and fold its always-enabled footer behavior into the shared
report implementation, or remove the redundant render_pr_comment wrapper;
preserve the existing identical Markdown output for render_job_summary and
render_pr_comment and update their callers accordingly.

353-360: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The raw-output fallback prints "(0 failures)".

Lines 354-355 compute and print the failure count before the raw_text branch. When the CLI produced unparsable output, scope.failures is empty, so the log line reads ✖ Branch (0 failures). That text contradicts the ✖ marker. The unit test at main_test.py line 627 asserts this string, so update it together with the code.

♻️ Proposed fix
-            failures = scope.failures
-            count = f" ({len(failures)} failure{'s' if len(failures) != 1 else ''})"
-            print(f"  \u2716 {scope.label}{count}")
             if scope.raw_text and not scope.checks:
                 # Defensive fallback: commit-check produced unexpected output.
+                print(f"  \u2716 {scope.label} (unexpected output)")
                 for line in scope.raw_text.strip().splitlines():
                     print(f"    {line}")
                 continue
+            failures = scope.failures
+            count = f" ({len(failures)} failure{'s' if len(failures) != 1 else ''})"
+            print(f"  \u2716 {scope.label}{count}")
🤖 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.py` around lines 353 - 360, Update the failure-reporting flow around
scope.failures so the raw_text fallback does not print a “(0 failures)” count
when scope.checks is empty; emit the ✖ label and raw output directly for
unparsable results while preserving normal failure counts. Update the
corresponding assertion in the relevant main_test.py test to expect the revised
output.

74-84: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Read status defensively in ScopeResult.

status and failures index c["status"] directly. The check dicts come from external CLI JSON. If a future commit-check release renames or omits that key, both properties raise KeyError and the action fails with a traceback instead of a report. The renderers already use .get() for every other field.

♻️ Proposed fix
     `@property`
     def status(self) -> str:
         """Overall status: ``pass`` when every check passed."""
         if self.raw_text and not self.checks:
             return "fail"
-        return "fail" if any(c["status"] == "fail" for c in self.checks) else "pass"
+        return "fail" if any(c.get("status") == "fail" for c in self.checks) else "pass"
 
     `@property`
     def failures(self) -> list[dict[str, str]]:
         """The checks that failed in this scope."""
-        return [c for c in self.checks if c["status"] == "fail"]
+        return [c for c in self.checks if c.get("status") == "fail"]
🤖 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.py` around lines 74 - 84, Update the ScopeResult.status and
ScopeResult.failures properties to read each check’s status defensively with the
existing default-safe dictionary access pattern, treating missing or renamed
status fields as non-failing rather than raising KeyError. Preserve the current
raw-text/no-check failure behavior and failure filtering for checks explicitly
marked "fail".
main_test.py (4)

512-512: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Rename the unused unpacked results.

Ruff reports RUF059 at lines 512, 530, 544, and 561. Each test unpacks results and asserts only on rc. Rename the binding to _results at these four sites, or replace it with rc, _ = main.run_commit_check().

Also applies to: 530-530, 544-544, 561-561

🤖 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` at line 512, Update the four test call sites of
main.run_commit_check at the referenced locations to avoid the unused results
binding: unpack the second return value as _results or discard it with _, while
preserving each test’s existing rc assertions.

Source: Linters/SAST tools


780-783: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Replace the hardcoded /tmp path.

Ruff reports S108 at line 782. The neighbouring tests already use tempfile.mkdtemp(). This PR adds Windows-runner support, and /tmp is not a valid path on Windows. The value is never opened here, so the test passes today, but keep it portable and consistent.

♻️ Proposed fix
     def test_no_output_env_is_noop(self):
         with patch.dict(os.environ, {}, clear=True):
-            os.environ["GITHUB_STEP_SUMMARY"] = "/tmp/step_summary.txt"
+            os.environ["GITHUB_STEP_SUMMARY"] = os.path.join(
+                tempfile.mkdtemp(), "step_summary.txt"
+            )
             main.set_result_output([pass_scope()])  # should not raise
🤖 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 780 - 783, Replace the hardcoded
“/tmp/step_summary.txt” assignment in test_no_output_env_is_noop with a path
built from tempfile.mkdtemp(), matching the neighboring tests while preserving
the existing no-op assertion.

Source: Linters/SAST tools


245-251: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for stderr contamination of the JSON stream.

run_check_json currently merges stderr into stdout, so a warning line printed by commit-check makes the JSON unparsable and turns a passing scope into a failure. See the comment on main.py lines 208-221. No test covers that case. Add one that emits a warning line plus valid JSON and asserts the parse still succeeds.

Do you want me to generate the test?

🤖 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 245 - 251, Add a test alongside
test_invalid_json_returns_none_with_raw_output that mocks subprocess.run with a
warning on stderr and valid JSON on stdout, then calls
run_check_json(["--branch"]) and asserts the return code succeeds and the JSON
data parses correctly. Configure the mock and assertions to verify stderr does
not contaminate the JSON stream.

828-844: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The github MagicMock cannot satisfy the except GithubException clause.

github_module is a plain MagicMock, so main.add_pr_comments imports GithubException as a MagicMock attribute rather than an exception class. Python rejects a non-BaseException class in an except clause. These tests pass only because the happy path raises nothing. If the code under test ever raises inside the try, the test fails with TypeError: catching classes that do not inherit from BaseException is not allowed and hides the real error. The 403 and generic-error branches at lines 695-708 of main.py are therefore not testable with this fixture.

Give the fake module a real exception class.

♻️ Proposed fix
         github_module = MagicMock()
+
+        class FakeGithubException(Exception):
+            def __init__(self, status=500, data=None):
+                super().__init__(status)
+                self.status = status
+                self.data = data or {}
+
+        github_module.GithubException = FakeGithubException
         github_module.Github.return_value.get_repo.return_value = mock_repo

Extract the fixture into a helper so all four PR-comment tests share it.

🤖 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 828 - 844, Update the shared GitHub mock fixture
used by the PR-comment tests around add_pr_comments to provide a real
GithubException class inheriting from BaseException instead of relying on the
default MagicMock attribute. Extract the module setup into a reusable helper and
use it across all four PR-comment tests so the 403 and generic-error exception
branches are exercised without TypeError.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@action.yml`:
- Around line 40-43: Add id: run-commit-check to the composite action step that
writes result to GITHUB_OUTPUT, then map outputs.result.value to
steps.run-commit-check.outputs.result in the action outputs definition. Keep the
existing result description unchanged so consumers can parse the emitted JSON.

In `@main.py`:
- Around line 540-541: Update the summary write in the job-summary flow around
render_job_summary to append a trailing newline after the rendered body,
ensuring subsequent GITHUB_STEP_SUMMARY appends start on a new line. Keep the
existing render_job_summary output unchanged and preserve the separate fork-PR
handling.
- Around line 395-404: Update the Markdown row construction in the results loop
to escape every pipe character in interpolated cell values, including
scope.label, the value rendered by _scope_value, and any other dynamically
generated cell text before insertion. Preserve the existing pass/fail formatting
and use the escaped values in both table-row branches.
- Around line 228-231: Update the ScopeResult construction in the run_check_json
handling to derive status from both the CLI return code and the top-level
response status. For dictionary responses, pass the failure condition rc != 0 or
data.get("status") == "fail" into ScopeResult.status while preserving the
existing checks extraction; keep raw_text handling for non-dictionary responses
unchanged.

---

Outside diff comments:
In `@main.py`:
- Around line 208-221: Update the subprocess.run invocation to capture stderr
separately instead of merging it into stdout, then parse only stdout as JSON.
Preserve stderr alongside stdout in the raw fallback text returned by this
execution flow so unexpected diagnostics remain visible, while keeping the
existing return code and successful JSON behavior unchanged.
- Around line 671-689: Update the matching_comments comprehension near
pull_request.get_comments() to require both the existing title prefix and that
the comment author matches the action token identity. Preserve support for both
REPORT_TITLE and the legacy "# Commit-Check" prefix, and ensure only those
action-authored comments reach the later edit() and delete() calls.

---

Nitpick comments:
In `@main_test.py`:
- Line 512: Update the four test call sites of main.run_commit_check at the
referenced locations to avoid the unused results binding: unpack the second
return value as _results or discard it with _, while preserving each test’s
existing rc assertions.
- Around line 780-783: Replace the hardcoded “/tmp/step_summary.txt” assignment
in test_no_output_env_is_noop with a path built from tempfile.mkdtemp(),
matching the neighboring tests while preserving the existing no-op assertion.
- Around line 245-251: Add a test alongside
test_invalid_json_returns_none_with_raw_output that mocks subprocess.run with a
warning on stderr and valid JSON on stdout, then calls
run_check_json(["--branch"]) and asserts the return code succeeds and the JSON
data parses correctly. Configure the mock and assertions to verify stderr does
not contaminate the JSON stream.
- Around line 828-844: Update the shared GitHub mock fixture used by the
PR-comment tests around add_pr_comments to provide a real GithubException class
inheriting from BaseException instead of relying on the default MagicMock
attribute. Extract the module setup into a reusable helper and use it across all
four PR-comment tests so the 403 and generic-error exception branches are
exercised without TypeError.

In `@main.py`:
- Around line 408-472: Remove the duplicate _markdown_passed_details function
and use _markdown_details for both success and failure report rendering. Update
render_report’s success path to call _markdown_details(results), and in
_markdown_details preserve the computed value suffix for failing scopes by
including it on the failure label as well.
- Around line 508-520: Remove the unused include_footer parameter from
render_report and fold its always-enabled footer behavior into the shared report
implementation, or remove the redundant render_pr_comment wrapper; preserve the
existing identical Markdown output for render_job_summary and render_pr_comment
and update their callers accordingly.
- Around line 353-360: Update the failure-reporting flow around scope.failures
so the raw_text fallback does not print a “(0 failures)” count when scope.checks
is empty; emit the ✖ label and raw output directly for unparsable results while
preserving normal failure counts. Update the corresponding assertion in the
relevant main_test.py test to expect the revised output.
- Around line 74-84: Update the ScopeResult.status and ScopeResult.failures
properties to read each check’s status defensively with the existing
default-safe dictionary access pattern, treating missing or renamed status
fields as non-failing rather than raising KeyError. Preserve the current
raw-text/no-check failure behavior and failure filtering for checks explicitly
marked "fail".
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 406882df-99ec-43fd-b525-eb78597ddebb

📥 Commits

Reviewing files that changed from the base of the PR and between 6337ec1 and 79571e9.

📒 Files selected for processing (4)
  • README.md
  • action.yml
  • main.py
  • main_test.py

Comment thread action.yml
Comment thread main.py
Comment on lines +228 to +231
_rc, data, raw = run_check_json(args, input_text=input_text)
if isinstance(data, dict):
return ScopeResult(label=label, checks=data.get("checks", []))
return ScopeResult(label=label, raw_text=raw)

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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

commit-check --format json output schema status checks fields

💡 Result:

When running the commit-check tool with the --format json option, the output provides a structured, machine-readable format designed for automation and integration with AI agents [1][2][3]. The output retains the original CLI exit codes (0 for pass, 1 for fail) [1][3]. The JSON output follows this schema [1][2][3]: { "status": "pass" | "fail", "checks": [ { "check": "", "status": "pass" | "fail", "value": "", "error": "", "suggest": "" } ] } Key fields in the schema include: - status: Represents the overall result of the validation ("pass" or "fail") [1][3]. - checks: A list of individual rule results [1][3]. - check: The name of the specific rule being validated (e.g., "message", "subject_imperative") [1][3]. - status: The pass/fail status of that specific check [1][3]. - value: The input value that was checked [1][3]. - error: A description of the error if the check failed [1][3]. - suggest: A suggestion or guidance on how to resolve the failure, which is particularly useful for automated self-correction by AI agents [1][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Files matching main.py:\n'
fd -a '^main\.py$' . | sed 's#^\./##'

printf '\nRelevant definitions/usages:\n'
rg -n "class ScopeResult|def check_scope|def run_check_json|ScopeResult\\(|failed|status|checks" -S .

printf '\nmain.py outline:\n'
ast-grep outline main.py --view compact || true

printf '\nmain.py relevant sections:\n'
sed -n '1,280p' main.py | cat -n

Repository: commit-check/commit-check-action

Length of output: 21687


🌐 Web query:

commit-check GitHub docs v2.13.1 JSON output schema status checks

💡 Result:

The commit-check tool provides structured machine-readable JSON output to support automation, CI/CD, and AI agents [1][2]. By passing the --format json flag to any CLI invocation, the tool returns a standardized JSON schema representing the status of individual checks [1][2]. The returned JSON schema maintains a consistent structure across CLI, API, and MCP integrations [3][4]. Its schema is defined as follows: { "status": "pass" | "fail", "checks": [ { "check": "", "status": "pass" | "fail", "value": "", "error": "", "suggest": "" } ] } In the context of GitHub Actions, the commit-check-action displays these results within the GitHub Actions job summary by default [5]. While the action facilitates the enforcement of these checks on pull requests and pushes, the underlying JSON output schema is a feature of the core commit-check engine [1][6]. Additionally, the configuration file for commit-check (cchk.toml or commit-check.toml) has its own schema published on SchemaStore, allowing for IDE autocompletion and validation in environments like VS Code, PyCharm, and IntelliJ [7][4]. This ensures that the policy definitions driving the status checks are validated against a structured schema at the configuration level [7][8].

Citations:


🌐 Web query:

site:commit-check.github.io/commit-check --format json "status" "checks"

💡 Result:

The --format json flag is a command-line option used to change the output of status checks from human-readable text (typically ASCII art) to a structured, machine-readable JSON format [1][2][3]. This flag is primarily designed for integration into automated environments such as CI/CD pipelines, tooling, and AI agent workflows [1][2]. Key characteristics include: 1. Consistency: The exit code of the process remains unchanged (0 for pass, 1 for fail), ensuring that existing scripts and pipelines continue to function without modification [2][3]. 2. Machine-Readable Structure: The output provides a JSON object containing the overall status and an array of individual check results [2][3]. 3. Diagnostic Detail: In the event of a failure, the JSON output includes specific fields—such as error messages and suggestions—which allow external tools or agents to parse the failure and potentially self-correct [2][3]. This mode is typically used as an alternative to default terminal output when verbose banners or ASCII art are unnecessary or problematic for downstream processing [2][4].

Citations:


🌐 Web query:

site:github.com/commit-check/commit-check "format json" "checks" "status"

💡 Result:

In the context of the commit-check tool, passing the --format json flag to command-line invocations returns machine-readable JSON output instead of standard human-readable text [1]. This structured output includes a status field indicating whether the overall checks passed or failed (status: "pass" or "fail"), as well as a list of detailed check results, each containing its own status and any relevant error or suggestion information [1]. This functionality is designed to maintain original exit codes (0 for pass, 1 for fail), allowing existing CI/CD scripts to remain compatible while benefiting from richer, structured data for automated processing or audit logging [1][2].

Citations:


🌐 Web query:

"commit-check" 2.13.1 github raw "status" "checks"

💡 Result:

Commit Check (commit-check) is a lightweight, versioned policy engine designed to validate Git commit metadata, including commit messages, branch names, author identity, signoff trailers, AI attribution, and push safety [1][2][3]. Regarding your query about version 2.13.1 and raw status checks: 1. Version Status: As of August 2026, the latest available versions on PyPI include 2.13.0 [1]. Version 2.13.1 may not be released or widely indexed yet. 2. Raw Status Checks: Commit Check does not natively output "raw status checks" in the sense of a proprietary binary format; instead, it provides machine-readable output designed for automation and integration [3][4]. Specifically: - The tool offers a Python API (commit_check.api) that allows AI agents, CI pipelines, and scripts to perform validations without spawning subprocesses [3][4]. - All API functions return standard Python dictionaries (dicts) that are easily serialized into JSON or other formats, making them ideal for integration into larger workflows or for forwarding to LLMs [3][4]. - When run in CI/GitHub Actions, it supports job summaries and can be configured to provide clear, actionable feedback within the GitHub environment [5]. The tool relies on a single TOML configuration file (cchk.toml or commit-check.toml) to enforce these policies consistently across local pre-commit hooks, CI pipelines, and GitHub Actions [1][2][3]. If no configuration file is provided, it defaults to checking compliance with the Conventional Commits and Conventional Branch specifications [3][4].

Citations:


Use the CLI exit code and top-level status when building ScopeResult.

_rc is dropped, and a top-level status is ignored. ScopeResult.status only checks checks, so a commit-check run returning status: "fail" plus an empty checks array would be treated as pass and report 0 for a failed policy check. Include rc != 0 or data.get("status") == "fail" in the result.

🤖 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.py` around lines 228 - 231, Update the ScopeResult construction in the
run_check_json handling to derive status from both the CLI return code and the
top-level response status. For dictionary responses, pass the failure condition
rc != 0 or data.get("status") == "fail" into ScopeResult.status while preserving
the existing checks extraction; keep raw_text handling for non-dictionary
responses unchanged.

Comment thread main.py Outdated
Comment on lines +395 to +404
for scope in results:
if scope.status == "pass":
rows.append(f"| {scope.label} | \u2014 | \u2014 | \u2705 |")
else:
value = _scope_value(scope)
value_display = f"`{value}`" if value else "\u2014"
links = " \u00b7 ".join(
_rule_markdown_link(check) for check in scope.failures
)
rows.append(f"| {scope.label} | {value_display} | {links} | \u274c |")

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

Escape pipe characters in table cells.

scope.label and value are interpolated straight into Markdown table rows. value comes from the commit message, branch name, or author field. A commit subject that contains |, for example feat: support a | b, adds extra cells and corrupts the whole table in both the job summary and the PR comment. Backticks do not neutralize | in GitHub Markdown tables; only \| does.

Escape | in every interpolated cell value.

🐛 Proposed fix
+def _cell(text: str) -> str:
+    """Escape a value so it cannot break out of a Markdown table cell."""
+    return text.replace("|", "\\|")
+
+
 def _markdown_table(results: list[ScopeResult]) -> str:
     for scope in results:
         if scope.status == "pass":
-            rows.append(f"| {scope.label} | \u2014 | \u2014 | \u2705 |")
+            rows.append(f"| {_cell(scope.label)} | \u2014 | \u2014 | \u2705 |")
         else:
             value = _scope_value(scope)
-            value_display = f"`{value}`" if value else "\u2014"
+            value_display = f"`{_cell(value)}`" if value else "\u2014"
             links = " \u00b7 ".join(
                 _rule_markdown_link(check) for check in scope.failures
             )
-            rows.append(f"| {scope.label} | {value_display} | {links} | \u274c |")
+            rows.append(
+                f"| {_cell(scope.label)} | {value_display} | {links} | \u274c |"
+            )
📝 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
for scope in results:
if scope.status == "pass":
rows.append(f"| {scope.label} | \u2014 | \u2014 | \u2705 |")
else:
value = _scope_value(scope)
value_display = f"`{value}`" if value else "\u2014"
links = " \u00b7 ".join(
_rule_markdown_link(check) for check in scope.failures
)
rows.append(f"| {scope.label} | {value_display} | {links} | \u274c |")
for scope in results:
if scope.status == "pass":
rows.append(f"| {_cell(scope.label)} | \u2014 | \u2014 | \u2705 |")
else:
value = _scope_value(scope)
value_display = f"`{_cell(value)}`" if value else "\u2014"
links = " \u00b7 ".join(
_rule_markdown_link(check) for check in scope.failures
)
rows.append(
f"| {_cell(scope.label)} | {value_display} | {links} | \u274c |"
)
🤖 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.py` around lines 395 - 404, Update the Markdown row construction in the
results loop to escape every pipe character in interpolated cell values,
including scope.label, the value rendered by _scope_value, and any other
dynamically generated cell text before insertion. Preserve the existing
pass/fail formatting and use the escaped values in both table-row branches.

Comment thread main.py
Comment on lines 540 to +541
with open(GITHUB_STEP_SUMMARY, "a", encoding="utf-8") as summary_file:
summary_file.write(build_result_body(result_text))
summary_file.write(render_job_summary(results))

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

Append a trailing newline to the job summary.

render_job_summary returns a body with no trailing newline, and the write appends it verbatim. The report ends with the footer line _Rules reference: .... Any later append to GITHUB_STEP_SUMMARY, by this action or a subsequent step, is concatenated onto that footer line and both fragments render as one paragraph. The fork-PR branch at line 643 works around this by prefixing its own \n.

🐛 Proposed fix
     with open(GITHUB_STEP_SUMMARY, "a", encoding="utf-8") as summary_file:
-        summary_file.write(render_job_summary(results))
+        summary_file.write(render_job_summary(results) + "\n")
📝 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
with open(GITHUB_STEP_SUMMARY, "a", encoding="utf-8") as summary_file:
summary_file.write(build_result_body(result_text))
summary_file.write(render_job_summary(results))
with open(GITHUB_STEP_SUMMARY, "a", encoding="utf-8") as summary_file:
summary_file.write(render_job_summary(results) + "\n")
🤖 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.py` around lines 540 - 541, Update the summary write in the job-summary
flow around render_job_summary to append a trailing newline after the rendered
body, ensuring subsequent GITHUB_STEP_SUMMARY appends start on a new line. Keep
the existing render_job_summary output unchanged and preserve the separate
fork-PR handling.

shenxianpeng and others added 2 commits August 5, 2026 03:10
Add an output specification block above render_report showing the exact
success and failure layouts, and golden tests that assert the full
rendered report byte-for-byte so the spec stays accurate as the output
evolves.
The output was declared with only a description, and the step that produces it
had no id. Composite actions do not forward step outputs automatically, so
steps.<id>.outputs.result resolved to the empty string and the fromJSON call in
the README documentation would have failed the calling workflow. The Python
side was already writing the payload to $GITHUB_OUTPUT correctly; the mapping
above it was missing.

The unit tests could not catch this — they exercise set_result_output, not the
action.yml plumbing around it.

Also notes in the README that a downstream step reading the result needs
dry-run or continue-on-error, since a failing check otherwise ends the job
before that step runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U9zFxq8V4qxG4aMzJhGBFn
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
main_test.py (1)

896-907: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Patch GITHUB_EVENT_NAME in the fork test.

add_pr_comments calls is_fork_pr_with_readonly_token, which returns is_fork_pr() and os.getenv("GITHUB_EVENT_NAME", "") != "pull_request_target". This test patches only main.is_fork_pr, so it depends on the ambient GITHUB_EVENT_NAME. If the workflow that runs the tests is triggered by pull_request_target, the guard returns False and the test fails. test_fork_pr_writes_job_summary_hint at Lines 909-924 has the same dependency.

🛡️ Proposed fix
         with (
             patch("main.PR_COMMENTS_ENABLED", True),
             patch("main.is_fork_pr", return_value=True),
+            patch.dict(os.environ, {"GITHUB_EVENT_NAME": "pull_request"}),
             patch("main.JOB_SUMMARY_ENABLED", False),
             patch("builtins.print") as mock_print,
         ):
🤖 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 896 - 907, Update
test_fork_pr_skips_comment_and_warns and test_fork_pr_writes_job_summary_hint to
patch main.os.getenv or main.GITHUB_EVENT_NAME so the simulated fork uses an
event name other than pull_request_target, making both tests independent of the
ambient workflow environment.
main.py (1)

232-246: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Capture stderr separately so diagnostics do not corrupt the JSON.

stderr=subprocess.STDOUT merges stderr into the stream that is parsed as JSON. Any warning the CLI or Python writes to stderr (deprecation notice, config warning) prepends text to the payload, json.loads then raises, and the scope falls back to raw_text. ScopeResult.status reports fail for that scope, so a passing run fails the job. Use stderr=subprocess.PIPE and keep the stderr text only for the fallback message.

The Ruff RUF005 hint on Line 232 is addressed in the same diff.

🐛 Proposed fix
-    command = ["commit-check", "--format", "json"] + args
+    command = ["commit-check", "--format", "json", *args]
     result = subprocess.run(
         command,
         input=input_text,
         stdout=subprocess.PIPE,
-        stderr=subprocess.STDOUT,
+        stderr=subprocess.PIPE,
         text=True,
         encoding="utf-8",
         check=False,
     )
     raw = result.stdout or ""
     try:
         return result.returncode, json.loads(raw), raw
     except json.JSONDecodeError:
-        return result.returncode, None, raw
+        return result.returncode, None, (raw + (result.stderr or "")).strip()
🤖 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.py` around lines 232 - 246, Update the subprocess.run call in the
commit-check execution flow to capture stderr separately with subprocess.PIPE
instead of merging it into stdout. Parse only result.stdout as JSON, and include
the captured stderr text only when constructing the raw fallback message while
preserving the existing return structure.

Source: Linters/SAST tools

🧹 Nitpick comments (3)
main_test.py (3)

515-524: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Silence the unused unpacked results values.

Ruff reports RUF059 for the unpacked results at Lines 520, 538, 552, and 569. These tests assert only on rc and on the mocks. Prefix the unused name with an underscore so the lint gate stays clean.

♻️ Proposed change (apply at each site)
-            rc, results = main.run_commit_check()
+            rc, _results = main.run_commit_check()
🤖 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 515 - 524, Update each run_commit_check()
unpacking in the affected tests to bind the unused results value to a name
prefixed with an underscore, while preserving the rc assertions and mock
assertions unchanged.

Source: Linters/SAST tools


227-259: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unused as mock_run binding.

test_parses_json_output binds mock_run at Line 230 but never asserts on it. test_command_includes_format_json already covers the command shape.

♻️ Proposed cleanup
-        with patch("main.subprocess.run", return_value=mock_result) as mock_run:
+        with patch("main.subprocess.run", return_value=mock_result):
             rc, data, raw = main.run_check_json(["--branch"])
🤖 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 227 - 259, Remove the unused mock_run binding from
the patch context in TestRunCheckJson.test_parses_json_output, while leaving the
test setup and assertions unchanged; retain the binding in
test_command_includes_format_json because that test asserts on its call
arguments.

832-887: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider replacing the hardcoded /tmp literals.

Ruff reports S108 for /tmp/step_summary.txt at Line 11 and Line 886. The path is only a placeholder for the module-level default, so the risk is low. Use tempfile.gettempdir() or a shared module constant to keep the lint output clean and to avoid duplicating the literal.

🤖 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 832 - 887, The test uses a hardcoded /tmp path in
test_no_output_env_is_noop, triggering Ruff S108. Replace that literal with
tempfile.gettempdir() or reuse an existing shared temporary-directory constant
while preserving the test’s no-output behavior.

Source: Linters/SAST tools

🤖 Prompt for all review comments with 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.

Inline comments:
In `@main.py`:
- Around line 99-109: Update the ScopeResult.status and ScopeResult.failures
accessors to read each parsed check’s status via c.get("status") with the
appropriate default, matching the defensive access pattern used elsewhere in the
file. Preserve the existing pass/fail behavior while preventing missing or
renamed status fields from raising KeyError.
- Around line 822-830: Update the target-handling logic around the up-to-date
early return to delete every comment in stale before returning when target.body
matches pr_comment_body. Preserve the existing status-based return value, and
keep stale deletion in the update path so both paths clean up duplicate marked
comments.

---

Outside diff comments:
In `@main_test.py`:
- Around line 896-907: Update test_fork_pr_skips_comment_and_warns and
test_fork_pr_writes_job_summary_hint to patch main.os.getenv or
main.GITHUB_EVENT_NAME so the simulated fork uses an event name other than
pull_request_target, making both tests independent of the ambient workflow
environment.

In `@main.py`:
- Around line 232-246: Update the subprocess.run call in the commit-check
execution flow to capture stderr separately with subprocess.PIPE instead of
merging it into stdout. Parse only result.stdout as JSON, and include the
captured stderr text only when constructing the raw fallback message while
preserving the existing return structure.

---

Nitpick comments:
In `@main_test.py`:
- Around line 515-524: Update each run_commit_check() unpacking in the affected
tests to bind the unused results value to a name prefixed with an underscore,
while preserving the rc assertions and mock assertions unchanged.
- Around line 227-259: Remove the unused mock_run binding from the patch context
in TestRunCheckJson.test_parses_json_output, while leaving the test setup and
assertions unchanged; retain the binding in test_command_includes_format_json
because that test asserts on its call arguments.
- Around line 832-887: The test uses a hardcoded /tmp path in
test_no_output_env_is_noop, triggering Ruff S108. Replace that literal with
tempfile.gettempdir() or reuse an existing shared temporary-directory constant
while preserving the test’s no-output behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d25e4a64-7776-4998-8979-631da69e836a

📥 Commits

Reviewing files that changed from the base of the PR and between 6337ec1 and 56e6f50.

⛔ Files ignored due to path filters (1)
  • assets/logo.png is excluded by !**/*.png
📒 Files selected for processing (4)
  • README.md
  • action.yml
  • main.py
  • main_test.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • README.md
  • action.yml

Comment thread main.py
Comment thread main.py
shenxianpeng and others added 3 commits August 5, 2026 08:19
Comment ownership was decided by `body.startswith("# Commit Check")`, and every
match but the newest was deleted. A person opening a comment with that heading
would have had it removed with no trace. Comments now carry a hidden
`<!-- commit-check-action -->` marker, the way Codecov, SonarQube and CodSpeed
identify theirs, and deletion is restricted to comments carrying it. A report
from an earlier version has no marker, so it is adopted in place — but only
when a bot posted it, since the only other signal is the title a person can
type by accident.

The header counted two different things at once: failures were counted in
checks while "passed" and the total counted scopes, so one commit failing two
rules rendered "2 failures · 2 passed (3 scopes)" and the numbers did not
reconcile. Counts are now checks throughout — "2 of 4 checks failed" — and the
details summary repeats the same total so the two can be checked against each
other.

The table dropped its Result column: only failed scopes reach the table, so it
read ❌ on every row. The title moved to h2 with the project logo beside it, an
h1 being louder than anything else in a PR comment, and the footer now names
the commit-check version that produced the result, which is the first thing
worth knowing when an outcome looks wrong.

Smaller things found while reading:

- an ImportError from PyGithub made the `except GithubException` clause raise
  NameError, which propagates past the `except Exception` under it and would
  have killed a step designed to never be fatal
- GITHUB_STEP_SUMMARY was read with os.environ[] at import time, so main.py
  could not be imported outside Actions
- the two details renderers differed only by a branch; the unparsable-output
  fallback now shows in the report instead of rendering an empty scope
- build_result_body had no caller but its own test, and include_footer was
  never passed False
- re and tempfile were imported but unused
- GITHUB_REPOSITORY was passed to get_repo() unchecked, which mypy flags once
  PyGithub types are available

The logo ships as PNG in this repository rather than as the SVG on
commit-check.com: GitHub proxies comment images through camo, which handles SVG
unreliably, and a local asset keeps the report free of a cross-repository
dependency.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U9zFxq8V4qxG4aMzJhGBFn
The header on this pull request read "1 of 100 checks failed". The number was
arithmetically right — sixteen commit messages against six enabled rules, plus
two branch rules and one each for author name and email — but it was the wrong
number to show.

It measured the size of the pull request rather than the strictness of the
policy: the same configuration reports 100 on a fifteen-commit branch and 10 on
a one-commit branch. Ninety-six of the hundred were the same six rules run
again per commit, so the total also overstated the work done. Worst of all, the
inflated denominator made a real failure look negligible: one bad commit out of
fifteen is not "1 of 100".

A check is now one thing that was checked — a commit message, the branch, the
author — so the same report reads "1 of 19 checks failed", and the total is
what the reader can count in the details block. Rule-level detail is unchanged;
it lives in the table and the details, which is where someone goes to find out
what to fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U9zFxq8V4qxG4aMzJhGBFn
The job log showed the same sentence twice in a row under every failure:

    Error: Commit message should use imperative mood (...)
        value: feat: settle the report format ...
        Commit message should use imperative mood (...)

The first line was an ::error workflow command carrying the first line of the
error, the third was the listing printing the whole error underneath it.

The annotation also broke the listing it sat inside. An ::error renders as a
line of its own wherever it appears, so printing one between the ✖ scope line
and its details split the indented tree apart. And its title= — the only place
the rule ID was written — is shown in the annotations UI, never inline, so the
visible line lost the rule ID entirely.

Annotations are now emitted after every group, one per failure, with the scope
in the message since the title cannot carry it. The listing keeps the reason,
the rule ID, the value, the suggestion and the docs link, indented under the
scope that failed. Passing scopes show the value that was checked, which the
Markdown report already did.

Also drops the "(0 failures)" that unparsable output rendered next to its ✖:
there is no check list to count, and zero next to a cross reads as a
contradiction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U9zFxq8V4qxG4aMzJhGBFn
@shenxianpeng
shenxianpeng force-pushed the feature/structured-action-output branch from 3f3da36 to 46b4737 Compare August 5, 2026 08:19
The details block gave every passing scope its checked value but replaced it
with the failure count on the one scope that failed, so the offending commit
subject appeared nowhere in the report except the table, truncated at sixty
characters. The value a reader has to act on was the only one they could not
read. It now prints in full underneath the failing scope, where the step log
already showed it.

The two surfaces also disagreed about layout despite a docstring claiming one
mirrored the other — four spaces of indent in the Markdown, six and eight in
the log, and the rule name folded into the reason on one side but not the
other. They now come from a single renderer, with the docs URL as the only
difference: the Markdown report already links the rule ID from the table above.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U9zFxq8V4qxG4aMzJhGBFn
@shenxianpeng shenxianpeng added the enhancement New feature or request label Aug 5, 2026
@shenxianpeng
shenxianpeng merged commit ce3ddb1 into main Aug 5, 2026
9 checks passed
@shenxianpeng
shenxianpeng deleted the feature/structured-action-output branch August 5, 2026 09:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Enhance error output

1 participant