Skip to content
Merged
19 changes: 17 additions & 2 deletions .github/workflows/test-update-rule-metadata.yml
Original file line number Diff line number Diff line change
Expand Up @@ -263,10 +263,23 @@ jobs:
echo "================================"
echo "✓ Integration test completed"

generate-summary-tests:
name: Test Generate Summary Logic
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Run generate-summary unit tests
run: |
bash update-rule-metadata/test-generate-summary.sh \
update-rule-metadata/generate-summary.sh

validation-summary:
name: Test Summary
runs-on: ubuntu-latest
needs: [input-parameter-tests, branch-parameter-tests, output-validation, integration-tests, vault-and-env-tests]
needs: [input-parameter-tests, branch-parameter-tests, output-validation, integration-tests, vault-and-env-tests, generate-summary-tests]
if: always()

steps:
Expand All @@ -280,12 +293,14 @@ jobs:
echo "Output Validation: ${{ needs.output-validation.result }}"
echo "Integration Tests: ${{ needs.integration-tests.result }}"
echo "Vault & Env Variable Tests: ${{ needs.vault-and-env-tests.result }}"
echo "Generate Summary Tests: ${{ needs.generate-summary-tests.result }}"
echo "================================"

if [[ "${{ needs.input-parameter-tests.result }}" == "success" && \
"${{ needs.branch-parameter-tests.result }}" == "success" && \
"${{ needs.output-validation.result }}" == "success" && \
"${{ needs.vault-and-env-tests.result }}" == "success" ]]; then
"${{ needs.vault-and-env-tests.result }}" == "success" && \
"${{ needs.generate-summary-tests.result }}" == "success" ]]; then
echo "✓ All validation tests passed!"
echo "✓ Action is properly configured and ready to use"
else
Expand Down
2 changes: 1 addition & 1 deletion update-rule-metadata/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ This action depends on:
| Output | Description |
|--------------------|-----------------------------------------------------------------------------------------------------------|
| `has-changes` | Boolean indicating whether any rule metadata changes were detected (from check-changes step) |
| `summary` | Summary of the rule metadata updates including rule counts for each language (from generate-summary step) |
| `summary` | Summary of the rule metadata updates including rules-to-update and rules-updated counts for each language (from generate-summary step) |
| `pull-request-url` | URL of the created pull request (only available if changes were detected, from create-pr step) |

## Usage
Expand Down
53 changes: 4 additions & 49 deletions update-rule-metadata/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -160,14 +160,7 @@ runs:
echo "Processing directory: $dir"
cd "$dir"

# Extract a meaningful name for logging (use last part of path)
dir_name=$(basename "$dir")
parent_dir=$(dirname "$dir")
if [ "$parent_dir" != "." ]; then
dir_name="${parent_dir##*/}/${dir_name}"
fi

echo "=== $dir_name/sonarpedia.json ===" >> $log_file
echo "=== PATH:$dir ===" >> $log_file

# Calculate relative path to rule-api.jar from current directory
rel_path=$(realpath --relative-to="$PWD" "$original_dir/rule-api.jar")
Expand Down Expand Up @@ -203,48 +196,10 @@ runs:
- name: Generate summary
id: generate-summary
shell: bash
env:
RULE_API_VERSION: ${{ steps.download.outputs.rule-api-version }}
run: |
summary_file="rule-api-summary.md"
current_sonarpedia=""
has_entries=false
total_rules=0

# Build a markdown table
echo "| Sonarpedia | Rules to update |" > "$summary_file"
echo "|---|---:|" >> "$summary_file"

while IFS= read -r line; do
if [[ $line == "=== "* ]]; then
current_sonarpedia=$(echo "$line" | sed 's/=== \(.*\) ===/\1/')
elif [[ $line == *"Found "* && $line == *" rule(s) to update"* ]]; then
rule_count=$(echo "$line" | grep -o 'Found [0-9]\+' | grep -o '[0-9]\+')
if [[ -n "$rule_count" && "$rule_count" != "0" && -n "$current_sonarpedia" ]]; then
echo "| \`${current_sonarpedia}\` | ${rule_count} |" >> "$summary_file"
total_rules=$((total_rules + rule_count))
has_entries=true
fi
fi
done < rule-api-logs.txt

