Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area":"cmd/mxcli","date":"2026-10-23","symptom":"The nightly goes red on the Mendix 11.14.0 and 11.15.0 legs only, in `TestRunLifecycle_DetachStatusExecWaitStop`: `wait (Mendix 11.14+): want build #2 to fail with the serve-rebuild hint; exit 0, \"applied: build #2 via restart in 48.1s (runtime restarted; browser sessions dropped)\"`. Every other leg, push CI and the unit tests are green","cause":"Not a product defect: a nightly-only test pinned the 11.14+ mxbuild --serve regression as a FAILED build #2, and cf135bfc (serveRecovery) made the watch loop restart mxbuild --serve and build on the fresh process when an incremental build fails for want of client source, so build #2 now applies. The lifecycle test needs a cached mxbuild and a runtime, so it runs only in the nightly matrix, never in the PR that changed the behaviour","file":"`cmd/mxcli/cmd_run_lifecycle_integration_test.go` (`wantBuild2`); the behaviour is `cmd/mxcli/docker/serve_recovery.go`","insight":"A test that pins a documented failure is a contract with the code that explains it: improving that failure flips the test, and when the test only runs nightly the flip lands a day later on main. Whoever changes serve_recovery.go, webclient_legacy_paths.go or the watch loop should grep for `serveRebuildsBroken` and run the lifecycle test on 11.14+. The new assertion keeps the 'when mxbuild fixes it, this branch should go' property by requiring the recovery's own log line ('cannot build this change incrementally') next to the applied build, not just an applied build. Reproduce in about 2 minutes: `mxcli setup mxbuild --version 11.14.0`, JAVA_HOME at a JDK 25 (11.14's serve refuses 21 with 'JDK 25 not found'), then `MXCLI_RUNLIFE_VERSION=11.14.0 go test -tags integration -run TestRunLifecycle_DetachStatusExecWaitStop ./cmd/mxcli/`. Controls: the old test against current code fails at line 157 with the nightly's exact message; the new test passes all six steps; the new test with the legacy-client restart branch stubbed fails with mxbuild's 'Could not find a part of the path …/web/pages/MyFirstModule.Hello.js'","refs":["mendixlabs/mxcli nightly run 39915484754","ako/mxcli nightly run 39914908250"],"test":"cmd/mxcli/cmd_run_lifecycle_integration_test.go"}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area":"mdl/backend","date":"2026-10-23","symptom":"`alter page … set (eventDataAttribute: JsonData) on chart1` was refused for every spelling: \"pluggable property \\\"eventDataAttribute\\\" holds a value of kind Attribute, which ALTER cannot set from a plain value — rewrite the widget with CREATE OR REPLACE PAGE (or ALTER PAGE REPLACE) instead\"; `$Ctx.EventData` failed earlier, at parse, as \"takes a plain value, not an expression\". check --references refused the same, so one binding could only be fixed by rewriting the whole widget.","cause":"Which entity a bare attribute name belongs to is the widget's binding rule applied to the property's datasource link and the scopes around it — not a fact of the BSON the page mutator holds — so setPluggableWidgetPropertyMut had nothing to qualify a name with and refused the kind outright. The ALTER SET visitor refused every expression-shaped value except DynamicClasses / DynamicCellClass, `$Param.Attr` included.","fix":"The mutator reports the property instead of binding it: PluggablePropertyScope (an optional backend.PluggablePropertyScoper) gives the kind, DataSourceProperty link, and the linked and enclosing sources' entity or flow. The executor (resolvePluggableSetValue) binds by CREATE's own functions — resolveBindingScope, misboundAttributeError (CE1613 and CE7247 refusals word for word), parameterVariable for `$Param.Attr`, resolveAttributePathForEntity for inherited members — and hands the mutator a backend.AttributeBinding, which writes AttributeRef and SourceVariable (null when bound to the context object). The visitor passes the strict `$Param.Attr` shape (ast.IsParameterAttributeText) through as a WidgetExpressionV3; on any other property the executor refuses it. check dry-runs the same applySetProperties, so it follows with no code of its own.","insight":"When a mutator refuses a kind because it 'cannot know' something, check whether the executor already knows it: here the whole binding rule existed for CREATE and only the property's link and scopes had to be read out of the stored document. Keep the mutator a writer of resolved values and the refusals stay shared — a second binding rule in the mutator is the duplicate-resolver drift that this area keeps paying for. The SourceVariable has to be written as null when a binding returns to the context object: leaving the stale `$Ctx` in place was measured as CE7247 \"Attribute 'MyFirstModule.ChartEntry.JsonData' is not an attribute of entity 'MyFirstModule.ChartContext'\" on 11.12.5, with describe printing `$Ctx.JsonData`. Wrong turn to skip: relaxing the visitor's expression refusal in general — only the one `$Param.Attr` shape is a binding.","issue":"ako/mxcli#1622","refs":["ako/mxcli#1502","mendixlabs/mxcli#1591"],"ce":["CE7247","CE1613"],"file":"mdl/backend/pagemutator/pluggable_attribute.go, mdl/backend/pagemutator/mutator.go (setPluggableWidgetPropertyMut), mdl/executor/alter_set_pluggable_attribute.go, mdl/executor/cmd_alter_page.go (applySetProperties), mdl/visitor/visitor_alter_page.go, mdl/ast/ast_page_v3.go (IsParameterAttributeText)","test":"mdl/backend/pagemutator/pluggable_attribute_set_1622_test.go, mdl/executor/alter_set_pluggable_attribute_1622_test.go, mdl/visitor/visitor_alter_set_parameter_attribute_test.go, mdl-examples/bug-tests/1622-alter-set-pluggable-attribute.mdl"}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "mdl/executor", "date": "2026-10-23", "symptom": "`find $l by Other.Person_Dept = $d` over HR.Emp (HR.Emp extends Other.Person) built clean, but describe printed `find $l by Person_Dept = $d`; executed as a new microflow that passed check -p and exec and mxbuild then failed `[CE1613] \"The selected attribute 'HR.Emp.Person_Dept' no longer exists.\"` (11.12.5). A hand-written bare inherited association in a find or a create's members stored the same attribute (ako/mxcli#1637)", "cause": "An association lives in its FROM entity's module, so one inherited from a generalization in another module is not in the specialization's module. resolveMemberChange looked a bare name up only there, then fell through to an attribute of the entity. resolveAttributeInEntityHierarchy already walked the chain for attributes; nothing did for associations", "file": "`mdl/executor/cmd_microflows_builder_actions.go` (`resolveInheritedAssociation`, tried after the inherited-attribute lookup in resolveMemberChange's bare-name branch)", "insight": "Fix the reader the short form is executed by, not the printer: describe has no variable types for a list operation, but the builder knows the entity, so making the bare name resolve through the chain makes describe's existing output correct, leaves every describe golden untouched, and lets the member speller (which shortens only when the short name resolves to the same member) agree with describe for free — its test was the cheapest proof, the round trip needs a FRESH build because re-running over the stored flow is spliced as 'Unchanged'. Assert on the stored unit bytes (`HR.Emp.Person_Dept` absent, `Other.Person_Dept` present): describe prints attribute and association alike, so it cannot show the damage. Not covered: the same bare name in two modules on one chain still resolves to the entity's own module first, and check -p still passes a member that names nothing", "refs": ["ako/mxcli#1637", "ako/mxcli#662", "mdl-examples/bug-tests/1637-inherited-association-member.mdl"], "ce": ["CE1613"], "rules": []}
2 changes: 1 addition & 1 deletion .claude/skills/mendix/alter-page/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ so a silent write would build cleanly and then fail to open.
> `Editable` on a non-input widget is rejected. This mirrors CREATE PAGE's
> `visible:` — see the create-page skill for enum-value rules.

**Pluggable widget properties** use quoted names to set values in the widget's `Object.Properties[]`. Boolean values are stored as `"yes"`/`"no"` in BSON.
**Pluggable widget properties** use quoted names to set values in the widget's `Object.Properties[]`. Boolean values are stored as `"yes"`/`"no"` in BSON. An **attribute** property binds as in CREATE: `Attr` (the bound entity's), `Module.Entity.Attr`, or `$Param.Attr` — `set (eventDataAttribute: $Ctx.EventData) on chart1`; an attribute the bound entity lacks is refused (CE1613, CE7247).

**Column property names are case-insensitive** in MDL — `set (caption: …)` and `set (Caption: …)` both work. The internal BSON keys are dictated by the widget schema and stay case-sensitive on the storage side.

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Fixed

- **A find, filter or create by an association inherited from another module keeps that association when written short** (ako/mxcli#1637) — over `HR.Emp extends Other.Person`, `describe` prints `find $l by Other.Person_Dept = $d` as `find $l by Person_Dept = $d`, and the builder looked a bare member up only in `HR`, so executing that description as a new microflow — or writing the name short by hand — stored the attribute `HR.Emp.Person_Dept`: `check -p` and exec passed, then mxbuild failed with CE1613 (11.12.5). Re-running over the stored flow said "Unchanged", so only a rebuild showed it. A bare member name now resolves through the generalization chain to an association of an ancestor in another module, after the inherited attributes.
- **`find` / `filter` by `System.owner` or `System.changedBy` builds, and describe prints System associations back so they re-execute** (ako/mxcli#1629) — `$m = find $l by System.owner = $User;` was refused at exec with advice to create a System association, though mxbuild builds it (11.12.5). It now resolves to the System association; on an entity whose generalization root does not store the member, `check -p` and exec refuse it with the CE0091 "No member selected." mxbuild would report, naming the `alter entity` that fixes it. `describe` printed a stored one as `by owner`, and a System association inherited by a specialization of `System.User` as `by User_Language` — which re-executed as an attribute of the specialization, CE1613. A System association in a find or filter is now printed qualified (`by System.User_Language`), and running the description again changes nothing.
- **`marketplace diff` and `update` no longer fail with "path is too long" under the default Windows `%TEMP%`** (mendixlabs/mxcli#1486) — `mx` refuses to write any file whose path is 260 characters or longer, on every platform (measured on Linux with mx 11.12.1: a 259-character path is written, a 260-character one refused with the reported *"The specified file name or path is too long"*), and the reference project was built in `%TEMP%\mxdiff<digits>\target-ref`. The deepest files mx writes there are bundled `node_modules` trees — 181 characters in the 11.12.1 blank app, about 202 in NanoflowCommons 7.x — so that layout fitted 7.x for no user name at all. References are now built under the shortest directory the user owns (`%USERPROFILE%\.mxcli\t` on Windows, `/tmp` on Linux and macOS, or `TMP`/`TMPDIR` when shorter), named in one letter beneath it; never a drive-root folder such as `C:\mxt`, whose children every local account may modify. A package deep enough to hit the limit anyway gets an error naming the directory, its length, what it leaves for the package, and how to move it. `marketplace install` builds its reference the same way.
- **`marketplace update` and `install` no longer write the package's metadata into the app root** (mendixlabs/mxcli#1487) — after updating Nanoflow Commons 6.2.1 → 7.4.0, `manifest.json`, `License.txt` and `SiemensMendixNanoflowCommons__7.4.0__READMEOSS_<timestamp>.html` sat beside the `.mpr` as untracked files, because every archive entry but `project.mpr` and `package.xml` was copied into the project. Only the files the package's `package.xml` lists under `<files>` are installed now — exactly what `mx module-import` installs, measured on 11.12.1 with ExternalDatabaseConnector 6.3.0 and BusinessEvents 3.12.1, which includes passing over a listed file the archive lacks. What is left out (`License.txt`, `manifest.json`, the READMEOSS report, `Mendix EULA.docx`) is named on one line of the output.
Expand Down
42 changes: 22 additions & 20 deletions cmd/mxcli/cmd_run_lifecycle_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,10 @@ import (
//
// On Mendix 11.14+ every build after the first in one mxbuild --serve process
// fails (an mxbuild regression, measured without mxcli: webclient_legacy_paths.go).
// There the steps that wait for build #2 assert that documented failure — the
// build fails and the run log explains why — instead of an applied build. When an
// mxbuild fixes it, those assertions fail and this branch should go.
// mxcli recovers by restarting mxbuild --serve and building on the fresh process
// (serve_recovery.go), so build #2 still applies there; the steps that wait for it
// also assert that the run log shows that recovery. When an mxbuild fixes the
// regression, those assertions fail and this branch should go.
func TestRunLifecycle_DetachStatusExecWaitStop(t *testing.T) {
version := os.Getenv("MXCLI_RUNLIFE_VERSION")
if version == "" {
Expand Down Expand Up @@ -109,22 +110,23 @@ func TestRunLifecycle_DetachStatusExecWaitStop(t *testing.T) {
return f
}

// wantBuild2 checks a `run wait` that should report build #2: applied, or on
// 11.14+ failed with mxcli's explanation of the mxbuild regression in the log.
// orApplied also accepts an applied build #2 on 11.14+, for a step whose
// claim is only that build #2 ran.
wantBuild2 := func(step, out string, code int, orApplied bool) {
// wantBuild2 checks a `run wait` that should report build #2 applied. On
// 11.14+ that build only applies because mxcli restarted mxbuild --serve after
// the incremental build failed, so the run log must show that recovery.
// orDirect drops that requirement, for a step whose claim is only that build
// #2 ran.
wantBuild2 := func(step, out string, code int, orDirect bool) {
t.Helper()
if serveRebuildsBroken && !(orApplied && code == 0 && strings.HasPrefix(out, "applied: build #2")) {
log, _ := os.ReadFile(docker.RunLogPath(mpr))
if code != 1 || !strings.HasPrefix(out, "failed: build #2") || !strings.Contains(string(log), "first build does not") {
t.Fatalf("%s (Mendix 11.14+): want build #2 to fail with the serve-rebuild hint; exit %d, %q\n%s", step, code, out, tailOf(docker.RunLogPath(mpr)))
}
return
}
if code != 0 || !strings.HasPrefix(out, "applied: build #2") {
t.Fatalf("%s: exit %d, %q\n%s", step, code, out, tailOf(docker.RunLogPath(mpr)))
}
if serveRebuildsBroken && !orDirect {
log, _ := os.ReadFile(docker.RunLogPath(mpr))
if !strings.Contains(string(log), "cannot build this change incrementally") {
t.Fatalf("%s (Mendix 11.14+): build #2 applied without mxcli restarting mxbuild --serve; "+
"if this mxbuild builds incrementally again, drop the 11.14+ branch\n%s", step, tailOf(docker.RunLogPath(mpr)))
}
}
}

// 1. detach: one line, exit 0, and the app answers.
Expand Down Expand Up @@ -205,11 +207,11 @@ func TestRunLifecycle_DetachStatusExecWaitStop(t *testing.T) {
if err := bg.Wait(); err != nil {
t.Fatalf("detach (boot race): %v\n%s", err, tailOf(docker.RunLogPath(mpr)))
}
// What this step claims is that the change is not lost: build #2 runs. On
// 11.14+ this build was measured applying (via restart) where step 4's failed;
// the likely difference is how much of the change the boot build had already
// read, which the test does not control. So either outcome passes there;
// "nothing to apply" never does.
// What this step claims is that the change is not lost: build #2 applies. On
// 11.14+ this build was measured applying without a serve restart where step
// 4's needed one; the likely difference is how much of the change the boot
// build had already read, which the test does not control. So the recovery is
// not required in the log here; "nothing to apply" never passes.
out, code = mxcli("run", "wait", "-p", mpr, "--timeout", "2m")
wantBuild2("a change made during the boot", out, code, true)
if out, code := mxcli("run", "stop", "-p", mpr); code != 0 {
Expand Down
9 changes: 9 additions & 0 deletions docs-site/src/reference/page/alter-page.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,15 @@ For page-level properties (like `Title`), omit the `ON` clause.

For pluggable widget properties, use quoted property names (e.g., `'showLabel'`).

A pluggable widget's **attribute** property is bound the way `CREATE PAGE` binds it.
A bare name (`set (eventDataAttribute: JsonData) on chart1`) belongs to the entity
the property binds to: the widget's own datasource when the property is linked to
one, otherwise the enclosing data container. `Module.Entity.Attr` must be an
attribute of that entity or of one of its generalizations. `$Param.Attr` reads an
attribute of a page or snippet parameter instead. A name the bound entity lacks
(CE1613), and another entity's qualified attribute (CE7247), are refused by
`check` and `exec` alike.

### INSERT BEFORE / INSERT AFTER

Inserts new widgets immediately before or after a named widget within its parent container. The new widgets use the same syntax as in `CREATE PAGE`.
Expand Down
55 changes: 55 additions & 0 deletions mdl-examples/bug-tests/1622-alter-set-pluggable-attribute.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
mdl 1;
-- ============================================================================
-- ALTER PAGE SET on a pluggable widget's attribute property (ako/mxcli#1622)
-- ============================================================================
--
-- Symptom (before fix), Mendix 11.12.5: every spelling was refused.
-- set (eventDataAttribute: JsonData) on chart1
-- → pluggable property "eventDataAttribute" holds a value of kind
-- Attribute, which ALTER cannot set from a plain value — rewrite the
-- widget with CREATE OR REPLACE PAGE (or ALTER PAGE REPLACE) instead
-- set (eventDataAttribute: $Ctx.EventData) on chart1
-- → Parse error: property eventDataAttribute takes a plain value, not an
-- expression
--
-- Fix: the mutator reports the property's kind, datasource link and scopes;
-- the executor binds the value by CREATE's rule and hands it the resolved
-- AttributeRef (+ SourceVariable for `$Param.Attr`). check --references
-- dry-runs the same path. Measured on mxbuild 11.12.5:
-- after each SET below → 0 errors
-- bound back to JsonData with the $Ctx
-- SourceVariable left in place (fault) → CE7247
--
-- Usage:
-- mxcli exec mdl-examples/bug-tests/1622-alter-set-pluggable-attribute.mdl -p app.mpr
-- mxcli docker check -p app.mpr → 0 errors
-- mxcli -p app.mpr -c "describe page BugTest1622.ChartPage"
-- → eventDataAttribute: $Ctx.EventData
-- `set (eventDataAttribute: EventData) on chart1` is refused (CE1613), and
-- `set (eventDataAttribute: BugTest1622.ChartContext.EventData)` too (CE7247).
-- ============================================================================

create module BugTest1622;

create non-persistent entity BugTest1622.ChartContext (EventData: String(unlimited));
create non-persistent entity BugTest1622.ChartEntry (JsonData: String(unlimited), JsonLayout: String(unlimited));

create page BugTest1622.ChartPage (
Title: 'Chart',
Layout: Atlas_Core.Atlas_Default,
Params: ( $Ctx: BugTest1622.ChartContext, $Entry: BugTest1622.ChartEntry )
) {
dataview dvEntry (DataSource: $Entry) {
customchart chart1 (
dataAttribute: JsonData,
layoutAttribute: JsonLayout,
eventDataAttribute: JsonLayout
)
}
};

-- A bare name: an attribute of the data view's entity.
alter page BugTest1622.ChartPage { set (eventDataAttribute: JsonData) on chart1; };

-- An attribute of the other page parameter.
alter page BugTest1622.ChartPage { set (eventDataAttribute: $Ctx.EventData) on chart1; };
Loading
Loading