Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m
`docs/RELEASE-PROCESS.md` §8.

## Unreleased
new(test-stack-up, check-requirements): a local lane — a down app starts with `mxcli run --local --watch --detach` (auto when no container runtime answers, or `--local` / `MXTK_APP_LANE=local`), free mxbuild serve port per project, restarts on the project's own APP_PORTS; container calls follow MXCLI_CONTAINER_CLI / docker (Desktop, colima, OrbStack) / podman; a missing runtime is no longer a missing requirement; a themed index.html no longer hides a Mendix app (refs #525)

- fix(progress-report): **`progress-report.py` no longer crashes with `TypeError: '<' not supported between instances of 'dict' and 'dict'` when two exec'd scripts share a leading number** (#513). `script_phases()` sorted `(number, phase dict)` pairs, so `10-data.mdl` and `10b-data-flows.mdl` (or one script logged under two paths), both matched to a plan phase, compared the dicts and every `exec.sh` run ended with `WARN: progress-report.html not updated`. It now sorts on (number, script name) only; an unmatched numbered script still takes the phase of the nearest lower-numbered matched one, ties broken by name. Fixture: `test-progress-report.sh` T17 (fails 3/3 on master). — jopterhorst (#513)
- new(canvas): **`project-bin/mf-canvas-facts.py` prints how a microflow or nanoflow lies on the Studio Pro canvas: labels on top of each other, lines through objects, note lines leaving the side that faces away, flows that run right-to-left or curl, captions wider than their shape, and decision captions that ask the opposite of their true branch (SENSE).** It prints facts, one line each, and no verdicts; read-only (it reads units through `mxpatch.py`), exit 2 when a flow could not be read. `verify-module.sh` runs it per module as an info rung (1c, log `14-canvas-facts.log`; a missing script, Python or `.mpr`, an exit 2 or a timeout is an INFO row reading NOT MEASURED, never a gate). Fixture `tests/wave2/test-mf-canvas-facts.sh` runs on a model mxcli v0.27.0 wrote (units and `.mpr` captured); `test-verify-module-446.sh` gains V8 for the rung. Calibration is from Studio Pro 10.24 screenshots, not verified on Studio Pro 11. Field evidence, measured 2026-10-21 on one Mendix 10.24 app: 1,588 flows read in one pass; on the 36 flows an mxcli-built branch added or changed in one module, OVERLAP 19, LINE 27, NOTE-SIDE 17, LEFTWARD 3, TURN 4, SENSE 9 before repair — saingge
Expand Down
12 changes: 10 additions & 2 deletions bin/check-requirements.sh
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,16 @@ else
gone "playwright" "not installed — every browser rung will fault (rc 2)"
note "fix: (cd $ROOT && npm i -D playwright && npx playwright install chromium)"
fi
command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1 \
&& ok "docker" "responding" || gone "docker" "not responding — no app, no DB, no Jaeger"
# The app needs a runtime, not a container: `mxcli run --local` serves it with none, so a missing
# container runtime is a note, never a missing requirement. Same CLI choice as mxcli
# (MXCLI_CONTAINER_CLI, else docker — Docker Desktop, colima, OrbStack — else podman).
_ctr="${MXCLI_CONTAINER_CLI:-}"; [ -n "$_ctr" ] || { command -v docker >/dev/null 2>&1 && _ctr=docker; } || { command -v podman >/dev/null 2>&1 && _ctr=podman; }
if [ -n "$_ctr" ] && "$_ctr" info >/dev/null 2>&1; then
ok "container runtime" "$_ctr responding (optional: mxcli docker, Jaeger)"
else
ok "container runtime" "${_ctr:-none} not responding — fine: the app runs with mxcli run --local"
note "Jaeger trace assertions need one; test-stack-up.sh starts the app on the local lane without it"
fi

# --- the work itself ---------------------------------------------------------------------
jn=$(ls "$ROOT"/journeys/*.journey.json 2>/dev/null | wc -l | tr -d ' ')
Expand Down
85 changes: 75 additions & 10 deletions project-bin/test-stack-up.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
# bin/test-stack-up.sh --check # report only, change nothing. Exit 0 iff required deps are up.
# bin/test-stack-up.sh # report, then bring up what is missing (may run a Docker build)
# bin/test-stack-up.sh --no-docker # bring up auxiliaries only; never build. "SP is already running it".
# bin/test-stack-up.sh --local # app down: start it with `mxcli run --local --watch --detach`, never a container
# bin/test-stack-up.sh --restart # skip the hot reload; rebuild and restart the container (old path)
# bin/test-stack-up.sh --track <pid> [label] # record a background test process THIS project started
# bin/test-stack-up.sh --reap # stop recorded processes older than MXTK_STALE_HOURS; nothing else
Expand Down Expand Up @@ -104,6 +105,7 @@
# project-specific in <project>/.claude/loop/stack.conf, which this script sources if present:
#
# APP_PORTS="8080 8081" # fallback scan list; ownership beats it
# RUN_LOCAL_SERVE_PORT=6644 # local lane's mxbuild port (default: first free from 6543)
# JAEGER_PORT=16686 # OTel collector UI (optional)
# MOCK_HEALTH_URL="http://localhost:3001/api/health" # set to enable the mock rung
# MOCK_PORT=3001
Expand Down Expand Up @@ -139,6 +141,22 @@ BOOT_TIMEOUT="${BOOT_TIMEOUT:-240}"
LOGDIR="${LOGDIR:-${TMPDIR:-/tmp}}"
MOCK_LOG="$LOGDIR/${PROJ}-mock.log"
DOCKER_LOG="$LOGDIR/${PROJ}-docker-run.log"
LOCAL_LOG="$LOGDIR/${PROJ}-run-local.log"

# APP LANE — how a down app is brought up. local = `mxcli run --local --watch --detach` (no container
# runtime at all); container = `mxcli docker run`. Default auto: the container lane when a runtime
# answers, else the local lane. MXTK_APP_LANE=local (or --local) skips containers even when one runs.
APP_LANE="${MXTK_APP_LANE:-auto}"
case "$APP_LANE" in auto|local|container) ;; *) echo "MXTK_APP_LANE takes auto, local or container" >&2; exit 2 ;; esac

# The container CLI, for every direct call below. mxcli itself honours MXCLI_CONTAINER_CLI, so this
# follows the same override; otherwise docker (Docker Desktop, colima, OrbStack, Rancher all ship it),
# else podman. Compose labels are com.docker.compose.* under docker compose and podman's
# docker-compose provider; podman-compose (python) labels differently and is not detected.
if [ -n "${MXCLI_CONTAINER_CLI:-}" ]; then CTR="$MXCLI_CONTAINER_CLI"
elif command -v docker >/dev/null 2>&1; then CTR=docker
elif command -v podman >/dev/null 2>&1; then CTR=podman
else CTR=""; fi

# mxcli lives in the project root on a wired project, but may be on PATH instead.
if [ -x "$ROOT/mxcli" ]; then MXCLI="$ROOT/mxcli"
Expand All @@ -149,6 +167,7 @@ MODE="up"
case "${1:-}" in
--check) MODE="check" ;;
--no-docker) MODE="nodocker" ;;
--local) MODE="up"; APP_LANE=local ;;
--restart) MODE="restart" ;;
--reap) MODE="reap" ;;
--track) MODE="track" ;;
Expand Down Expand Up @@ -194,6 +213,10 @@ mendix_at() {
case "$body" in
*mxui*|*mx-name*|*"Mendix"*|*mxclientsystem*) return 0 ;;
esac
# A themed index.html can carry none of those words (field project, 2026-10-23: its custom page has
# only a title and <div id="root">). The runtime's own client bundle cannot be themed away.
code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 4 "http://localhost:$p/mxclientsystem/mxui/mxui.js" 2>/dev/null)"
[ "$code" = 200 ] && return 0
done
return 1
}
Expand All @@ -218,18 +241,18 @@ mendix_at() {
# the pin is missing.)
owned_app_port() {
local want="$ROOT/.docker" name dir port
command -v docker >/dev/null 2>&1 || return 1
[ -n "$CTR" ] || return 1
while IFS='|' read -r name dir port; do
[ "$dir" = "$want" ] || continue
[ -n "$port" ] || continue
echo "$port"; return 0
done <<EOF
$(docker ps --filter 'label=com.docker.compose.service=mendix' \
$("$CTR" ps --filter 'label=com.docker.compose.service=mendix' \
--format '{{.Names}}' 2>/dev/null | while read -r n; do
[ -n "$n" ] || continue
printf '%s|%s|%s\n' "$n" \
"$(docker inspect -f '{{index .Config.Labels "com.docker.compose.project.working_dir"}}' "$n" 2>/dev/null)" \
"$(docker inspect -f '{{with index .NetworkSettings.Ports "8080/tcp"}}{{(index . 0).HostPort}}{{end}}' "$n" 2>/dev/null)"
"$("$CTR" inspect -f '{{index .Config.Labels "com.docker.compose.project.working_dir"}}' "$n" 2>/dev/null)" \
"$("$CTR" inspect -f '{{with index .NetworkSettings.Ports "8080/tcp"}}{{(index . 0).HostPort}}{{end}}' "$n" 2>/dev/null)"
done)
EOF
return 1
Expand Down Expand Up @@ -313,7 +336,9 @@ pid_alive() {
run_local_ports() {
local a="" m=""
if pick_local_hs; then a="$(hs_field appPort)"; m="$(sed -n 's/^ "adminPort": *\([0-9][0-9]*\).*/\1/p' "$LOCAL_HS" 2>/dev/null | head -1)"; fi
a="${RUN_LOCAL_APP_PORT:-${a:-8080}}"
# No handshake (a `run stop` removes it): the project's first configured APP_PORTS entry, so a
# restart lands where it always runs instead of on 8080, which another project may own.
a="${RUN_LOCAL_APP_PORT:-${a:-${APP_PORTS%% *}}}"; a="${a:-8080}"
m="${RUN_LOCAL_ADMIN_PORT:-${m:-$((a + 10))}}"
echo "$a $m"
}
Expand Down Expand Up @@ -626,9 +651,10 @@ fi
# Confirmed: container->localhost:3001 = 000, container->host.docker.internal:3001 = 200.
# Runs in --check too: this is exactly what you need to know BEFORE deciding to test.
CTR_WARNED=0
CONTAINER="$(docker ps --filter 'label=com.docker.compose.service=mendix' --format '{{.Names}}' 2>/dev/null | head -1)"
CONTAINER=""
[ -n "$CTR" ] && CONTAINER="$("$CTR" ps --filter 'label=com.docker.compose.service=mendix' --format '{{.Names}}' 2>/dev/null | head -1)"
if [ -n "$CONTAINER" ] && [ $MOCK_REQUIRED -eq 1 ] && [ $MOCK_OK -eq 0 ]; then
FROM_CTR="$(docker exec "$CONTAINER" sh -c \
FROM_CTR="$("$CTR" exec "$CONTAINER" sh -c \
"curl -s -o /dev/null -w '%{http_code}' --max-time 4 '$MOCK_HEALTH_URL'" 2>/dev/null)"
if [ "$FROM_CTR" = "000" ] || [ -z "$FROM_CTR" ]; then
CTR_WARNED=1
Expand Down Expand Up @@ -750,16 +776,52 @@ if [ $APP_FOUND -ne 0 ]; then
# `mxcli docker run` fail with a daemon error the agent then reports as "cannot start Docker".
# A cloud container ships the docker CLI with no daemon; that is normal, not a blocker.
# Bounded: a stopped Docker Desktop can leave `docker info` silent for minutes.
_RT=docker; command -v docker >/dev/null 2>&1 || _RT=podman
if ! command -v "$_RT" >/dev/null 2>&1; then _DK=1; else
_RT="$CTR"
if [ "$APP_LANE" = local ]; then _DK=3
elif [ -z "$_RT" ] || ! command -v "$_RT" >/dev/null 2>&1; then _DK=1; else
"$_RT" info >/dev/null 2>&1 & _dk_pid=$!; _dk_w=0; _DK=
while kill -0 "$_dk_pid" 2>/dev/null; do
[ "$_dk_w" -ge 15 ] && { kill "$_dk_pid" 2>/dev/null; _DK=2; break; }
sleep 1; _dk_w=$((_dk_w + 1))
done
[ -n "$_DK" ] || { wait "$_dk_pid"; _DK=$?; }
fi
if [ "$_DK" != 0 ]; then
[ "$_DK" != 0 ] && [ "$APP_LANE" = container ] && { bad "App down, MXTK_APP_LANE=container, and no container runtime answers ($([ -n "$_RT" ] && echo "$_RT info failed" || echo 'none installed'))."; exit 1; }
# LOCAL LANE: start the app without any container. Measured with mxcli v0.26.0 (2026-10-23): a
# split model (mprcontents/, 3563 units) served by run --local --watch for hours stayed split and
# git-clean. Older builds consolidated it, so the unit count is compared before and after anyway.
if [ "$_DK" != 0 ] && [ -n "$MXCLI" ]; then
_U0=$(find "$MODEL_DIR/mprcontents" -name '*.mxunit' 2>/dev/null | wc -l | tr -d ' ')
check_run_local_ports || { bad "App not serving yet — this project's own runtime holds its port. Re-run --check when it answers."; exit 1; }
read -r _LA _LM <<<"$(run_local_ports)"
# mxbuild --serve listens on 6543 unless told otherwise, so a second project's local lane would
# collide with the first. RUN_LOCAL_SERVE_PORT (stack.conf) wins; else the first free port from 6543.
_LS="${RUN_LOCAL_SERVE_PORT:-}"
if [ -z "$_LS" ]; then
for _p in $(seq 6543 6599); do mxtk_port_holder "$_p" >/dev/null 2>&1 || { _LS=$_p; break; }; done
fi
echo "→ App is down. Starting it without a container: mxcli run --local --watch --detach (:$_LA, admin :$_LM, build :${_LS:-6543})..."
[ "$_DK" = 3 ] || echo " (no container runtime answered; the local lane needs none)"
T0=$(date +%s)
"$MXCLI" run --local --watch --detach -p "$MPR" --app-port "$_LA" --admin-port "$_LM" ${_LS:+--serve-port "$_LS"} >"$LOCAL_LOG" 2>&1
LOCAL_RC=$?
if [ $LOCAL_RC -ne 0 ]; then
bad "mxcli run --local failed (rc=$LOCAL_RC) — see $LOCAL_LOG"; tail -20 "$LOCAL_LOG"
echo " A missing database is the usual cause: it must exist before the first run (createdb, or --db-type hsqldb)."
exit 1
fi
_U1=$(find "$MODEL_DIR/mprcontents" -name '*.mxunit' 2>/dev/null | wc -l | tr -d ' ')
if [ "$_U0" -gt 0 ] && [ "$_U1" -lt "$_U0" ]; then
warn "this mxcli consolidated the split model ($_U0 → $_U1 mprcontents units): a build artifact."
warn " → never commit it; restore with: git checkout -- $(basename "$MPR") mprcontents/"
fi
if prove_app; then
APP_FOUND=0; record_served
ok "App serving on :$APP_PORT in $(( $(date +%s) - T0 ))s (ownership $APP_OWNERSHIP, $APP_SOURCE) — --watch keeps it current"
else
bad "mxcli run --local returned, but no Mendix of this project answers over HTTP — see $LOCAL_LOG"; exit 1
fi
elif [ "$_DK" != 0 ]; then
check_run_local_ports || { bad "App not serving yet — this project's own runtime holds its port. Re-run --check when it answers."; exit 1; }
bad "App down and no Docker/Podman reachable, so this script cannot build the app container."
echo " Normal in a cloud container, and fine on a desktop without one. Run the app without it:"
Expand All @@ -769,6 +831,9 @@ if [ $APP_FOUND -ne 0 ]; then
echo " Snapshot first — mxcli run --local consolidates a split-model .mpr. Then re-run with --check."
exit 1
fi
fi
# CONTAINER LANE — still down after the local lane, so a runtime answered and the lane allows it.
if [ $APP_FOUND -ne 0 ]; then
if [ -z "$MXCLI" ]; then
bad "App down and no mxcli found (neither ./mxcli nor on PATH) — cannot build."
exit 2
Expand Down
Loading