ci: consolidate pmd, checkstyle, spotbugs into one static-analysis job #3219
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |