feat(workflow): Flowable up with registratie.bpmn external task (closes #4)
Some checks failed
CI / lint (pull_request) Has been cancelled
CI / build (pull_request) Has been cancelled
CI / unit (pull_request) Has been cancelled
CI / compose-smoke (pull_request) Has been cancelled

Add infra/flowable/docker-compose.yml (flowable-rest on Postgres, host :8090)
and workflows/registratie.bpmn — a minimal "Registratie ontvangen" process:
start -> external-worker task OpenZaakAanmaken -> end. A flowable-init
container deploys the model via the REST API on boot (idempotent: skips if
already deployed).

Add `make flowable-up/flowable-smoke/flowable-down`; flowable-smoke runs
infra/flowable/verify.py, which starts an instance and asserts it parks on
the OpenZaakAanmaken external task, then cleans up. Runbook included.

Verified clean-slate: down --volumes -> `make flowable-smoke` deploys on
boot, starts an instance, and confirms it waits at OpenZaakAanmaken.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 09:16:35 +02:00
parent c904c64597
commit d61594974f
5 changed files with 225 additions and 1 deletions

View File

@@ -0,0 +1,65 @@
# Flowable (S-03): the flowable-rest engine on Postgres.
# The registratie.bpmn model is deployed via the REST API on startup by flowable-init.
#
# docker compose -f infra/flowable/docker-compose.yml up -d
# # REST API (basic auth) under http://localhost:8090/flowable-rest/service/
#
# Host port 8090 (8000/8001/8080/8180 are taken by OpenZaak/NRC/BFF/Keycloak).
services:
flowable-db:
image: docker.io/library/postgres:16
environment:
POSTGRES_USER: flowable
POSTGRES_PASSWORD: flowable
POSTGRES_DB: flowable
volumes:
- flowable-db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U flowable -d flowable"]
interval: 5s
timeout: 3s
retries: 10
networks: [cg]
flowable-rest:
image: docker.io/flowable/flowable-rest:latest
environment:
SPRING_DATASOURCE_DRIVER-CLASS-NAME: org.postgresql.Driver
SPRING_DATASOURCE_URL: jdbc:postgresql://flowable-db:5432/flowable
SPRING_DATASOURCE_USERNAME: flowable
SPRING_DATASOURCE_PASSWORD: flowable
ports:
- "8090:8080"
depends_on:
flowable-db:
condition: service_healthy
networks: [cg]
# Deploys workflows/registratie.bpmn via the REST API once flowable-rest is up.
# Idempotent: skips if a deployment named "registratie" already exists.
flowable-init:
image: docker.io/curlimages/curl:latest
restart: "no"
volumes:
- ../../workflows/registratie.bpmn:/work/registratie.bpmn:ro,z
command:
- sh
- -c
- |
base=http://flowable-rest:8080/flowable-rest/service/repository/deployments
until curl -sf -u rest-admin:test "$$base" >/dev/null 2>&1; do echo "waiting for flowable-rest..."; sleep 3; done
if curl -s -u rest-admin:test "$$base?name=registratie" | grep -q '"name":"registratie"'; then
echo "registratie already deployed; skip"
else
curl -sf -u rest-admin:test -F 'file=@/work/registratie.bpmn;filename=registratie.bpmn' "$$base" >/dev/null && echo "deployed registratie"
fi
depends_on:
flowable-rest:
condition: service_started
networks: [cg]
volumes:
flowable-db:
networks:
cg:

54
infra/flowable/verify.py Normal file
View File

@@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""Smoke-check Flowable: the registratie process is deployed, and starting an
instance parks it on the OpenZaakAanmaken external task. Stdlib only.
"""
import base64, json, sys, time, urllib.error, urllib.request
BASE = "http://localhost:8090/flowable-rest/service"
AUTH = "Basic " + base64.b64encode(b"rest-admin:test").decode()
def call(method, path, payload=None):
data = json.dumps(payload).encode() if payload is not None else None
req = urllib.request.Request(BASE + path, data=data, method=method, headers={
"Authorization": AUTH, "Content-Type": "application/json", "Accept": "application/json"})
with urllib.request.urlopen(req, timeout=30) as r:
return r.status, json.loads(r.read() or "null")
def main():
# 1. process definition deployed? (wait for the async init-container deploy)
defs = {"total": 0}
for _ in range(40):
_, defs = call("GET", "/repository/process-definitions?key=registratie")
if defs["total"] >= 1:
break
time.sleep(3)
assert defs["total"] >= 1, "registratie process definition not deployed"
print(f"process definition 'registratie' deployed (total={defs['total']})")
# 2. start an instance
st, pi = call("POST", "/runtime/process-instances", {"processDefinitionKey": "registratie"})
assert st == 201, f"start failed: {st} {pi}"
pid = pi["id"]
assert pi.get("ended") is False, "instance ended immediately — external task not reached"
print(f"started instance {pid} (ended={pi.get('ended')})")
# 3. waiting on the external task?
_, ex = call("GET", f"/runtime/executions?processInstanceId={pid}")
activities = [e.get("activityId") for e in ex["data"]]
assert "OpenZaakAanmaken" in activities, f"not waiting at OpenZaakAanmaken: {activities}"
print(f"instance is waiting at the external task: {activities}")
# 4. cleanup
try:
call("DELETE", f"/runtime/process-instances/{pid}")
print("cleaned up instance")
except urllib.error.HTTPError:
pass
print("flowable smoke OK")
if __name__ == "__main__":
main()