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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions .github/scripts/annotate-violations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#!/usr/bin/env python3
"""Annotate PMD, Checkstyle, and SpotBugs violations as GitHub workflow commands.

Walks `target/` reports under GITHUB_WORKSPACE, emits one `::warning` or `::error`
per violation (rendered inline on the PR's Files-changed view), and exits 1 if any
violation was found so the workflow step fails fast.
"""
import argparse
import os
import sys
from pathlib import Path
from xml.etree import ElementTree as ET


def make_emit(root: Path):
count = 0

def emit(level: str, file, line, title: str, msg: str | None) -> None:
nonlocal count
count += 1
text = (msg or '').strip().replace('\n', ' ')[:1000]
try:
rel = Path(file).relative_to(root)
except ValueError:
rel = file
print(f"::{level} file={rel},line={line},title={title}::{text}")

return emit, lambda: count


def parse_pmd(root: Path, emit) -> None:
for xml in root.rglob('target/pmd.xml'):
for f in ET.parse(xml).getroot().findall('file'):
for v in f.findall('violation'):
level = 'error' if v.attrib.get('priority') == '1' else 'warning'
emit(level, f.attrib['name'], v.attrib.get('beginline', '1'),
f"PMD/{v.attrib.get('rule', '')}", v.text)


def parse_checkstyle(root: Path, emit) -> None:
for xml in root.rglob('target/checkstyle-result.xml'):
for f in ET.parse(xml).getroot().findall('file'):
for e in f.findall('error'):
level = 'error' if e.attrib.get('severity') == 'error' else 'warning'
emit(level, f.attrib['name'], e.attrib.get('line', '1'),
f"Checkstyle/{e.attrib.get('source', '').split('.')[-1]}",
e.attrib.get('message', ''))


def parse_spotbugs(root: Path, emit) -> None:
for xml in root.rglob('target/spotbugsXml.xml'):
# SpotBugs sourcepath is package-relative (e.g. "com/avaloq/tools/ddk/Foo.java"),
# not repo-relative — combine with the module's source root so GitHub renders the
# annotation inline on the file in the PR's Files-changed view.
module_dir = xml.parent.parent
for b in ET.parse(xml).getroot().findall('BugInstance'):
sl = b.find('SourceLine')
lm = b.find('LongMessage')
if sl is None or lm is None:
continue
sourcepath = sl.attrib.get('sourcepath', '?')
file_path = None
for src_root in ('src', 'src/main/java', 'src-gen'):
candidate = module_dir / src_root / sourcepath
if candidate.exists():
file_path = candidate
break
if file_path is None:
file_path = module_dir / sourcepath
emit('warning', file_path, sl.attrib.get('start', '1'),
f"SpotBugs/{b.attrib.get('type', '')}", lm.text)


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--pmd', action='store_true', help='annotate PMD violations')
parser.add_argument('--checkstyle', action='store_true', help='annotate Checkstyle violations')
parser.add_argument('--spotbugs', action='store_true', help='annotate SpotBugs violations')
args = parser.parse_args()

if not (args.pmd or args.checkstyle or args.spotbugs):
parser.error('pick at least one of --pmd, --checkstyle, --spotbugs')

root = Path(os.environ.get('GITHUB_WORKSPACE', '.'))
emit, total = make_emit(root)

if args.pmd:
parse_pmd(root, emit)
if args.checkstyle:
parse_checkstyle(root, emit)
if args.spotbugs:
parse_spotbugs(root, emit)

kinds = ' + '.join(k for k, on in (('PMD', args.pmd), ('Checkstyle', args.checkstyle), ('SpotBugs', args.spotbugs)) if on)
print(f"{kinds} violations: {total()}")
return 1 if total() > 0 else 0


