Skip to content

CLP-179 Add ruling-diff-comment GitHub Action for cross-analyzer use#31

Merged
romainbrenguier merged 10 commits into
masterfrom
romain/ruling-diff-action
Jul 10, 2026
Merged

CLP-179 Add ruling-diff-comment GitHub Action for cross-analyzer use#31
romainbrenguier merged 10 commits into
masterfrom
romain/ruling-diff-action

Conversation

@romainbrenguier

@romainbrenguier romainbrenguier commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Part of Core Languages & Parsers shared tooling

Summary

This PR adds a reusable GitHub Action (ruling-diff-comment) that analyzes changes in ruling JSON files and posts human-readable summaries on pull requests. This helps reviewers understand the impact of code changes on static analysis results.

The action is now analyzer-agnostic and can be used across Python, Java, C#, and other analyzers.

Features

  • ✅ Detects changed ruling JSON files between base and head commits
  • ✅ Compares before/after versions to identify issue differences
  • ✅ Generates code snippets showing where issues appear or were fixed
  • ✅ Posts or updates PR comments with formatted summaries
  • ✅ Supports both Python and Java Maven ruling key formats
  • ✅ Configurable paths for different analyzer structures
  • ✅ Comprehensive unit tests (44 tests)
  • ✅ CI integration to ensure action stays functional

Configuration

The action accepts optional parameters for different analyzers:

Python (default):

uses: SonarSource/core-languages-tooling-public/ruling-diff-comment@master
with:
  pr-number: ${{ github.event.pull_request.number }}
  repository: ${{ github.repository }}
  base-sha: ${{ github.event.pull_request.base.sha }}
  head-sha: ${{ github.event.pull_request.head.sha }}

Java:

uses: SonarSource/core-languages-tooling-public/ruling-diff-comment@master
with:
  pr-number: ${{ github.event.pull_request.number }}
  repository: ${{ github.repository }}
  base-sha: ${{ github.event.pull_request.base.sha }}
  head-sha: ${{ github.event.pull_request.head.sha }}
  ruling-root: 'its/ruling/src/test/resources'
  sources-root: 'its/sources'

Ruling Key Format Support

Automatically handles different ruling JSON key formats:

Python (2-part):

{"airflow:airflow/cli/cli_parser.py": [42]}

Java Maven (3-part):

{
  "commons-beanutils:commons-beanutils:src/main/java/...": [42],
  "org.eclipse.jetty:jetty-project:jetty-http/src/main/...": [42]
}

Testing

Tested on sonar-java: SonarSource/sonar-java#5761 (comment)

The action successfully:

  • Detected changed ruling files in Java format
  • Parsed Java Maven coordinates correctly
  • Resolved source files and generated snippets
  • Posted formatted PR comments

Commits

  1. Add ruling-diff-comment GitHub Action - Initial implementation with comprehensive tests and documentation
  2. Add unit tests to CI - Ensures action stays functional on every change
  3. Make action configurable - Add ruling-root and sources-root parameters for different analyzers
  4. Fix Java Maven coordinate handling - Support 3-part groupId:artifactId:path format with unit tests

Migration Path

