diff --git a/CHANGELOG.md b/CHANGELOG.md index 43945e7f..c67cd3a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/project-bin/exec.sh b/project-bin/exec.sh index d1d46e22..c3a7ec20 100755 --- a/project-bin/exec.sh +++ b/project-bin/exec.sh @@ -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 " + 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." diff --git a/tests/wave2/test-exec-crash-rollback.sh b/tests/wave2/test-exec-crash-rollback.sh new file mode 100644 index 00000000..93c0bc9e --- /dev/null +++ b/tests/wave2/test-exec-crash-rollback.sh @@ -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