Skip to content

ci: consolidate pmd, checkstyle, spotbugs into one static-analysis job #3219

ci: consolidate pmd, checkstyle, spotbugs into one static-analysis job

ci: consolidate pmd, checkstyle, spotbugs into one static-analysis job #3219

Workflow file for this run

name: verify
on:
push:
branches: [master]
pull_request:
jobs:
static-analysis:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with:
distribution: 'temurin'
java-version: '21'
- name: Set up Workspace Environment Variable
run: echo "WORKSPACE=${{ github.workspace }}" >> $GITHUB_ENV
- name: Cache Maven dependencies
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: /home/runner/.m2/repository
key: ${{ runner.os }}-maven-0-${{ hashFiles('**/pom.xml') }}
- name: Compile + generate PMD/Checkstyle reports
# Compile first so PMD has an auxClasspath for type-resolving rules.
# PMD/Checkstyle are fast; run their reports first, fail-fast on their checks
# before paying for the slower SpotBugs analysis.
run: |
mvn -f ./ddk-parent/pom.xml --batch-mode --fail-at-end \
compile \
pmd:pmd pmd:cpd \
checkstyle:checkstyle
- name: Annotate PMD/Checkstyle violations
# Parse PMD + Checkstyle XML reports and emit GitHub workflow-command annotations
# so each violation appears inline in the PR's Files-changed view. Runs even when
# the next step fails.
if: always()
run: |
python3 - <<'EOF'
import os
from xml.etree import ElementTree as ET
from pathlib import Path
root = Path(os.environ.get('GITHUB_WORKSPACE', '.'))
def annot(level, file, line, title, msg):
msg = (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}::{msg}")
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'
annot(level, f.attrib['name'], v.attrib.get('beginline', '1'),
f"PMD/{v.attrib.get('rule','')}", v.text)
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'
annot(level, f.attrib['name'], e.attrib.get('line', '1'),
f"Checkstyle/{e.attrib.get('source','').split('.')[-1]}",
e.attrib.get('message',''))
EOF
- name: Enforce PMD/Checkstyle thresholds
# Fail-fast here. If PMD or Checkstyle violates a threshold, SpotBugs (slow) is
# skipped entirely — devs get fast feedback and re-push after fixing.
run: |
mvn -f ./ddk-parent/pom.xml --batch-mode --fail-at-end \
pmd:check pmd:cpd-check \
checkstyle:check
- name: Generate SpotBugs report
run: |
mvn -f ./ddk-parent/pom.xml --batch-mode --fail-at-end \
spotbugs:spotbugs
- name: Annotate SpotBugs violations
if: always()
run: |
python3 - <<'EOF'
import os
from xml.etree import ElementTree as ET
from pathlib import Path
root = Path(os.environ.get('GITHUB_WORKSPACE', '.'))
def annot(level, file, line, title, msg):
msg = (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}::{msg}")
for xml in root.rglob('target/spotbugsXml.xml'):
for b in ET.parse(xml).getroot().findall('BugInstance'):
sl = b.find('SourceLine')
lm = b.find('LongMessage')
if sl is not None and lm is not None:
annot('warning', sl.attrib.get('sourcepath', '?'),
sl.attrib.get('start', '1'),
f"SpotBugs/{b.attrib.get('type','')}", lm.text)
EOF
- name: Enforce SpotBugs threshold
run: |
mvn -f ./ddk-parent/pom.xml --batch-mode --fail-at-end \
spotbugs:check
line-endings:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Check LF line endings in index
# .gitattributes declares `* text=auto eol=lf` with .bat/.cmd/.ps1
# exempted. Git's clean filter normalizes on commit, but verify it
# explicitly in case a file is miscategorized as binary or a filter
# is bypassed.
run: |
violations=$(git ls-files --eol \
| grep -E "^i/(crlf|mixed)" \
| grep -vE "\.(bat|cmd|ps1)$" || true)
if [ -n "$violations" ]; then
echo "Files with CRLF/mixed line endings stored in the index:"
echo "$violations"
exit 1
fi
maven-verify:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Set up JDK 21
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with:
distribution: 'temurin'
java-version: '21'
- name: Log Maven version
run: mvn --version
- name: Set up Workspace Environment Variable
run: echo "WORKSPACE=${{ github.workspace }}" >> $GITHUB_ENV
- name: Cache Maven dependencies
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
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: 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()
run: |
if ! find . -path '*/target/surefire-reports/TEST-*.xml' -print -quit | grep -q .; then
echo "::error::No surefire reports found. Test discovery is likely broken."
exit 1
fi
- name: Archive Tycho Surefire Plugin
if: ${{ failure() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: tycho-surefire-plugin
path: ${{ env.GITHUB_WORKSPACE }}/com.avaloq.tools.ddk.xtext.test/target/work/data/.metadata/.log