sync: settings/configuration and page fixes (#759, #760, #762, #813), plus merge and CI guards - #818
Merged
Conversation
The nightly matrix runs integration tests against 10.24.19.104498, 11.6.6 and 11.12.0. TestBuild_PreservesMPRv2StorageFormat, added with the mendixlabs#808 fix, failed on the 10.24 row only: Build failed: portable app distribution requires Mendix >= 11.6.1, found 10.24.19.104498 10.24 is MPRv2 (v2 storage arrived in 10.18), so the test's storage-format precondition passed and it went on to call Build, which refuses below 11.6.1 before it ever reaches the update-widgets step the test is about. The failure is a property of the matrix row, not a regression in the fix. The 11.x rows are unaffected, which is why only one job went red. Adds the missing precondition. This is a real capability gate rather than a masked failure: there is no portable app distribution to protect on 10.x, and TestCheck_PreservesMPRv2StorageFormat has no version guard, so MPRv2 preservation is still covered on every matrix row — it passes on 10.24 today. Reproduced and verified against real MxBuild installs rather than reasoned about: only 10.24 cached, before -> FAIL (the exact CI error) only 10.24 cached, after -> SKIP, and Check still PASSes only 11.x cached, after -> PASS (39s — the test still genuinely runs) The last of those matters most: a version gate that silently disabled the test everywhere would be worse than the failure it fixes. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_012XR649rKk68z6gBpngu6MA
…he repo MXCLI_HUB_KEY is a credential and a gitignored file wouldn't survive container recycling anyway — make it explicit that the hub key is set on the Claude Code environment, not committed. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
…s#762, mendixlabs#813) Two DataView properties parsed, passed `mxcli check`, and were then silently discarded. Different causes, identical symptom. mendixlabs#762 — FormOrientation: Vertical had no effect on the DEFAULT (modelsdk) engine, while working under --engine legacy. Studio Pro's "Form orientation" radio has no BSON field of its own: it IS LabelWidth (0 = Vertical, 3 = Horizontal, Mendix's metamodel default). Only the legacy writer performed that translation; the modelsdk writer emitted LabelWidth solely when an explicit `LabelWidth:` was given, so the orientation was read into the model and dropped. That is the mendixlabs#812 shape again — a field set on the model that no writer on the active engine reads. The derivation now lives on the model (pages.DataView.ResolvedLabelWidth) and both writers call it, so they cannot drift apart again; the modelsdk writer emits LabelWidth unconditionally. mendixlabs#813 — showFooter was not settable at all. ShowFooter was only ever set implicitly, by the presence of a `footer { … }` block, so there was no way to show an empty footer or to declare footer widgets that start hidden. The property sits in the validator's allow-list, so it parsed clean and was thrown away. It is now read explicitly and wins over the block in both directions; hiding a footer does not discard its widgets. A trap worth recording: WidgetV3.GetBoolProp is case-SENSITIVE and accepts only a real bool, unlike GetStringProp which is case-insensitive. My first attempt found the key and still read false, so the value is now coerced from the looked-up value and a nonsense one is refused rather than defaulting to false — defaulting is the failure mode this whole fix is closing. DESCRIBE emits ShowFooter only when the implicit rule would not reproduce the stored value, so a plain footer block still round-trips as a block. Verified end-to-end against a real project, both engines producing identical BSON: LabelWidth [0, 3, 5, 3] (Vertical, Horizontal, explicit 5, default) ShowFooter [false, false, false, true] and DESCRIBE round-trips all four forms. Reverting either fix fails the new tests: 1 case for mendixlabs#762, 6 for mendixlabs#813. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_012XR649rKk68z6gBpngu6MA
mendixlabs#760) Every mxcli-authored page carried a container nobody asked for. The builder wrapped each non-empty layout placeholder in a Forms$DivContainer named "conditionalVisibilityWidget<N>", so creating a single button produced a button AND a container, visible in the Studio Pro widget tree of every page mxcli wrote. The wrapper was never a BSON requirement. Forms$FormCallArgument carries a `Widgets` array and a Studio Pro page fills it with its top-level widgets directly — both writers already emitted an array containing exactly one element. It existed only because pages.LayoutCallArgument declared a single `Widget` field, so N widgets had to be squeezed through a 1-widget hole. Verified against Mendix's own output rather than inferred: Administration.Account_Overview in a `mx create-project` 11.12.2 app has two top-level widgets in one placeholder (a DivContainer and a LayoutGrid) and zero conditionalVisibilityWidget wrappers anywhere. That also corrects a comment of ours which claimed the wrapper is what "mxcli (and Studio Pro) adds as a layout placeholder container". Studio Pro uses that name when wrapping a widget for conditional visibility, not for placeholders — the same reasoned-by-analogy error that produced mendixlabs#812. Three places already worked around the container: DESCRIBE unwrapped it to avoid "a phantom CONTAINER wrapping all widgets", and the catalog builder skipped it as "transparent". Those readers are deliberately KEPT — projects authored before this fix still contain wrappers, and must keep reading correctly. This changes what we write, it does not retro-clean existing pages. LayoutCallArgument.Widget becomes Widgets []Widget; the builder places the authored widgets directly. Seven call sites across the two writers, the MCP backend, the builder and one example. Verified end-to-end on a real project, identical on both engines: one button -> placeholder widgets [Forms$ActionButton] 0 wrappers three widgets -> [Forms$DynamicText, Forms$ActionButton, Forms$DivContainer] (the DivContainer is the one the author wrote) 0 wrappers mx check -> "The app contains: 0 errors." DESCRIBE -> round-trips to the authored MDL Reverting the writer's widget list fails the new test with the reported symptom. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_012XR649rKk68z6gBpngu6MA
…abs#759) CREATE CONFIGURATION reported success and `mx check` passed, but Studio Pro threw on the next open of the changed unit: System.InvalidOperationException: Sequence contains no matching element at ...MprProperty.cs:line 25 mxbuild does not catch this — its deserializer tolerates unknown properties, while Studio Pro resolves each stored property against the type's property list. Diffing the Settings$ProjectSettings unit before and after the command, against a project `mx create-project` produced, found three defects: 1. DatabaseType was hardcoded "HSQLDB". The enumeration member is "Hsqldb"; nothing matches "HSQLDB". User-supplied values were not validated either, so 'postgres' went to disk unchanged. 2. The codec engine read the ports through the gen accessors, which bind the SDK names RuntimePortNumber / AdminPortNumber, while Studio Pro stores HttpPortNumber / ServerPortNumber. The read returned 0 for both and the overlay wrote that 0 back, so every settings write silently reset the ports of every existing configuration (Default lost http=8080, server=8090). 3. Mendix renamed the runtime Java version property between 11.6 ("JavaVersion" = "Java21") and 11.12 ("JavaMajorVersion" = "21"). mxcli wrote the 11.6 name unconditionally, so on an 11.12 project a settings write left JavaMajorVersion stale and added a JavaVersion property that version's metamodel does not define — the property lookup that fails. Fixes: - settingsDatabaseType canonicalises the value against the metamodel enum (case-insensitively) and rejects anything else, in the executor and in `mxcli check` (MDL-SET03, wired through typedSettingsKeys so the existing drift guard covers it). New configurations default to Hsqldb and both of Studio Pro's ports (8080 / 8090). - The codec read takes the ports from the stored keys, keeping the gen accessors as fallback. - settingsoverlay.JavaVersionKey / JavaVersion / SetJavaVersion read and write the Java version under whichever key the document already carries, and never invent one. Both engines use them. The same reasoning removes the hardcoded `Tracing: nil` from the no-sibling fallback — 11.12 spells it OpenTelemetry, and with no sibling to copy there is no way to tell which is expected. Verified on real projects at both spellings (11.6.6 and 11.12.2): the Settings$ProjectSettings diff after `create configuration` is now purely additive on both, both write engines produce identical output, and `mx check` passes. Each fix was mutation-checked — reverting it makes the new test fail with the reported symptom. Refs mendixlabs#759
A constant override's value is stored one of two ways. Settings$SharedValue
carries a "Value" and lives in the model, so it is in version control and every
developer gets it. Settings$PrivateValue is a marker type with no properties at
all: the value is on the developer's own workstation, deliberately kept out of
the repository (the usual choice for development API tokens).
The overlay assumed every stored SharedOrPrivateValue was a SharedValue and
assigned cv.Value into it. For a private override cv.Value is always "" — the
value is not in the model — so any settings write produced:
{"$Type": "Settings$PrivateValue", "Value": ""}
a property that type does not define. Studio Pro resolves each stored property
against the type's property list and throws on open:
System.InvalidOperationException: Sequence contains no matching element
at ...MprProperty.cs:line 25
the same failure shape as mendixlabs#759. mxbuild does not catch it — `mx check` passes on
the corrupted document. The blast radius is wider than a local mistake:
configurations are shared in version control, so one developer running any
ALTER SETTINGS or CREATE CONFIGURATION corrupted every developer's private
overrides and pushed the result.
Separately, the read type-asserted to *SharedValue, failed, and returned "" with
no way to tell private from empty — so `describe settings` rendered the override
as `value ''`, and replaying describe's own output converted it into a *shared*
empty override, publishing into git a value the developer chose to keep local.
The shared/private choice belongs to the constant; configurations must just
respect it. So mxcli now carries the distinction and preserves it without ever
authoring it:
- model.ConstantValue.IsPrivate, set by both read paths (codec matches on the
child's type name rather than its Go type, since the registry may decode the
marker to a bare element.Base).
- The overlay leaves a PrivateValue node byte-identical.
- `describe settings` emits a comment instead of a re-executable statement.
- `alter settings constant ... value ...` on a private override is refused, with
a pointer to change it in Studio Pro first. `drop constant` is still allowed —
it discards the whole override, which is what was asked for.
Verified on a real 11.12.2 project with a Studio-Pro-shaped private override:
an unrelated port change leaves the node as {"$Type": "Settings$PrivateValue"}
with no Value key, on both write engines, and `mx check` passes. All four guards
were mutation-checked — reverting each makes its test fail with the reported
symptom.
Follows the private-override fix with the reporting and documentation half. SHOW CONSTANT VALUES rendered a private override as an empty cell, which is indistinguishable from an override deliberately set to the empty string. Since the value genuinely is not in the project, report "(private)" instead. Documentation, covering each surface the constant/configuration story touches: - mxcli syntax domain-model.constant.create — a "Shared vs private values" section stating the rule and what each statement does with a private override. It lives on the constant topic, not the settings topic, because the choice belongs to the constant. - docs-site create-constant.md — the same, as a table. This page already told readers to use an empty default "for secrets"; private values are the actual mechanism, so it now says so. - docs-site show-settings.md — what the (private) marker means. - MDL_QUICK_REFERENCE.md — a note under Constants. - skills/mendix/project-settings.md — the SHOW CONSTANT VALUES behaviour alongside the describe/alter/drop rules. CLAUDE.md gains the invariant both this bug and mendixlabs#759 come from, since it applies to any future overlay write and neither `mx check` nor the build catches a violation: write only keys the document already carries (property names are version-specific — JavaVersion/JavaMajorVersion, Tracing/OpenTelemetry), and dispatch a polymorphic child on $Type before assigning fields, because variants can differ in arity (SharedValue carries a value, PrivateValue is a bare marker). Plus the sibling trap: validate enum-valued properties against generated/metamodel rather than passing a user string through. The (private) marker is mutation-checked.
…lder-wrapper-760 # Conflicts: # .claude/skills/fix-issue.md
…guration-759 # Conflicts: # .claude/skills/fix-issue.md
…nt-values # Conflicts: # .claude/skills/fix-issue.md
… sessions The overview was a flat list of live endpoints that vanished ~10 min after a container went idle. Group them by the Claude Code session that exposed them, and keep past sessions visible. Client (`mxcli run --hub`): - Auto-detects the session id from CLAUDE_CODE_REMOTE_SESSION_ID (fallbacks: MXCLI_HUB_SESSION, CLAUDE_CODE_SESSION_ID) and sends it on registration. Override with `--hub-session`. Hub: - Backends carry a Session; a new /api/sessions groups endpoints by session, deriving the claude.ai/code conversation URL from the remote id so each session links back. - A durable SessionLog (cmd/mxcli/tunnelhub/sessions.go) records every endpoint a session exposed. On reap/deregister the live entry is stamped into it, so the overview keeps offline sessions (state: available → stale → offline). Persisted to `--sessions-file` (default ~/.mxcli/hub-sessions.json, survives restarts), pruned after `--session-retention` (default 30d). Atomic 0600 writes, mirroring the key store. - Overview page (`/`) regrouped into per-session cards, each linking to its claude.ai conversation and listing its endpoints; viewer-scoped like /api/backends. Tests: SessionLog persist+prune+upsert, Registry.Sessions grouping/offline retention/viewer scoping, sessionURL derivation, client session-id precedence. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
…guration-759 # Conflicts: # .claude/skills/fix-issue.md
…driver Every bug fix appends a row to the symptom table in .claude/skills/fix-issue.md, so two concurrent fixes always collide on the same line — five resolution rounds in one week. Moving the insertion point from the top of the table to the bottom did not help: both sides still append to the same place, so the collision simply moved with it (PRs #76, #77 and #78 all hit it). git's built-in "union" merge driver keeps BOTH sides of a conflicting hunk instead of raising a conflict. That is exactly right here: the file is only ever appended to, and it is looked up by matching a symptom rather than read in order, so row order carries no meaning. Verified: two branches that each append a row now merge with no conflict and both rows present. Caveat: union applies to the whole file, so two branches editing the same *prose* line would silently keep both instead of conflicting. The failure is a visible duplicated line, not corruption. Splitting the table into its own file — so union covers only append-only content — is the follow-up, deferred because that restructure would conflict with the three open PRs that all touch this table.
The union-merge commit wrote .gitattributes from scratch, dropping the 53 lines of line-ending normalization already there (* text=auto, *.go text eol=lf, the binary declarations). Restore them and append the union rule instead, so the change is purely additive.
…nt-values # Conflicts: # .claude/skills/fix-issue.md
Every bug fix appends a row to the symptom table, so two concurrent fixes always collide on the same line — and moving rows from the top of the table to the bottom did not help, because both sides still append to the same place. git's built-in union driver keeps both sides instead of conflicting, which is correct for a file that is only appended to and looked up by matching a symptom. Appended to the existing rules, not replacing them.
The rationale in fix-issue.md and CLAUDE.md claimed that appending at the end of the table avoids the conflict two concurrent fixes cause. It does not — both sides still append to the same line, which is why #76, #77 and #78 each hit it again after that convention was adopted. Merging is handled by the merge=union driver in .gitattributes (added in #77). Appending at the end is kept for readable diffs and rough chronology, which is a human reason, not a git one.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Sync of
ako/mxcli:maininto upstream. Four reported issues fixed, each verifiedagainst a real Mendix project rather than by inspection, plus infrastructure fixes
found along the way.
Settings and configuration
Runtime Configuration ( Create or Update ) are not fully supported #759 — runtime configuration create/update writes properties Mendix cannot resolve.
Studio Pro threw
System.InvalidOperationException: Sequence contains no matching elementatMprProperty.cs:25whilemx checkpassed (mxbuild tolerates unknownproperties; Studio Pro does not). Three defects, not one:
DatabaseTypewas hardcoded"HSQLDB"— the enum member isHsqldb, anduser-supplied values were never validated.
RuntimePortNumber/AdminPortNumberwhile Studio Pro storesHttpPortNumber/ServerPortNumber,so the read returned 0 and the overlay wrote it back — any settings write
silently reset the ports of every configuration.
JavaVersion="Java21")and 11.12 (
JavaMajorVersion="21"); mxcli wrote the 11.6 name unconditionally,leaving the real value stale and adding a property 11.12 does not define.
Enum values are now canonicalised against
generated/metamodeland rejectedotherwise (executor and
mxcli check, ruleMDL-SET03); version-renamedproperties are read from and written back to the key the document already carries,
and never invented.
Private constant overrides were corrupted by any settings write.
Settings$PrivateValueis a marker type with no properties — the value lives onthe developer's workstation, out of version control. The overlay assumed
SharedValueand wroteValue: ""into it, producing the sameMprPropertyfailure. Since configurations are shared in git, one developer's unrelated edit
corrupted every developer's private overrides. mxcli now carries the distinction
(
model.ConstantValue.IsPrivate) and preserves it without ever authoring it:describe settingsemits a comment rather than a re-executable statement,alter settings constant … valueis refused on a private override,drop constantstill works, and
show constant valuesreports(private)instead of a blank cell.Pages
FormOrientationandshowFooterparsed, checked clean, andhad no effect. Two different causes:
FormOrientationwas a model field no writeron the active engine read, and
showFooterwas never read explicitly.conditionalVisibilityWidgetcontainer.
Forms$FormCallArgumentholds aWidgetsarray; the wrapper existedonly because the model declared a single
Widget. Verified against Studio Pro's ownoutput, which places top-level widgets directly in the placeholder.
Build and CI
where portable app distribution is unavailable.
merge=unionfor.claude/skills/fix-issue.md— every bug fix appends a row toone table, so concurrent fixes always collided on the same line. Moving rows from the
top of the table to the bottom did not help; git's union driver does, and the
stale guidance claiming otherwise is corrected.
Tunnel hub
Verification
Each fix was mutation-checked — reverting it makes its test fail with the reported
symptom — and confirmed end-to-end on real 11.6.6 and 11.12.2 projects with
mxcli docker checkreporting 0 errors. The settings fixes were validated by diffingthe
Settings$ProjectSettingsunit before and after each command against a projectmx create-projectproduced: the write must be purely additive, and now is on bothversions and both write engines.
Two limits worth stating: Studio Pro is not available in this environment, so the
reported exception could not be reproduced directly — the evidence is byte-level BSON
parity with Studio-Pro-authored output. And the port fix is a read-path workaround
rather than a corrected binding in
modelsdk/gen/settings/types.go, whose generatoris not in the repo