if [[ "$has_entries" == "true" ]]; then
echo "| **Total** | **${total_rules}** |" >> "$summary_file"
fi

echo -e "\nRule API Version: ${{ steps.download.outputs.rule-api-version }}" >> "$summary_file"

# Write summary to output using delimiter to preserve newlines
if [[ "$has_entries" == "false" ]]; then
echo "summary=Update rule metadata" >> $GITHUB_OUTPUT
else
{
echo "summary<<EOF"
cat "$summary_file"
echo "EOF"
} >> $GITHUB_OUTPUT
fi

rm rule-api-logs.txt
rm -f "$summary_file"
bash "$GITHUB_ACTION_PATH/generate-summary.sh" "$RULE_API_VERSION"

- name: Check Rule Metadata Changes
id: check-changes
Expand Down
69 changes: 69 additions & 0 deletions update-rule-metadata/generate-summary.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env bash
# Generate the rule-update summary markdown table from rule-api-logs.txt.
# Usage: generate-summary.sh <rule-api-version> [log-file]
# rule-api-version Version string appended at the end of the summary.
# log-file Path to the log file (default: rule-api-logs.txt).
#
# Reads the log file, builds a markdown table with one row per sonarpedia
# directory, and writes the result to GITHUB_OUTPUT (summary=...).
# Exits 0 in all cases; sets summary to "Update rule metadata" when there
# are no rules to report.

set -euo pipefail

rule_api_version="${1:-}"
log_file="${2:-rule-api-logs.txt}"

summary_file="rule-api-summary.md"
current_sonarpedia=""
current_dir=""
has_entries=false
total_rules=0
total_updated=0

echo "| Sonarpedia | Rules to update | Rules updated |" > "$summary_file"
echo "|---|---:|---:|" >> "$summary_file"

while IFS= read -r line; do
if [[ $line == "=== PATH:"* ]]; then
current_dir=$(echo "$line" | sed 's/=== PATH:\(.*\) ===/\1/')
current_sonarpedia="${current_dir}/sonarpedia.json"
elif [[ $line == *"Found "* && $line == *" rule(s) to update"* ]]; then
rule_count=$(echo "$line" | grep -oE 'Found [0-9]+' | grep -oE '[0-9]+')
if [[ -n "$rule_count" && "$rule_count" != "0" && -n "$current_sonarpedia" ]]; then
# Count distinct rule IDs changed under this sonarpedia directory.
# Each rule has an .html and a .json file; count unique basenames.
updated_count=$(git diff --name-only HEAD -- "${current_dir}" \
| grep -v 'sonarpedia\.json$' \
| grep -E '\.(html|json)$' \
| sed 's/\.[^.]*$//' \
| sort -u \
| wc -l \
| tr -d ' ' || true)
echo "| \`${current_sonarpedia}\` | ${rule_count} | ${updated_count} |" >> "$summary_file"
total_rules=$((total_rules + rule_count))
total_updated=$((total_updated + updated_count))
has_entries=true
fi
fi
done < "$log_file"

if [[ "$has_entries" == "true" ]]; then
echo "| **Total** | **${total_rules}** | **${total_updated}** |" >> "$summary_file"
fi

if [[ -n "$rule_api_version" ]]; then
echo -e "\nRule API Version: ${rule_api_version}" >> "$summary_file"
fi

if [[ "$has_entries" == "false" ]]; then
echo "summary=Update rule metadata" >> "${GITHUB_OUTPUT:-/dev/null}"
else
{
echo "summary<<EOF"
cat "$summary_file"
echo "EOF"
} >> "${GITHUB_OUTPUT:-/dev/null}"
fi

rm -f "$log_file" "$summary_file"
194 changes: 194 additions & 0 deletions update-rule-metadata/test-generate-summary.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
#!/usr/bin/env bash
# Unit tests for generate-summary.sh.
# Usage: test-generate-summary.sh <path-to-generate-summary.sh>
# Exits 0 if all tests pass, 1 on first failure.

set -euo pipefail

_script_arg="${1:-$(dirname "$0")/generate-summary.sh}"
SCRIPT="$(cd "$(dirname "$_script_arg")" && pwd)/$(basename "$_script_arg")"

pass=0
fail=0