For analyzer repositories (Python, Java, C#, etc.):

  1. Add workflow file referencing this action
  2. Set ruling-root and sources-root parameters for your analyzer
  3. Configure trigger paths to match your ruling file locations

See ruling-diff-comment/example-workflow.yml for a complete reference implementation.


@rombirli - Please review when you have a chance!


Summary by Gitar

  • Refactored testing infrastructure:
    • Removed obsolete workspace fallback test from test_ruling_diff.py.
  • Resolved technical issues:
    • Fixed strip_project_key to correctly process Java Maven coordinates by handling 3-part formats.
    • Initialized GitHubActionIO with mocked paths in unit tests to prevent failures.
    • Fixed SonarQube issues in core logic to ensure cleaner code quality.
    • Updated FakeRulingDiffIO to include mandatory ruling_root and sources_root parameters.

This will update automatically on new commits.

@romainbrenguier romainbrenguier requested a review from rombirli July 8, 2026 15:23
@hashicorp-vault-sonar-prod hashicorp-vault-sonar-prod Bot changed the title Add ruling-diff-comment GitHub Action for cross-analyzer use CLP-179 Add ruling-diff-comment GitHub Action for cross-analyzer use Jul 8, 2026
@hashicorp-vault-sonar-prod

hashicorp-vault-sonar-prod Bot commented Jul 8, 2026

Copy link
Copy Markdown

CLP-179

Comment thread ruling-diff-comment/ruling_diff_core_lib/comment_rendering.py Outdated
Comment thread ruling-diff-comment/ruling_diff_core_lib/ruling_diff_logic.py
Comment thread ruling-diff-comment/ruling_diff_io.py
Comment thread .github/workflows/build.yml Fixed
Comment thread ruling-update-and-notify/action.yml Outdated
Comment thread ruling-update-and-notify/action.yml Outdated
Comment thread ruling-update-and-notify/action.yml Outdated
Comment thread ruling-update-and-notify/action.yml Outdated
@romainbrenguier romainbrenguier force-pushed the romain/ruling-diff-action branch from 1d1b285 to 761a922 Compare July 9, 2026 10:44
romainbrenguier added a commit that referenced this pull request Jul 9, 2026
Changed ruling-diff-comment reference from @romain/ruling-diff-action to
@romain/ruling-sync-PR to match the branch where both actions live.

This keeps PR #31 focused on just ruling-diff-comment, while this branch
includes both ruling-diff-comment and ruling-update-and-notify.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

@rombirli rombirli 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.

LGTM, as long as it works

Comment thread ruling-diff-comment/ruling_diff_io.py Outdated
Comment on lines +293 to +299
def load_text_with_workspace_fallback(path: str, ref: str) -> str | None:
workspace_content = load_workspace_text(path)
if workspace_content is None:
logging.warning("Source file '%s' not found at %s", path, ref)
return None
logging.warning("Source file '%s' not found at %s, using workspace copy", path, ref)
return workspace_content

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Source snippets can show the wrong code

When the action can't load a source file at base_sha / head_sha (common if the sources submodule isn't initialized or the clone is shallow), it falls back to the file on disk — i.e. the current checkout (PR HEAD).

For removed issues, snippets should show code at base_sha, but they may show HEAD instead. The PR comment won't say that; it's only a warning in the Actions log.

Suggestion: drop the workspace fallback and show "(source file not found at this revision)" when git show {ref}:path fails.

ruling_diff_io.pyload_text_with_workspace_fallback()

romainbrenguier and others added 9 commits July 10, 2026 14:14
This action analyzes changes in ruling JSON files and posts human-readable
summaries on pull requests, helping reviewers understand the impact of code
changes on static analysis results.

Features:
- Detects changed ruling JSON files between base and head commits
- Compares before/after versions to identify issue differences
- Generates code snippets showing where issues appear or were fixed
- Posts or updates PR comments with formatted summaries
- Includes comprehensive unit tests and documentation

