Skip to content
Merged
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 @@ -19,6 +19,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m

## Unreleased

- fix(exec): **when mxbuild crashes on the written model (non-zero exit, no error list) after a baseline that mxbuild did measure, exec.sh now rolls the write back instead of keeping it.** Before, the default path kept the write, the next exec printed "Baseline NOT measured" and built on a model nothing could load. One BUILD-LOG row says the script was rolled back and carries the mxbuild exit code; without a measured baseline (the crash may be the machine) and in `--patch` mode the old behaviour is unchanged. No new refusal, no guard. Extracted from the crash-rollback hunk of #311. Test: `tests/wave2/test-exec-crash-rollback.sh` (synthetic stub mxbuild; 4 of its checks fail on master) — field incident 2026-10-13 (#309)
- fix(_common): **`find_java` on macOS picks the JDK that matches the model's Mendix version: 25 for 11.14 and above, 21 below (and when the version cannot be read).** It always asked for `java_home -v 21`, a pin added for 11.12.2 (JDK 25 breaks that build's gradle 8.5). On 11.15 the same pin broke the other way: mxbuild failed `release version 25 not supported` (exit 3) on the first Java-action build, 21 times in one lab phase, until `JAVA_HOME` was set by hand in `toolkit.env`. Mendix's system requirements list JDK 21 for Studio Pro 11.13 and below and JDK 25 for 11.14 and above. The version comes from `mxtk_model_version` (the .mpr's `_MetaData`). Detection uses `java_home -F -v N`, because plain `-v N` never fails on macOS (a missing JDK silently returns the default, exit 0). When the wanted JDK is missing, the default is still used, and the new `find_java_note` line is printed by exec.sh, verify-model.sh and doctor.sh (they all discard find_java's stderr). `JAVA_HOME` still wins; `MXTK_JAVA_HOME_BIN` overrides the java_home binary for tests. `tests/wave2/test-find-java-version.sh` (fake java_home that mimics the real fallback-with-exit-0, 14 cases) fails on master for 11.14, 11.15 and 12.x. Not run: a real two-JDK Mac build; the version table is taken from the Mendix docs. Field report: a five-app lab project (Mendix 11.15, mxcli v0.26.0), TK-018 — MendixMau
- learn(workflow skills): **a Jump has one fixed target, so one failure handler for N steps is a child workflow, not a shared node; plus three probed workflow errors (CE0117 on a `System.Workflow` argument means map `$WorkflowInstance`, CE1613 means the called workflow's parameter must be `$WorkflowContext`, and `returns` goes before `exposed as workflow action`).** `workflow-structure-rules.md` §1 and `learned-workflow-patterns.md` §9. The §9 rows were probed on mxcli v0.26.0 / Mendix 11.15.0 with native `mx check`. The third row corrects the reading that a called microflow cannot return a value. Upstream issues: mendixlabs/mxcli#1529 (CE0117), #1530 (CE1613, and `syntax workflow call-workflow` shows the failing form), #1531 (describe drops every call workflow mapping), #1533 (`returns` parse messages). Source: a five-app lab project (Mendix 11.15, mxcli v0.26.0) — five-app lab report, WF-1
- learn(fixture-seeding): **resetting workflow data: abort instances through the runtime (Workflow Commons admin or *Change workflow state*); if SQL is unavoidable, delete the link tables first (`system$workflow_currentactivity`, `system$workflow_workflowdefinition`, `system$workflowusertask_assignees`, `system$workflowusertask_targetusers`, `system$workflowusertask_targetgroups`, plus the `system$workflowendedusertask_*` equivalents; names read from a Mendix 11.15 database), and reset a context object only after its instances are gone.** A seed reset that deleted workflow rows by SQL failed on those links (WF-5). Text only, `ondemand` skill. — five-app lab report, WF-5
Expand Down
24 changes: 21 additions & 3 deletions project-bin/exec.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1769,9 +1769,27 @@ EOF
echo " ✗ mxbuild failed to run (exit $MXBUILD_EXIT) — gate could not verify the model."
[ -n "$MXB_WHY" ] && echo " mxbuild says: $MXB_WHY"
echo "$MXBUILD_OUT" | grep -v "^$\|icon\|Microsoft\|Assembly\|__" | head -20 || true
echo " → Snapshot preserved. Open in SP to verify manually before proceeding."
# --patch logs its own row below, after rolling back; one row per run.
[ "$PATCH_MODE" = "1" ] || log_build "❌ gate could not run" "mxbuild exit $MXBUILD_EXIT"
# mxbuild CRASHED ON THE WRITTEN MODEL (field 2026-10-13, Mendix 11.14.0, mxcli v0.25.0): a
# two-hop data source passed `check`, then mxbuild's loader died with no errors file. This
# branch kept the write, the next exec printed "Baseline NOT measured" and wrote on top of a
# model nothing could load. BASE_KNOWN=1 means mxbuild ran on this model a moment ago
# (measured baseline, or a clean verification stamp), so the crash is a property of the
# WRITE, not of the machine: roll it back like a failed gate. Without a measured baseline it
# may be the machine (wrong version, JDK), and the old behaviour stays: kept, flagged.
if [ "$PATCH_MODE" != "1" ] && [ "${BASE_KNOWN:-0}" = "1" ]; then
echo " ✗ mxbuild ran on this model before the write and crashed after it — rolling the write back."
restore_snapshot
if [ "$RESTORED" -eq 1 ]; then
log_build "❌ gate could not run" "mxbuild exit $MXBUILD_EXIT on the written model (ran on the baseline) — script rolled back${MXB_WHY:+; mxbuild: $MXB_WHY}"
else
echo " STOP: the model may not load. Restore it before the next exec: ./bin/restore-mpr.sh <newest .mpr-snapshots/ dir>"
log_build "❌ gate could not run" "mxbuild exit $MXBUILD_EXIT on the written model — NOT rolled back (restore did not run)"
fi
else
echo " → Snapshot preserved. Open in SP to verify manually before proceeding."
# --patch logs its own row below, after rolling back; one row per run.
[ "$PATCH_MODE" = "1" ] || log_build "❌ gate could not run" "mxbuild exit $MXBUILD_EXIT"
fi
else
GATE_STATE="pass"; GATE_ERRORS=0
echo " ✓ mxbuild: 0 errors — model is clean."
Expand Down
118 changes: 118 additions & 0 deletions tests/wave2/test-exec-crash-rollback.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#!/usr/bin/env bash
# test-exec-crash-rollback.sh — exec.sh on the DEFAULT path (no --patch, no --on-copy) when
# mxbuild crashes on the written model.
#
# usage: bash tests/wave2/test-exec-crash-rollback.sh [path-to-exec.sh]
#
# Field (2026-10-13, Mendix 11.14.0): a script passed `mxcli check`, mxbuild's loader then died
# with no errors file. exec.sh kept the write; the next exec printed "Baseline NOT measured" and
# built on a model nothing could load. Now: when mxbuild ran on the model BEFORE the write (the
# baseline is known) and exits non-zero with no error list AFTER it, the write is rolled back.
#
# A normal success: the write is kept, "applied" row [guard]
# B baseline ok, then mxbuild crashes (exit 137, no errors file): model byte-identical to
# before, one "rolled back" row, nothing left half-applied
# C mxbuild crashes on the baseline too (baseline not measured): it may be the machine, so
# the old behaviour stays, the write is kept and flagged [guard]
# D a clean error list after the write (an ordinary gate failure): still the normal
# gate-failed rollback, not the crash row [guard]
#
# Synthetic only: throwaway git repo, stub mxcli, stub mxbuild, stub java. Nothing real runs.
set -uo pipefail

TOOLKIT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
SUBJECT="${1:-$TOOLKIT/project-bin/exec.sh}"
case "$SUBJECT" in /*) ;; *) SUBJECT="$PWD/$SUBJECT" ;; esac
WORK="$(cd "$(mktemp -d "${TMPDIR:-/tmp}/exec-crash.XXXXXX")" && pwd -P)" || exit 2
trap 'rm -rf "$WORK"' EXIT
PASS=0; FAIL=0
ok() { PASS=$((PASS+1)); echo " ok — $1"; }
bad() { FAIL=$((FAIL+1)); echo " FAIL — $1${2:+: $2}"; }
echo "== subject: $SUBJECT"

P="$WORK/proj"
mkdir -p "$P/bin" "$P/mprcontents" "$P/mdlsource" "$WORK/jdk/bin"
printf 'not a real model\n' > "$P/Fixture.mpr"
printf 'bson\n' > "$P/mprcontents/a.mxunit"
printf 'CREATE MODULE "Nope";\n' > "$P/mdlsource/test.mdl"
cp "$TOOLKIT/project-bin/_common.sh" "$TOOLKIT/project-bin/restore-mpr.sh" "$P/bin/"
cp "$SUBJECT" "$P/bin/exec.sh"
cat > "$P/bin/snapshot-mpr.sh" <<'SNAP'
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")/.."
D=".mpr-snapshots/$(date +%Y%m%d-%H%M%S)-$$-$RANDOM"
mkdir -p "$D"; cp Fixture.mpr "$D/"; cp -r mprcontents "$D/"
echo "Snapshot saved: $D"
SNAP
# The write: `mxcli exec` appends to the model, as a real script would.
cat > "$P/mxcli" <<'MXCLI'
#!/usr/bin/env bash
D="$(cd "$(dirname "$0")" && pwd)"
case "${1:-}" in
check) exit 0 ;;
exec) echo "written by the script" >> "$D/Fixture.mpr"; echo "x" >> "$D/mprcontents/a.mxunit"; echo "fake mxcli exec"; exit 0 ;;
esac
exit 0
MXCLI
chmod +x "$P/bin/"*.sh "$P/mxcli"
# mxbuild: MXB_MODE per call number, comma list: ok | crash (exit 137, no errors file) | err (a CE0001 error list)
cat > "$WORK/mxbuild" <<'MXB'
#!/usr/bin/env bash
n=$(( $(cat "$MXB_N" 2>/dev/null || echo 0) + 1 )); echo "$n" > "$MXB_N"
mode="$(printf '%s\n' "$MXB_MODES" | tr ',' '\n' | sed -n "${n}p")"; mode="${mode:-ok}"
err=""; for a in "$@"; do case "$a" in --write-errors=*) err="${a#--write-errors=}" ;; esac; done
case "$mode" in
ok) [ -n "$err" ] && printf '{"problems":[]}' > "$err"; exit 0 ;;
crash) echo "Unhandled exception: loader died" >&2; exit 137 ;;
err) [ -n "$err" ] && printf '{"problems":[{"severity":"Error","code":"CE0001","message":"boom","element":{"document":"M.X"}}]}' > "$err"; exit 1 ;;
esac
MXB
printf '#!/usr/bin/env bash\nexit 0\n' > "$WORK/jdk/bin/java"
chmod +x "$WORK/mxbuild" "$WORK/jdk/bin/java"
( cd "$P" && git init -q . && printf '.mpr-snapshots/\ndocs/\n.claude/\n' > .gitignore && git add -A \
&& git -c user.email=t@t -c user.name=t commit -qm fixture ) >/dev/null 2>&1

LOG="$P/docs/BUILD-LOG.md"
sum() { cat "$P/Fixture.mpr" "$P/mprcontents/a.mxunit" | cksum; }
BEFORE="$(sum)" # the committed model; run() resets to it
# run <label> <modes> — resets the model, runs the default path, prints the exit code
run() {
( cd "$P" && git checkout -q -- Fixture.mpr mprcontents && rm -rf .mpr-snapshots docs .claude ) >/dev/null 2>&1
rm -f "$WORK/n"; mkdir -p "$WORK/tmp"
( cd "$P" && TMPDIR="$WORK/tmp" SP_RESTART=0 MXTK_NO_INSTALL=1 MXB_N="$WORK/n" MXB_MODES="$2" \
MXBUILD_PATH="$WORK/mxbuild" JAVA_HOME="$WORK/jdk" ./bin/exec.sh mdlsource/test.mdl ) >"$WORK/out.$1" 2>&1
echo $?
}
lastrow() { tail -1 "$LOG" 2>/dev/null; }
kept() { grep -q 'written by the script' "$P/Fixture.mpr"; }

echo "== A: baseline ok, gate ok: kept, applied row [guard] =="
RC=$(run A ok,ok)
[ "$RC" -eq 0 ] && kept && ok "exit 0, write kept" || bad "A rc=$RC" "$(tail -8 "$WORK/out.A")"
lastrow | grep -q 'applied' && ok "applied row" || bad "A row" "$(lastrow)"
grep -q 'rolling the write back' "$WORK/out.A" && bad "A: rollback message on a clean run" || ok "no rollback message"

echo "== B: baseline ok, mxbuild crashes after the write: rolled back =="
RC=$(run B ok,crash)
kept && bad "B: the write was KEPT after a crash" || ok "write rolled back"
[ "$(sum)" = "$BEFORE" ] && ok "model identical to before the exec" || bad "B: model differs from before"
grep -q 'rolling the write back' "$WORK/out.B" && ok "says it is rolling back" || bad "B: no message" "$(tail -10 "$WORK/out.B")"
[ "$(grep -c 'gate could not run' "$LOG")" -eq 1 ] && lastrow | grep -q 'mxbuild exit 137 on the written model.*script rolled back' \
&& ok "one BUILD-LOG row, says rolled back with the exit code" || bad "B: row" "$(cat "$LOG" 2>/dev/null | tail -3)"

echo "== C: baseline crashes too (not measured): old behaviour, kept and flagged [guard] =="
RC=$(run C crash,crash)
kept && ok "write kept (machine may be the cause)" || bad "C: rolled back without a measured baseline"
grep -q 'Snapshot preserved' "$WORK/out.C" && ok "old message" || bad "C: message" "$(tail -8 "$WORK/out.C")"
lastrow | grep -q 'gate could not run | mxbuild exit 137 ·' && ok "old row, no rollback claim" || bad "C: row" "$(lastrow)"

echo "== D: a clean error list after the write: the ordinary gate-failed path [guard] =="
RC=$(run D ok,err)
kept && bad "D: a new error was KEPT" || ok "rolled back by the normal gate"
lastrow | grep -q 'gate failed' && ok "gate failed row, not the crash row" || bad "D: row" "$(lastrow)"
grep -q 'rolling the write back' "$WORK/out.D" && bad "D: took the crash branch" || ok "crash branch not taken"

echo ""
echo "PASS: $PASS FAIL: $FAIL"
[ "$FAIL" -eq 0 ]
Loading