INITIAL_COMMIT_MSG="initial"
OLD_RULE_CONTENT='<rule>old</rule>'
NEW_RULE_CONTENT='<rule>new</rule>'
UPDATED_JSON='{"updated":true}'

assert_output_contains() {
local test_name="$1"
local pattern="$2"
local output_file="$3"
if grep -q "$pattern" "$output_file"; then
echo "✓ $test_name"
pass=$((pass + 1))
else
echo "✗ $test_name"
echo " Expected pattern: $pattern"
echo " Actual output:"
sed 's/^/ /' "$output_file"
fail=$((fail + 1))
fi
return 0
}

# ---------------------------------------------------------------------------
# Helper: create an isolated git repo, commit initial files, stage changes,
# write a log file, run the script, and return the output file path.
# ---------------------------------------------------------------------------
run_test() {
local work_dir
work_dir=$(mktemp -d)

# Each test receives the work_dir; caller populates it before calling run_script.
echo "$work_dir"
return 0
}

init_git() {
git init -q
git config user.email "test@test.com"
git config user.name "Test"
return 0
}

run_script() {
local work_dir="$1"
local gh_output="$work_dir/gh_output"
(cd "$work_dir" && GITHUB_OUTPUT="$gh_output" bash "$SCRIPT" "2.99.0") || true
echo "$gh_output"
return 0
}

# ---------------------------------------------------------------------------
# Test 1: flat structure (sonar-rpg style)
# sonarpedia.json at the repo root — mirrors SonarSource/sonar-rpg.
# ---------------------------------------------------------------------------
echo "--- Test 1: flat structure (sonar-rpg style) ---"
WORK_DIR=$(run_test)
cd "$WORK_DIR"
init_git
mkdir -p rules
echo '{}' > sonarpedia.json
echo "$OLD_RULE_CONTENT" > rules/S1000.html
echo '{}' > rules/S1000.json
git add . && git commit -q -m "$INITIAL_COMMIT_MSG"

echo "$NEW_RULE_CONTENT" > rules/S1000.html
echo "$UPDATED_JSON" > rules/S1001.json
git add .

printf '=== PATH:. ===\nRunning rule-api update\nFound 3 rule(s) to update\n' > rule-api-logs.txt

OUTPUT=$(run_script "$WORK_DIR")
assert_output_contains "flat: row shows 3 to update and 2 updated" \
'./sonarpedia.json.*3.*2' "$OUTPUT"
rm -rf "$WORK_DIR"

# ---------------------------------------------------------------------------
# Test 2: 2-level structure (sonar-security frontend/<lang> style)
# ---------------------------------------------------------------------------
echo "--- Test 2: 2-level structure (sonar-security frontend/java style) ---"
WORK_DIR=$(run_test)
cd "$WORK_DIR"
init_git
mkdir -p frontend/java/rules frontend/python/rules
echo '{}' > frontend/java/sonarpedia.json
echo '{}' > frontend/python/sonarpedia.json
echo "$OLD_RULE_CONTENT" > frontend/java/rules/S1000.html
echo '{}' > frontend/java/rules/S1000.json
echo "$OLD_RULE_CONTENT" > frontend/python/rules/S2000.html
git add . && git commit -q -m "$INITIAL_COMMIT_MSG"

# Update java rules only; python unchanged.
echo "$NEW_RULE_CONTENT" > frontend/java/rules/S1000.html
echo "$UPDATED_JSON" > frontend/java/rules/S1001.json
git add .

printf '=== PATH:frontend/java ===\nRunning rule-api update\nFound 5 rule(s) to update\n=== PATH:frontend/python ===\nRunning rule-api update\nFound 2 rule(s) to update\n' > rule-api-logs.txt

OUTPUT=$(run_script "$WORK_DIR")
assert_output_contains "2-level: java row shows 5 to update and 2 updated" \
'frontend/java/sonarpedia.json.*5.*2' "$OUTPUT"
assert_output_contains "2-level: python row shows 2 to update and 0 updated" \
'frontend/python/sonarpedia.json.*2.*0' "$OUTPUT"
rm -rf "$WORK_DIR"