The action is designed to be analyzer-agnostic and can be used by any
analyzer repository (Python, Java, C#, etc.) by referencing:
  uses: SonarSource/core-languages-tooling-public/ruling-diff-comment@master

Configuration may need minor adjustments in models_and_constants.py for
different ruling directory structures.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

Remove unit test step from action execution

Unit tests should run during action development/CI, not on every
invocation. This reduces overhead and execution time for users.

Tests can still be run locally with:
  cd ruling-diff-comment && uv run python -m unittest discover
Run the action's unit tests as part of the build workflow to ensure
the action remains functional as changes are made.
Allows the action to work with different analyzer directory structures by
accepting ruling-root and sources-root as input parameters.

Default values preserve backward compatibility with Python analyzer:
- ruling-root: 'private/its-enterprise/ruling/src/test/resources/expected_ruling'
- sources-root: 'private/its-enterprise/sources_ruling'

For Java analyzer, use:
- ruling-root: 'its/ruling/src/test/resources'
- sources-root: 'its/sources'

Changes:
- Add ruling-root and sources-root inputs to action.yml
- Pass parameters through ruling_diff.py CLI arguments
- Update GitHubActionIO to accept and use these parameters
- Update all file loading functions to respect custom paths
- Add ruling_root and sources_root to RulingDiffIO protocol
- Update example workflow with Java configuration example

Document ruling-root and sources-root inputs in README

Add the optional configuration parameters to the Inputs table with their
default values, and show example usage in the workflow snippet.
Java ruling JSON keys use Maven coordinate format:
  groupId:artifactId:path

Examples:
- commons-beanutils:commons-beanutils:src/main/java/... → src/main/java/...
- org.eclipse.jetty:jetty-project:jetty-http/src/main/... → jetty-http/src/main/...

Python uses simpler format:
- airflow:airflow/cli/cli_parser.py → airflow/cli/cli_parser.py

Updated strip_project_key to:
- Detect 3-part format (Java) and return everything after 2nd colon
- Detect 2-part format (Python) and return everything after 1st colon
- Handle edge cases (no colons)

This fixes source file resolution for Java analyzers where files were not
found due to incorrect path extraction from ruling keys.

Add unit tests for strip_project_key function

Tests cover:
- Python format (2-part): project:path
- Java Maven format (3-part): groupId:artifactId:path
- Java Maven with modules: org.eclipse.jetty:jetty-project:jetty-http/src/...
- Edge cases: no colons, simple filenames

All 6 new tests pass. Total test count: 38 → 44 tests.
- Use full commit SHA for astral-sh/setup-uv action (security)
- Use logging.exception() instead of logging.error() to include traceback
The FakeRulingDiffIO test mock was missing the ruling_root and sources_root
attributes that were added to the RulingDiffIO protocol when we made the
action configurable.

Fixes CI test failures:
- test_build_rule_diffs_caches_source_loads_per_ref_and_path
- test_build_rule_diffs_missing_source_produces_placeholder_snippet
- test_build_rule_diffs_uses_io_object_and_respects_refs
1. Fix syntax highlighting for different languages
   - Detect language from file extension (.py, .java, .cs, etc.)
   - Apply appropriate markdown fence language
   - Supports Python, Java, C#, JavaScript, TypeScript, C/C++, Go, Ruby, PHP, Kotlin, Scala

2. Handle non-conforming ruling paths gracefully
   - Catch ValueError from parse_ruling_path and log warning
   - Skip malformed files instead of aborting entire action
   - Allows mixed directory layouts across analyzers

3. Tighten git error detection
   - Remove overly broad 'path' marker from _is_missing_at_ref
   - Prevents real git errors from being misclassified as missing files
   - Keep specific markers: 'exists on disk, but not in', 'does not exist in'
… paths

The tests were instantiating GitHubActionIO before applying patches to the
module-level constants, causing the instance to use default paths. This led
to test failures because:
1. Default parameter values are evaluated at function definition time
2. The instance's sources_root was set to the unpatched hardcoded constant

Also removed one test that relied on non-existent filesystem structure.

Fixes:
- Move GitHubActionIO() instantiation inside patch context
- Pass sources_root explicitly to constructor
- Remove test_resolve_source_path_for_project_rulings_falls_back_to_sources_child

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Drop the workspace fallback when source files cannot be found at a specific
git ref. Previously, when git show failed to retrieve a file at base_sha or
head_sha, the code would fall back to reading the current workspace file,
which could show incorrect code (e.g., PR HEAD code for removed issues that
should show base_sha code).

Now the code returns None and logs a warning when source files are not found,
allowing the PR comment to indicate that the source is unavailable at that
revision instead of showing misleading code snippets.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@romainbrenguier romainbrenguier force-pushed the romain/ruling-diff-action branch from 761a922 to fe3be9a Compare July 10, 2026 12:16
The test_load_text_at_ref_falls_back_to_workspace_copy_with_warning test
was checking for behavior that was intentionally removed in commit fe3be9a.
The workspace fallback feature no longer exists, and the new behavior
(returning None with a warning when sources are missing) is already covered
by test_load_text_at_ref_warns_when_source_missing.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@romainbrenguier romainbrenguier merged commit a1938e2 into master Jul 10, 2026
4 checks passed
@romainbrenguier romainbrenguier deleted the romain/ruling-diff-action branch July 10, 2026 12:28
@gitar-bot

gitar-bot Bot commented Jul 10, 2026

Copy link
Copy Markdown
Code Review ✅ Approved 7 resolved / 7 findings

Adds a reusable analyzer-agnostic GitHub Action to post ruling-diff summaries, addressing multiple issues including script injection risks, hardcoded language fences, and git-related failure handling. All previous findings have been resolved.

✅ 7 resolved
Quality: Snippet code fence hardcoded to ```python for all analyzers

📄 ruling-diff-comment/ruling_diff_core_lib/comment_rendering.py:97-98
format_snippet_block in comment_rendering.py always emits a ```python fenced block (line 98), but the PR's headline feature is making the action analyzer-agnostic (Java, C#, etc.). Java/C# snippets will be rendered with Python syntax highlighting in the PR comment, which is misleading. Consider deriving the fence language from the source file extension (e.g. .java -> `java`, `.cs` -> `csharp`, `.py` -> `python`) and passing it through the Snippet, defaulting to no language when unknown.

Edge Case: One non-conforming ruling path aborts the entire comment

📄 ruling-diff-comment/ruling_diff_core_lib/ruling_diff_logic.py:19-33 📄 ruling-diff-comment/ruling_diff_core_lib/ruling_diff_logic.py:144-151 📄 ruling-diff-comment/ruling_diff.py:82-86
build_rule_diff_for_file calls parse_ruling_path, which raises ValueError for any changed .json file that does not match the strict <project>/<repo>-<rule>.json (exactly 2 relative parts) shape. That exception propagates through build_rule_diffs -> main, where the top-level handler in ruling_diff.py logs and sys.exit(1). As a result, a single unexpected ruling file (deeper nesting, a non-ruling JSON accidentally under the ruling root, or a differently-structured layout in another analyzer) causes the whole action to fail and post no comment, even for the files that were parseable. Since the action is now used across analyzers with differing layouts, consider skipping (log a warning and continue) files that fail to parse rather than aborting the entire run.

Bug: _is_missing_at_ref may swallow genuine git errors

📄 ruling-diff-comment/ruling_diff_io.py:162-166 📄 ruling-diff-comment/ruling_diff_io.py:169-179
_is_missing_at_ref treats stderr as a 'file missing at ref' signal if it contains any of the markers, including the very broad substring "path '". Many unrelated git failures include path '...' in their message, so a real error (e.g. an unexpected git failure while reading an existing file) could be silently reclassified as 'file absent', returning None and producing a misleading '(source file not found)' snippet or an incorrect new/deleted-file classification (is_new_file/is_deleted_file in RuleDiff derive from these None results). Consider tightening the match to the specific git phrases (does not exist in, exists on disk, but not in) and dropping the bare path ' marker.

Security: Script injection via untrusted refs/inputs in run blocks

📄 ruling-update-and-notify/action.yml:66-80 📄 ruling-update-and-notify/action.yml:138-140 📄 ruling-update-and-notify/action.yml:190-193 📄 ruling-update-and-notify/action.yml:283
In ruling-update-and-notify/action.yml many values are interpolated directly into shell: bash run: blocks using ${{ ... }}, including attacker-controllable data. Line 79 does TARGET_REF="${{ github.head_ref || github.ref_name }}"github.head_ref is the PR source branch name, which an external contributor fully controls and which may contain shell metacharacters. Because the expression is expanded by the Actions runner into the script before bash executes it, a branch name such as $(malicious) or ";curl … # results in arbitrary command execution in a job that has contents: write, pull-requests: write and GH_TOKEN. The same pattern applies to sync-command (line 140) and other interpolated inputs (base-ref, target-ref, pr-number, fix-pr-branch-prefix). GitHub's hardening guidance is to never interpolate untrusted ${{ }} values into run scripts; instead pass them through env: and reference "$VAR". Recommend binding all these values to env: entries and using the shell variables in the script body.

Bug: git stash pop fails when sync produced no working-tree changes

📄 ruling-update-and-notify/action.yml:207-221
In the 'Create fix PR' step (action.yml ~lines 208-249) the script runs git stash push -m ... -- "${{ inputs.ruling-root }}/" and later unconditionally git stash pop. git stash push does NOT create a stash entry when there are no local changes (it prints 'No local changes to save' and exits 0). In that case the subsequent git stash pop fails with 'No stash entries found', aborting the step with a non-zero exit. This step runs whenever has-differences == 'true' and ruling-failed == 'true', but has-differences is derived from the base↔head commit diff (see below), not from the actual synced working-tree changes, so it is entirely possible to reach git stash pop with an empty stash. Guard the pop, e.g. only pop if a stash was actually created (git stash list | grep -q ruling-sync-changes) or capture the result of git stash push and branch on it.

...and 2 more resolved from earlier reviews

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants