fix(docker): stop docker build converting MPRv2 projects to MPRv1 (#808) - #64
Merged
Conversation
…endixlabs#808) `mx update-widgets` rewrites an MPRv2 project into the self-contained MPRv1 storage format: it inlines every unit into the .mpr (adding a Unit.Contents column) and deletes mprcontents/. A command that checks or builds must not mutate the source project's on-disk format — doing so silently desyncs the working tree from a Git repository that tracks the mprcontents/ files, breaks a running `mxcli run --local` watch loop, and has been observed to leave Studio Pro unable to open the project. mendixlabs#763 reported this for `docker check` and PR mendixlabs#764 fixed it there, by wrapping the invocation inside Check with a snapshot/restore. But build.go carried its own bare copy of the same invocation, so the conversion continued through `mxcli docker build`, `docker run` and `docker reload` — all three reporting success while rewriting the project. The guard now lives on the operation rather than on a call site. runUpdateWidgets snapshots the v2 storage, runs the step, and returns the restore func for the caller to defer; both call sites are one line and there is exactly one place in the tree that may exec `update-widgets`. A third caller cannot reintroduce the bug by forgetting the snapshot. Restore stays deferred rather than immediate: the caller's `mx check` and MxBuild have to see the widget-normalized model, or the false CE0463 errors the step exists to suppress come straight back. Only the on-disk format is put back, after the caller is done with the model. Note on scope: the issue reports `mxcli test` as affected, giving this more exposure than mendixlabs#763. It is not — update-widgets sits inside build.go's `if !opts.SkipCheck` block and testrunner invokes `docker build --skip-check`. `run --local` is also unaffected; it goes through mxserve and never calls update-widgets. The three affected commands are `docker build`, `docker run` and `docker reload`, all at their default settings. Tests: unit coverage for the four paths through runUpdateWidgets (v2 conversion undone; v1 needs no snapshot and restore is a no-op; an unsnapshottable v2 project skips the step entirely rather than risking an unrecoverable conversion; a failed step still restores). Removing the protection fails three of the four. Plus an integration counterpart to TestCheck_PreservesMPRv2StorageFormat that drives Build with DryRun — which stops right after the check step, exercising the whole buggy path without paying for a full MxBuild. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012XR649rKk68z6gBpngu6MA
ako
pushed a commit
that referenced
this pull request
Aug 1, 2026
Resolves the recurring conflict in .claude/skills/fix-issue.md: every fix appends a symptom row at the same anchor, so any two branches that both add one collide. Both sides kept — this branch's mendixlabs#812 row plus the two loop rows that arrived from #63. Main has since gained #64 (runUpdateWidgets) and #66 (mandatory microflow semicolons). Parser regenerated against the new grammar; full suite green, and the example corpus is unchanged at 235/39 including this branch's repro script.
ako
pushed a commit
that referenced
this pull request
Aug 1, 2026
Same recurring conflict as #68: .claude/skills/fix-issue.md, where every fix appends a symptom row at the identical anchor. Both sides kept. check.go auto-merged cleanly — #64's runUpdateWidgets and this branch's localMxForVersion touch different functions. Parser regenerated against #66's grammar; full suite green.
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.
Fixes mendixlabs#808.
The bug
mx update-widgetsrewrites an MPRv2 project into the self-contained MPRv1 storage format — it inlines every unit into the.mpr(adding aUnit.Contentscolumn) and deletesmprcontents/. A command that checks or builds must not mutate the source project's on-disk format: doing so silently desyncs the working tree from a Git repository that tracks themprcontents/files, breaks a runningmxcli run --localwatch loop, and has been observed to leave Studio Pro unable to open the project.mendixlabs#763 reported this for
docker check, and mendixlabs#764 fixed it there by wrapping the invocation insideCheckwith a snapshot/restore.build.gocarried its own bare copy of the same invocation, so the conversion continued throughdocker build,docker runanddocker reload— all three reporting success while rewriting the project.Reproduced against a real
mx, then verified fixedTestBuild_PreservesMPRv2StorageFormatscaffolds a project withmx create-project(which produces MPRv2), runsBuild, and asserts the storage format survived. Run against the pre-fixbuild.gowith MxBuild 11.12.2 present:and with this PR applied:
Neither skipped — each takes ~34s because
mx create-projectgenuinely runs. So the bug is reproduced end-to-end, the fix is verified end-to-end, and the test is proven non-vacuous.The fix
The guard now lives on the operation rather than on a call site. New
cmd/mxcli/docker/update_widgets.goholdsrunUpdateWidgets, which snapshots the v2 storage, runs the step, and returns the restore func for the caller to defer. Both call sites become one line, and there is exactly one place in the tree that may execupdate-widgets—grep -rn '"update-widgets"'returns a single hit. A third caller cannot reintroduce the bug by forgetting the snapshot.Restore stays deferred rather than immediate: the caller's
mx checkand MxBuild have to see the widget-normalized model, or the false CE0463 ("widget definition changed") errors the step exists to suppress come straight back. Only the on-disk format is put back, once the caller is done with the model. InBuildthat means the deferred restore fires at function exit, so MxBuild sees the normalized model too.snapshotStorageFormatandupdateWidgetsPathArgmoved fromcheck.gointo the new file unchanged — they were never check-specific.Scope correction
The issue states
mxcli testis affected, and that this gives the bug more exposure than mendixlabs#763. It does not:update-widgetssits insidebuild.go'sif !opts.SkipCheckblock, andtestrunner/runner.go:150invokesdocker build --skip-check.run --localis also unaffected — it goes throughmxserveand never callsupdate-widgets.mxcli docker build(default)mxcli docker run(default)mxcli docker reload(default)mxcli test--skip-checkmxcli run --localThree commands at their default settings, so still full severity; the fix just shouldn't be justified on a
mxcli testrepro that won't reproduce.Unit coverage
Four paths through
runUpdateWidgets, with a stubbed mx invocation:update-widgetsstill restores — it may have converted the project before failingRemoving the protection fails three of the four; the v1 case correctly still passes.
Docs
Updated the existing
.claude/skills/fix-issue.mdsymptom row rather than adding one — it already covered this symptom but pointed atcheck.go … + build.goas though mendixlabs#764 had covered both. Corrected, with the generalisable lesson recorded: when guarding a mutating external step, put the guard in a function that also performs the step, so a new caller cannot get it wrong.No
mdl-examples/bug-tests/fixture — this is a CLI/docker path bug with no MDL expression, matching the precedent set by mendixlabs#763.🤖 Generated with Claude Code
https://claude.ai/code/session_012XR649rKk68z6gBpngu6MA