# ---------------------------------------------------------------------------
# Test 3: deep 3-level structure (sonar-security dotnet style)
# frontend/dotnet/<plugin>/sonarpedia.json — the path the old bug truncated.
# ---------------------------------------------------------------------------
echo "--- Test 3: deep 3-level structure (sonar-security dotnet style) ---"
WORK_DIR=$(run_test)
cd "$WORK_DIR"
init_git
mkdir -p frontend/dotnet/sonar-security-csharp-frontend-plugin/rules
mkdir -p frontend/dotnet/sonar-security-vbnet-frontend-plugin/rules
echo '{}' > frontend/dotnet/sonar-security-csharp-frontend-plugin/sonarpedia.json
echo '{}' > frontend/dotnet/sonar-security-vbnet-frontend-plugin/sonarpedia.json
echo "$OLD_RULE_CONTENT" > frontend/dotnet/sonar-security-csharp-frontend-plugin/rules/S3649.html
echo '{}' > frontend/dotnet/sonar-security-csharp-frontend-plugin/rules/S3649.json
echo "$OLD_RULE_CONTENT" > frontend/dotnet/sonar-security-vbnet-frontend-plugin/rules/S3649.html
git add . && git commit -q -m "$INITIAL_COMMIT_MSG"

echo "$NEW_RULE_CONTENT" > frontend/dotnet/sonar-security-csharp-frontend-plugin/rules/S3649.html
echo "$UPDATED_JSON" > frontend/dotnet/sonar-security-csharp-frontend-plugin/rules/S3650.json
git add .

printf '=== PATH:frontend/dotnet/sonar-security-csharp-frontend-plugin ===\nRunning rule-api update\nFound 4 rule(s) to update\n=== PATH:frontend/dotnet/sonar-security-vbnet-frontend-plugin ===\nRunning rule-api update\nFound 4 rule(s) to update\n' > rule-api-logs.txt

OUTPUT=$(run_script "$WORK_DIR")
assert_output_contains "3-level: csharp row shows 4 to update and 2 updated" \
'sonar-security-csharp-frontend-plugin/sonarpedia.json.*4.*2' "$OUTPUT"
assert_output_contains "3-level: vbnet row shows 4 to update and 0 updated" \
'sonar-security-vbnet-frontend-plugin/sonarpedia.json.*4.*0' "$OUTPUT"
rm -rf "$WORK_DIR"

# ---------------------------------------------------------------------------
# Test 5: deduplication — both .html and .json changed for same rule = 1 rule
# ---------------------------------------------------------------------------
echo "--- Test 5: deduplication (.html + .json same rule counts as 1) ---"
WORK_DIR=$(run_test)
cd "$WORK_DIR"
init_git
mkdir -p rules
echo '{}' > sonarpedia.json
echo "$OLD_RULE_CONTENT" > rules/S1896.html
echo '{}' > rules/S1896.json
git add . && git commit -q -m "$INITIAL_COMMIT_MSG"

echo "$NEW_RULE_CONTENT" > rules/S1896.html
echo "$UPDATED_JSON" > rules/S1896.json
git add .

printf '=== PATH:. ===\nRunning rule-api update\nFound 2 rule(s) to update\n' > rule-api-logs.txt

OUTPUT=$(run_script "$WORK_DIR")
assert_output_contains "dedup: S1896.html+S1896.json counts as 1 rule updated" \
'./sonarpedia.json.*2.*1' "$OUTPUT"
rm -rf "$WORK_DIR"

# ---------------------------------------------------------------------------
# Test 4: no rules to update — fallback summary string
# ---------------------------------------------------------------------------
echo "--- Test 4: no rules to update (fallback summary) ---"
WORK_DIR=$(run_test)
cd "$WORK_DIR"
init_git
echo '{}' > sonarpedia.json
git add . && git commit -q -m "$INITIAL_COMMIT_MSG"

printf '=== PATH:. ===\nRunning rule-api update\nFound 0 rule(s) to update\n' > rule-api-logs.txt

OUTPUT=$(run_script "$WORK_DIR")
assert_output_contains "zero rules: fallback summary written" \
'summary=Update rule metadata' "$OUTPUT"
rm -rf "$WORK_DIR"

# ---------------------------------------------------------------------------
# Results
# ---------------------------------------------------------------------------
echo ""
echo "Results: $pass passed, $fail failed"
[[ "$fail" -eq 0 ]]
Loading