PlantUML in CI/CD: GitHub Actions auto-render, PR checks, and drift detection

puml.online

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
#!/usr/bin/env python3
"""drift_detector.py — detect stale PlantUML diagram references"""
import subprocess, re, sys

def 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
  • Title: PlantUML in CI/CD: GitHub Actions auto-render, PR checks, and drift detection
  • Author: puml.online
  • Created at : 2026-08-08 10:00:00
  • Updated at : 2026-08-14 21:34:29
  • Link: https://puml.online/blog/plantuml-ci-cd-en/
  • License: This work is licensed under CC BY-NC-SA 4.0.