Skip to content

fix(docker): stop docker build converting MPRv2 projects to MPRv1 (#808) - #64

Merged
ako merged 1 commit into
mainfrom
claude/docker-build-mprv2-808
Jul 31, 2026
Merged

fix(docker): stop docker build converting MPRv2 projects to MPRv1 (#808)#64
ako merged 1 commit into
mainfrom
claude/docker-build-mprv2-808

Conversation

@ako

@ako ako commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Fixes mendixlabs#808.

The bug

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 mendixlabs#764 fixed it there by wrapping the invocation inside Check with a snapshot/restore. build.go carried its own bare copy of the same invocation, so the conversion continued through docker build, docker run and docker reload — all three reporting success while rewriting the project.

Reproduced against a real mx, then verified fixed

TestBuild_PreservesMPRv2StorageFormat scaffolds a project with mx create-project (which produces MPRv2), runs Build, and asserts the storage format survived. Run against the pre-fix build.go with MxBuild 11.12.2 present:

--- FAIL: TestBuild_PreservesMPRv2StorageFormat (36.30s)
    Build converted the project to 1; the MPRv2 storage format must be preserved (#808)
    mprcontents/ missing after Build, storage format was not preserved

and with this PR applied:

--- PASS: TestBuild_PreservesMPRv2StorageFormat (34.10s)
--- PASS: TestCheck_PreservesMPRv2StorageFormat  (34.45s)

Neither skipped — each takes ~34s because mx create-project genuinely 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.go holds runUpdateWidgets, 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 exec update-widgetsgrep -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 check and 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. In Build that means the deferred restore fires at function exit, so MxBuild sees the normalized model too.

snapshotStorageFormat and updateWidgetsPathArg moved from check.go into the new file unchanged — they were never check-specific.

Scope correction

The issue states mxcli test is affected, and that this gives the bug more exposure than mendixlabs#763. It does not: update-widgets sits inside build.go's if !opts.SkipCheck block, and testrunner/runner.go:150 invokes docker build --skip-check. run --local is also unaffected — it goes through mxserve and never calls update-widgets.

Path Affected
mxcli docker build (default) yes
mxcli docker run (default) yes
mxcli docker reload (default) yes
mxcli test no — always passes --skip-check
mxcli run --local no — mxserve path

Three commands at their default settings, so still full severity; the fix just shouldn't be justified on a mxcli test repro that won't reproduce.

Unit coverage

Four paths through runUpdateWidgets, with a stubbed mx invocation:

  • a v2 conversion is undone by restore
  • a v1 project needs no snapshot, the step still runs, and restore is a harmless no-op
  • a v2 project whose storage cannot be snapshotted skips the step entirely rather than risk an unrecoverable conversion (a CE0463 false positive is the lesser evil), and reports the skip
  • a failed update-widgets still restores — it may have converted the project before failing

Removing the protection fails three of the four; the v1 case correctly still passes.

Docs

Updated the existing .claude/skills/fix-issue.md symptom row rather than adding one — it already covered this symptom but pointed at check.go … + build.go as 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

…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
ako merged commit e246032 into main Jul 31, 2026
3 checks passed
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants