PlantUML in CI/CD: GitHub Actions auto-render, PR checks, and drift detection
PlantUML diagrams are code, but unlike regular code they have no lint/compile checks. This post covers: auto-render on every push, PR diagram diff comments, drift detection, and full CI pipeline setup.
Why integrate PlantUML into CI Common problems without CI:
.puml syntax errors only discovered when someone opens the file
Code changes but the diagram doesn’t — drift between implementation and documentation
PR reviewers can’t see which diagrams changed without manually opening each file
CI integration solves all three: auto-render on every push, automatic PR comments with SVG artifacts, drift detection.
Minimal GitHub Actions Workflow Create .github/workflows/plantuml.yml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 name: PlantUML Render on: push: branches: [main ] paths: - '**.puml' pull_request: paths: - '**.puml' jobs: render: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Java uses: actions/setup-java@v4 with: distribution: temurin java-version: 17 - name: Cache PlantUML uses: actions/cache@v4 with: path: ~/plantuml.jar key: plantuml-1.2024.2 - name: Download PlantUML if: steps.cache.outputs.cache-hit != 'true' run: | curl -L -o ~/plantuml.jar \ https://github.com/plantuml/plantuml/releases/download/v1.2024.2/plantuml.jar - name: Render all .puml files run: | find . -name "*.puml" -exec java -jar ~/plantuml.jar -tsvg {} \; - name: Upload SVG artifacts uses: actions/upload-artifact@v4 with: name: rendered-diagrams path: '**/*.svg'
Every .puml file change auto-renders, SVG artifacts downloadable from the GitHub Actions run.
PR Checks: Detect Changed Diagrams 1 2 3 4 5 6 7 8 9 10 11 - name: Comment changed diagrams on PR if: github.event_name == 'pull_request' run: | CHANGED_FILES=$(git diff --name-only origin/main...HEAD | grep '\.puml$') echo "Changed .puml files:" echo "$CHANGED_FILES" for file in $CHANGED_FILES; do echo "Rendering: $file" java -jar ~/plantuml.jar -tsvg "$file" done
Reviewers see the rendered SVG directly — no need to clone and render locally.
Auto-generate Diagram Change Report Post a PR comment with the list of changed diagrams:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 - name: Post diagram changes to PR if: github.event_name == 'pull_request' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | CHANGED=$(git diff --name-only origin/main...HEAD | grep '\.puml$') if [ -z "$CHANGED" ]; then echo "No .puml files changed" exit 0 fi COMMENT_BODY="## PlantUML Change Report | File | Status | |------|--------| $(for f in $CHANGED; do echo "| \\\`$f\\\` | \\\`\\\` Awaiting review |" ; done) _These .puml files have been auto-rendered. SVG artifact attached._" curl -s -X POST \ -H "Authorization: token $GH_TOKEN" \ -d "{\"body\": \"$COMMENT_BODY\"}" \ "https://api.github.com/repos/$GITHUB_REPOSITORY/issues/${{ github.event.pull_request.number }} /comments"
Drift Detection Drift = code changed but diagram didn’t. Detecting it:
1 2 3 4 5 6 7 8 - name: Check for stale diagram references run: | GHOST_FUNCTIONS="getUserById|createOrder|deleteSession" STALE=$(grep -r "$GHOST_FUNCTIONS" diagrams/ || true) if [ -n "$STALE" ]; then echo "⚠️ Potential stale references:" echo "$STALE" fi
For deeper drift detection, parse code and compare with diagram notes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 """drift_detector.py — detect stale PlantUML diagram references""" import subprocess, re, sysdef get_functions (repo_path ): """Extract function names from Python code.""" result = subprocess.run( ['grep' , '-r' , r'def \w+' , repo_path, '--include=*.py' ], capture_output=True , text=True ) return set (re.findall(r'def (\w+)' , result.stdout)) def get_puml_notes (puml_dir ): """Extract note text from .puml files.""" notes = set () for f in subprocess.run(['find' , puml_dir, '-name' , '*.puml' ], capture_output=True , text=True ).stdout.split(): with open (f) as fh: notes.update(re.findall(r'note\s+of\s+(\w+[^\n]*)' , fh.read())) return notes funcs = get_functions('.' ) notes = get_puml_notes('diagrams/' ) drift = notes & funcs if drift: print (f"⚠️ Drift detected: {drift} " ) sys.exit(1 )
Cache PlantUML JAR for Faster CI JAR is ~50MB. Cache it to cut CI time from ~60s to ~15s:
1 2 3 4 5 6 7 8 9 10 11 12 - name: Cache PlantUML JAR id: cache-plantuml uses: actions/cache@v4 with: path: ~/plantuml.jar key: plantuml-1.2024.2 - name: Download PlantUML (if cache miss) if: steps.cache-plantuml.outputs.cache-hit != 'true' run: | curl -L -o ~/plantuml.jar \ https://github.com/plantuml/plantuml/releases/download/v1.2024.2/plantuml.jar
Permissions and Security PlantUML can execute arbitrary code via !include / !function. In CI:
1 2 3 4 5 - name: Render with sandbox run: | java -jar ~/plantuml.jar \ -DROOT_DIR=/tmp/plantuml-sandbox \ -pipe -tsvg < diagram.puml > diagram.svg
Control !include with ALLOW_INCLUDE:
ALLOW_INCLUDE=local — local files only
ALLOW_INCLUDE=off — no includes
ALLOW_INCLUDE=secure — allowlist only
Never use INTERNET security level in CI — it allows network requests which is a security risk.
Complete Workflow 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 name: PlantUML CI Pipeline on: push: branches: [main ] paths: ['**.puml' , '.github/workflows/plantuml*.yml' ] pull_request: paths: ['**.puml' ] jobs: plantuml-check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-java@v4 with: distribution: temurin java-version: 17 - name: Cache PlantUML uses: actions/cache@v4 with: path: ~/plantuml.jar key: plantuml-1.2024.2 - name: Download PlantUML if: steps.cache.outputs.cache-hit != 'true' run: | curl -L -o ~/plantuml.jar \ https://github.com/plantuml/plantuml/releases/download/v1.2024.2/plantuml.jar - name: Syntax check all .puml files run: | find . -name "*.puml" -print0 | \ xargs -0 -I{} sh -c 'java -jar ~/plantuml.jar -checkonly {} && echo "OK: {}"' - name: Render to SVG run: | mkdir -p ci-artifacts find . -name "*.puml" -exec java -jar ~/plantuml.jar -tsvg -o ci-artifacts {} \; - name: Run drift detection run: python3 scripts/drift_detector.py - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: puml-render-results path: ci-artifacts/ - name: Comment on PR if: github.event_name == 'pull_request' run: scripts/comment_on_pr.sh
Results
Feature
Before CI
After CI
.puml syntax check
manual
automated
Diagram change review
manual screenshots
auto PR comment
Drift detection
none
automated
SVG artifacts
manual render
CI output
CI time (cached)
—
~15s