Skip to content

Commit a5645fb

Browse files
committed
ci: eclair: add SARIF summary step
Add scripts/ci/sarif_summary.py and a new 'Summarize SARIF results' workflow step that runs after the scan and artifact upload. The script reads results.sarif and: - Writes a Markdown table to $GITHUB_STEP_SUMMARY, which surfaces on the GitHub Actions job summary page. Rules are sorted by classification (mandatory first, then required, then advisory) and by descending violation count within each group. The header shows the total violation count and the number of distinct rules triggered. - Emits one ::notice:: workflow annotation per triggered rule so each rule ID, classification, description, and count appears in the Actions log viewer. Both outputs are derived from the SARIF tool.driver.rules metadata (fullDescription.text and properties.tags) and the per-result ruleId counts. The script handles multi-run SARIF files and falls back to stdout when $GITHUB_STEP_SUMMARY is not set, making it usable locally for inspection. Assisted-by: Claude:claude-opus-4.6 Signed-off-by: Anas Nashif <anas.nashif@intel.com>
1 parent e8ab982 commit a5645fb

2 files changed

Lines changed: 133 additions & 1 deletion

File tree

.github/workflows/coding_guidelines_full.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Eclair Code Scanning
1+
name: Coding Guidelines Scanning
22
on:
33
push:
44
branches:
@@ -161,6 +161,11 @@ jobs:
161161
DIAGNOSTIC.txt
162162
results_*.sarif
163163
164+
- name: Summarize SARIF results
165+
if: always()
166+
run: |
167+
python3 scripts/ci/sarif_summary.py results.sarif
168+
164169
# disabled for now
165170
# - name: Upload Analysis Results
166171
# if: always()

scripts/ci/sarif_summary.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
#!/usr/bin/env python3
2+
# SPDX-FileCopyrightText: Copyright The Zephyr Project Contributors
3+
# SPDX-License-Identifier: Apache-2.0
4+
"""Generate a GitHub Actions step summary from a SARIF file.
5+
6+
Reads the SARIF file produced by the ECLAIR scan and writes a Markdown
7+
table to $GITHUB_STEP_SUMMARY. Also emits one ::notice:: workflow
8+
annotation per triggered rule so each rule appears in the Actions log
9+
with its violation count and description.
10+
11+
Usage:
12+
python3 scripts/ci/sarif_summary.py [results.sarif]
13+
"""
14+
15+
import argparse
16+
import collections
17+
import json
18+
import os
19+
import sys
20+
21+
22+
def _tag_order(tag):
23+
return {"mandatory": 0, "required": 1, "advisory": 2}.get(tag, 3)
24+
25+
26+
def parse_sarif(path):
27+
"""Return (counts, rule_meta) from *path*.
28+
29+
counts -- Counter mapping ruleId -> total violation count
30+
rule_meta -- dict mapping ruleId -> {desc, tag}
31+
"""
32+
with open(path, encoding="utf-8") as fh:
33+
sarif = json.load(fh)
34+
35+
counts = collections.Counter()
36+
rule_meta = {}
37+
38+
for run in sarif.get("runs", []):
39+
for rule in run.get("tool", {}).get("driver", {}).get("rules", []):
40+
rid = rule.get("id", "")
41+
tags = rule.get("properties", {}).get("tags", [])
42+
# Pick the most restrictive classification tag present.
43+
tag = ""
44+
for candidate in ("mandatory", "required", "advisory"):
45+
if candidate in tags:
46+
tag = candidate
47+
break
48+
rule_meta[rid] = {
49+
"desc": rule.get("fullDescription", {}).get("text", rid),
50+
"tag": tag,
51+
}
52+
53+
for result in run.get("results", []):
54+
rid = result.get("ruleId", "unknown")
55+
counts[rid] += 1
56+
57+
return counts, rule_meta
58+
59+
60+
def write_summary(counts, rule_meta, out):
61+
total = sum(counts.values())
62+
n_rules = len(counts)
63+
64+
out.write("## :mag: ECLAIR MISRA Coding Guidelines Scan\n\n")
65+
out.write(
66+
f"> **Total violations:** {total:,} &nbsp;|&nbsp; "
67+
f"**Rules triggered:** {n_rules}\n\n"
68+
)
69+
70+
# Sort by tag priority then by count (descending within same tag).
71+
sorted_rules = sorted(
72+
counts.items(),
73+
key=lambda kv: (_tag_order(rule_meta.get(kv[0], {}).get("tag", "")), -kv[1]),
74+
)
75+
76+
out.write("| # | Rule ID | Classification | Violations | Description |\n")
77+
out.write("|--:|---------|---------------|----------:|-------------|\n")
78+
for idx, (rid, cnt) in enumerate(sorted_rules, 1):
79+
m = rule_meta.get(rid, {})
80+
tag = m.get("tag", "")
81+
desc = m.get("desc", "")
82+
out.write(f"| {idx} | `{rid}` | {tag} | {cnt:,} | {desc} |\n")
83+
84+
85+
def emit_annotations(counts, rule_meta):
86+
"""Emit one ``::notice::`` annotation per rule for the Actions log."""
87+
for rid, cnt in sorted(counts.items(), key=lambda kv: -kv[1]):
88+
m = rule_meta.get(rid, {})
89+
desc = m.get("desc", rid)
90+
tag = m.get("tag", "")
91+
prefix = f"[{tag}] " if tag else ""
92+
# Colons inside the message must be escaped so the annotation
93+
# parser does not mis-identify them as property separators.
94+
safe_desc = desc.replace("::", " ")
95+
print(f"::notice title={rid}::{prefix}{safe_desc}{cnt} violation(s)")
96+
97+
98+
def main():
99+
ap = argparse.ArgumentParser(description=__doc__)
100+
ap.add_argument(
101+
"sarif",
102+
nargs="?",
103+
default="results.sarif",
104+
help="Path to the SARIF file (default: results.sarif)",
105+
)
106+
ap.add_argument(
107+
"--no-annotations",
108+
action="store_true",
109+
help="Skip emitting ::notice:: workflow command annotations",
110+
)
111+
args = ap.parse_args()
112+
113+
counts, rule_meta = parse_sarif(args.sarif)
114+
115+
summary_file = os.environ.get("GITHUB_STEP_SUMMARY")
116+
if summary_file:
117+
with open(summary_file, "a", encoding="utf-8") as fh:
118+
write_summary(counts, rule_meta, fh)
119+
else:
120+
write_summary(counts, rule_meta, sys.stdout)
121+
122+
if not args.no_annotations:
123+
emit_annotations(counts, rule_meta)
124+
125+
126+
if __name__ == "__main__":
127+
main()

0 commit comments

Comments
 (0)