if __name__ == '__main__':
sys.exit(main())
66 changes: 48 additions & 18 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
branches: [master]
pull_request:
jobs:
pmd:
static-analysis:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
Expand All @@ -14,20 +14,52 @@ jobs:
java-version: '21'
- name: Set up Workspace Environment Variable
run: echo "WORKSPACE=${{ github.workspace }}" >> $GITHUB_ENV
- name: PMD Check
run: mvn pmd:pmd pmd:cpd pmd:check pmd:cpd-check -f ./ddk-parent/pom.xml --batch-mode --fail-at-end
checkstyle:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
- name: Cache Maven dependencies
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
distribution: 'temurin'
java-version: '21'
- name: Set up Workspace Environment Variable
run: echo "WORKSPACE=${{ github.workspace }}" >> $GITHUB_ENV
- name: Checkstyle Check
run: mvn checkstyle:checkstyle checkstyle:check -f ./ddk-parent/pom.xml --batch-mode --fail-at-end
path: /home/runner/.m2/repository
key: ${{ runner.os }}-maven-0-${{ hashFiles('**/pom.xml') }}
- name: Compile + generate PMD/Checkstyle reports
# Report goals (pmd:pmd, pmd:cpd, checkstyle:checkstyle) never fail the build, so
# every module's XML is produced — no Maven cascade-skip. `compile` runs in the
# same invocation so PMD's type-resolving rules (e.g. InvalidLogMessageFormat) get
# the aux-classpath they need; without it, those rules misfire on idioms like
# SLF4J's trailing-Throwable pattern.
run: |
mvn -T 2C -f ./ddk-parent/pom.xml --batch-mode --fail-at-end \
compile \
pmd:pmd pmd:cpd \
checkstyle:checkstyle
- name: Annotate + count PMD/Checkstyle violations
# Emits one GitHub workflow-command annotation per violation (visible inline in
# PR Files-changed) and exits 1 if any are found — fail-fast that bypasses
# SpotBugs (slow) when PMD/Checkstyle already has issues. Runs even if a previous
# step failed.
if: always()
run: python3 .github/scripts/annotate-violations.py --pmd --checkstyle
- name: Enforce PMD/Checkstyle thresholds (Maven safety-net)
# Redundant in the success case (Python check above already passed). Provides
# official-tool validation in case the Python parser misreads schema. `compile`
# is required in this invocation too — pmd:check re-runs analysis from scratch
# rather than reading the prior pmd.xml, so it needs its own aux-classpath.
run: |
mvn -T 2C -f ./ddk-parent/pom.xml --batch-mode --fail-at-end \
compile \
pmd:check pmd:cpd-check \
checkstyle:check
- name: Generate SpotBugs report
run: |
mvn -T 2C -f ./ddk-parent/pom.xml --batch-mode --fail-at-end \
compile \
spotbugs:spotbugs
- name: Annotate + count SpotBugs violations
if: always()
run: python3 .github/scripts/annotate-violations.py --spotbugs
- name: Enforce SpotBugs threshold (Maven safety-net)
run: |
mvn -T 2C -f ./ddk-parent/pom.xml --batch-mode --fail-at-end \
compile \
spotbugs:check
line-endings:
runs-on: ubuntu-24.04
steps:
Expand Down Expand Up @@ -64,10 +96,8 @@ jobs:
with:
path: /home/runner/.m2/repository
key: ${{ runner.os }}-maven-0-${{ hashFiles('**/pom.xml') }}
- name: Build with Maven within a virtual X Server Environment
# Run pmd:pmd and pmd:cpd first to generate reports for all modules, then run pmd:check and pmd:cpd-check
# This ensures all violations are collected and reported before the build fails
run: xvfb-run mvn clean verify checkstyle:check pmd:pmd pmd:cpd pmd:check pmd:cpd-check spotbugs:check -f ./ddk-parent/pom.xml --batch-mode --fail-at-end
- name: Build with Maven within a virtual X Server Environment
run: xvfb-run mvn clean verify -f ./ddk-parent/pom.xml --batch-mode --fail-at-end
- name: Fail on missing surefire reports
# Tycho-Surefire writes no TEST-*.xml when discovery is empty — fail the job in that case.
if: always()
Expand Down