Skip to content

Sync ako/mxcli: git doctor/commit follow-ups to #1670, Mendix 9/10 writer gaps, module role description, test-runner false pass - #1675

Merged
ako merged 22 commits into
mendixlabs:mainfrom
ako:main
Oct 29, 2026
Merged

ako merged 22 commits into
mendixlabs:mainfrom
ako:main

Conversation

@ako

@ako ako commented Oct 29, 2026

Copy link
Copy Markdown
Collaborator

Syncs ako/mxcli:main into upstream: 18 commits from fork PRs ako#1764–ako#1767.

Follow-ups to #1670 (mxcli git commit / mxcli git doctor)

Four defects were reproduced while reviewing #1670. Each has its own commit and a test that fails before the fix.

  • git doctor judges notes only once the remote's notes are fetched. git clone never fetches refs/notes/*. So on any fresh clone or CI checkout, doctor reported every commit as having no note and exited 1, while the same report said the remote had the notes ref.
    • Now, an unfetched ref is the finding, and the report gives the git fetch command for it.
    • --fix fetches the ref as a fast-forward only, so a note that exists only locally is never dropped.
  • git doctor judges the configured remote URL, not the insteadOf rewrite. With a common global https → git@ rule, an HTTPS remote read as SSH. --fix then rewrote it on every run.
    • The check now reads remote.<name>.url, which is what --fix writes.
    • A different effective URL is reported as information that names the rule.
  • git commit --dry-run leaves what the user staged as it was. It used to undo its staging with git reset, which resets to HEAD and unstaged the user's own changes. It now stages into a temporary copy of the index.
  • git commit leaves out only the project's own build folders, not same-named folders nested elsewhere.
  • The git doctor tests no longer read the developer's git config. Two tests failed make test on any machine with a GitHub URL rewrite in its git config. CI has none, so it stayed green.

Mendix 9 / 10 writer gaps

These are the remaining rows found by the 9.24 doctype run. They were measured against mendixmodelsdk 4.117.0, a fresh 9.24.30 starter app, Workflow Commons 2.6.0, and mx convert with the property floor turned off.

  • A workflow's state-change handlers are completed on 9.24.
  • No workflow PersistentId is written below 10.21.
  • A constant override is written flat below 10.9.
  • A $currentObject flow argument is left out below 10.10.
  • AggregateFunction is declared on Mendix 9, and Reduce's fold is floored at 10.0.
  • create published odata service is gated at Mendix 10.4.
  • Docs: a wiki note says a version refusal isn't always an older shape.

Other fixes

  • create or modify module role without a description clause keeps the stored description. Before, it reported "Modified" and emptied it. description '' still clears it.
  • In mxcli test, a later passing @expect no longer clears an earlier failure. The suite used to report PASS and exit 0.
    • A sibling fix: on the after-startup runner, two tests that assign the same variable with lower-case call / create no longer collide with MDL063.

Contributor tooling

  • make fmt-check uses the toolchain's gofmt, not the one on PATH. With an older Go installed, make lint failed main on a file CI accepts, and passed one CI then rejected.
  • New make ci-local runs what CI's build-and-test job runs, in the same order.
  • New contributor skill: .claude/skills/steward/SKILL.md describes the loop from a PR being opened to it being merged. It isn't synced to user projects

claude and others added 22 commits October 28, 2026 17:49
#1753)

Symptom, from the issue: "On the after-startup test runner, a test with
more than one `@expect` is reported **PASS** when an earlier assertion
fails and a later one passes. `mxcli test` exits 0." Affected: every
Docker run and `--local --legacy-runner`; the endpoint runner is not.

Cause: writeExpectAssertion logs one verdict line per assertion,
MXTEST:PASS:<id> or MXTEST:FAIL:<id>:..., and ParseLogResults let every
line overwrite the test's status. The last line won, and the PASS case
also cleared the failure message. The generated flow returned
$AllPassed = false, but nothing reads it.

Fix: the parser folds a test's verdict lines with a sticky failure
(applyVerdictLine). Once a test has a FAIL or an ERROR, no later PASS or
SKIP line clears it. The first failure's message is reported, with
"(and N more failure(s) in this test)" when more followed. This matches
the endpoint runner, which evaluates an assertion only while $Verdict is
PASS and whose skipped-assertions guard never replaces a failure. The
skipped-assertions FAIL (#1448), logged after the PASS lines of the
assertions that ran, still wins over them: a sticky failure, not "first
line wins". A RUN line resets the fold, so a failure cannot outlive its
run. The fix is in the parser, so it also covers logs written by runners
generated before it. The generator is unchanged: logging one verdict per
test would change a flow whose verdicts were measured on 11.13.0, and it
would cover nothing more, because parser and generator ship together.

Measurement (unit level, the symptom's layer: the parser reads a log).
sticky_failure_test.go, run before the fix existed and again with
results.go reverted:

  --- FAIL: TestLaterPassDoesNotClearEarlierFailure
      FAIL then PASS: status = PASS, want FAIL
      FAIL then PASS: message = "", want "Expected $r = 1, actual: 2"
      FAIL then PASS: the suite passed, so `mxcli test` exits 0
  --- FAIL: TestFirstFailureMessageIsReported
      message = "Expected $r = 4, actual: 2", want "Expected $r = 1, actual: 2 (and 2 more failures in this test)"
  --- FAIL: TestLaterPassDoesNotClearEarlierError
      ERROR then PASS:test_1: got PASS "", want ERROR "Setup failed: Shop.Seed"

TestSkippedAssertionsFailureStillWins and the PASS-then-FAIL and
PASS-then-PASS cases pass both before and after; they are the controls.
With the RUN reset stubbed out, TestRunLineStartsVerdictAfresh fails.
mdl-examples/bug-tests/test-runner-1753-earlier-expect-fails.test.mdl is
the runtime reproduction. It was not run against an app for this change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
…he after-startup runner

Symptom, from #1753's sibling note: "on this runner,
`assignPattern` matches only upper-case `CALL|CREATE`. So mdl 1's
`$r = call …` is not renamed per test, and two tests that both use `$r`
fail injection (MDL063 / CE0111, measured)." Affected: every Docker run
and `--local --legacy-runner`. The endpoint runner gives each test its own
flow and does not rename.

Cause: the legacy runner compiles every test into one microflow, so it
gives each test's variables a `_N` suffix. assignPattern decides which
variables are renamed, and it matched only `CALL|CREATE` in upper case.
MDL keywords are case-insensitive, and mdl 1 and `fmt` write them in
lower case, so `$r = call microflow …` stayed `$r` in every test.

Fix: assignPattern matches `call|create` in any case, followed by a word
boundary so `$r = callback` does not count. No other pattern in
cmd/mxcli/testrunner makes the upper-case assumption: the rest lower-case
their input or use EqualFold.

TestThrowsCatchesEveryCallKind and TestVerdictCatchesEveryCallKind
matched the legacy output against the unrenamed lower-case statement, so
they passed only because of the defect. Their legacy assertions now
expect the renamed statement (legacyStmt).

The 1448 bug-test's note that the legacy runner "needs a distinct
variable name per test" is updated.

Measurement. rename_keyword_case_test.go with the old pattern (the
upper-case control subtest passes):

  --- FAIL: TestLegacyRunnerRenamesLowerCaseAssignments/call_microflow,_mdl_1
      test 1: no "$r_1 = call microflow" — the assignment was not renamed
      an assertion still reads the unrenamed $r
      MDL063: '$r' is created twice in this microflow — first by call
      microflow, then by call microflow. ... mxbuild rejects this with
      CE0111 "Duplicate variable name"
  (and the same for mdl 0, call java action, create object, create list,
  mixed case)
  --- FAIL: TestAssignPatternNeedsAWholeKeyword
      control: $r = call microflow was not picked up: map[]

I also swept every non-.fail `.test.mdl` in mdl-examples/ (16 files)
through GenerateTestRunner and executor.ValidateProgram. Before the fix,
10 files drew MDL063, among them the 1448, 1514 and 923 bug-tests and
doctype-tests/microflow-spec (86). After it, 2 do. Those 2 come from
`retrieve $Brands` and from `retrieve $Request` / `$Request = head(…)`.
They are a separate gap: assignPattern has never matched `retrieve`,
`declare`, list operations, `send rest request` or
`execute database query` in any case. This change does not address it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
… omits it

Symptom (#1218): a bare `create or modify module role M.R;`
reported "Modified module role: M.R" and erased the role's stored
description. `describe` -> `exec` was unaffected because describe prints
the description; grants were untouched.

Cause: CreateModuleRoleStmt.Description was a plain string, so "no
description clause" and `description ''` were the same value, and the OR
MODIFY branch of execCreateModuleRole passed it to AddModuleRole, which
overwrites the existing role in place.

Fix: Description is a *string, nil when the statement has no clause, as
CreateUserRoleStmt's properties have been since #707. OR MODIFY
writes the stored description when it is nil, so the write is elided
and the run reports "Unchanged"; `description ''` clears it. Adopting an
auto-provisioned role still drops its marker description. Docs (skill,
docs-site reference, `mxcli syntax`) state the rule.

Measurement:
- Unit test (mdl/executor/issue1218_module_role_description_test.go)
  fails before the fix, and with the fix forced back, with the reported
  symptom: description written = "", want "Read the published OData
  services. Nothing else." / Modified module role: Shop.ApiUser.
- Real app, Mendix 11.13.0: role created with a description; bare
  statement with the fixed build -> "Unchanged module role", describe
  keeps the description, describe -> exec "Unchanged"; with the pre-fix
  build -> "Modified module role" and the description is empty.
  `description ''` with the fixed build clears it. mx check: 0 errors in
  every state, the same as the blank app, fault included.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
…fold at 10.0

Symptom (#1163, 02-microflow-examples on 9.24.30): every
count/sum/average/minimum/maximum in a microflow was refused with
"this Mendix 9.24.30 project cannot store
Microflows$AggregateAction.AggregateFunction (Mendix 10.0.0+)".

Cause: not an older storage shape. The vendored generated version data
gave aggregateFunction the row of reduceReturnDataType
({Introduced 10.0.0, Required}) and had no row for reduceReturnDataType,
so the property floor refused the key every aggregate carries and let the
fold's return type through unrefused.

Evidence: mendixmodelsdk 4.117.0 has aggregateFunction with no version
(its default even changes at 9.24.0) and reduceReturnDataType introduced
10.0.0. Studio Pro 9.24.0 stores AggregateFunction on 10 of 10 aggregates
in Workflow Commons 2.6.0. A floor-off build's aggregate survives
`mx convert` 9.24.30 with every key.

Fix: correct the Microflows$AggregateAction row by hand (gen cannot be
regenerated here) and regenerate the copy in modelsdk/version.

Measured: bug-test 1163-aggregate-on-mendix9.mdl on a fresh 9.24.30 app:
exec writes all five aggregates, mx check 0 errors, mx convert strips
nothing. Before, the first was refused. Control: the new tests fail on
the unfixed table with the reported refusal. 02-microflow-examples on
9.24 now stops at the expression aggregate (`sum … of …`, UseExpression
and Expression, Mendix 10.0+), which is correct.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
Symptom (#1163, 25-rename on 9.24.30): every workflow was
refused with "this Mendix 9.24.30 project cannot store
Workflows$Workflow.PersistentId (Mendix 10.21.0+)". The same held on
10.0-10.20.

Cause: the workflow writer, and the ALTER WORKFLOW mutator for the
activities it inserts, mint a fresh PersistentId on the workflow and on
every activity, outcome and boundary event. mendixmodelsdk introduces
PersistentId in 10.21.0 on all four (Workflow, and the abstract
WorkflowActivity, Outcome and BoundaryEvent). The floor refused the
workflow's own. It never saw the others, which are declared on supertypes
it does not look up. `mx convert` 9.24.30 strips the PersistentId from
all 6 element types a floor-off build wrote.

Fix: canon.LowerToProjectShape drops PersistentId from every Workflows$
element below 10.21. It runs on every unit write before the floor, so
CREATE, CREATE OR MODIFY and wfmutator's raw inserts are all covered.
The ID is dropped rather than refused because the writer mints it; no
script states it, and Studio Pro 9.24 stores none.

Measured: bug-test 1163-workflow-on-mendix9.mdl on a fresh 9.24.30 app:
exec ok, mx check 0 errors, `mx convert -p` strips no key. 25-rename on
9.24: FAIL (the refusal) -> PASS. Controls: 25-rename and
24b-workflow-mendix10 PASS on 10.24.24 and 11.13.0. The unit test fails
on the unfixed code with the reported refusal.

Not fixed: a parallel split writes Workflows$EndOfParallelSplitPathActivity,
a 10.21 type, and 9.24 then does not load the project.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
Symptom (found while measuring #1163): with PersistentId
lowered, `mx convert -p` 9.24.30 strips nothing from an mxcli-written
workflow but adds WorkflowOnStateChangeEvent: null and
UsertaskOnStateChangeEvent: null. Studio Pro's next save then differs in
property names, which Mendix's merge engine refuses to compare (#1373).

Cause: the two defaults (mendixmodelsdk: introduced 9.12.0, removed
11.0.0) were measured on 10.24.24 only, and an entry is added only from
the lowest version it was measured on.

Fix: lower their measured floor to 9.24.30, where `mx convert` adds both
with the same null value.

Measured: bug-test 1163-workflow-on-mendix9.mdl on a fresh 9.24.30 app:
mx check 0 errors, and `mx convert -p` now neither strips nor adds a key
on the workflow. Before, it added these two. The test fails on the
previous floor (present false on 9.24). The 11.13 case stays absent as
the control.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
Symptom (#1163, 14-project-settings on 9.24.30):
`alter settings constant @M.C value '...' in configuration 'Default'`
was refused with "this Mendix 9.24.30 project cannot store
Settings$ConstantValue.SharedOrPrivateValue (Mendix 10.9.0+)". The same
held on 10.0-10.8.

Cause: the settings overlay writes a new override as a Settings$SharedValue
nested under SharedOrPrivateValue. From 10.9 that is the only shape Mendix
reads. Below 10.9 an override is {$ID, $Type, ConstantId, Value} with a
flat string (mendixmodelsdk: Value deleted and SharedOrPrivateValue
introduced in 10.9.0). With the floor off, `mx convert` 9.24.30 turns the
nested form into Value: "" and the override silently becomes empty.

Fix: canon.LowerToProjectShape replaces SharedOrPrivateValue with the flat
Value in its place, or fills the empty flat sibling the overlay leaves.
A Settings$PrivateValue has no flat form, so it is refused, naming the
constant. The reader already preferred a flat Value; a backend test now
pins reading and updating a 9.x flat override.

Measured: bug-test 1163-constant-override-on-mendix9.mdl on a fresh
9.24.30 app: describe settings shows 'broker:9092', mx check 0 errors,
and mx convert keeps the value and changes no key in the settings unit.
14-project-settings on 9.24: FAIL (the refusal) -> PASS. Controls:
10.24.24 and 11.13.0 PASS. The unit tests fail on the unfixed code with
the reported refusal.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
Symptom (#1163, 29-datagrid DG12 on 9.24.30): a page passing
`$currentObject` to a microflow or nanoflow was refused with
"this Mendix 9.24.30 project cannot store
Forms$MicroflowParameterMapping.Expression (Mendix 10.10.0+)". The same
held on 10.0-10.9.

Cause: bindParameterMappingValue writes such an argument as Expression
text with Variable null. Expression is 10.10.0 on both mapping types.
Before 10.10 a mapping holds only Variable, a Forms$PageVariable that is
required there. Measured with the floor off: 9.24 does not load the page
at all ("An error occurred when trying to set the 'Variable' property of
a Microflow parameter mapping ... Value cannot be null"). Studio Pro 9
stores no mapping for an argument that is the enclosing object; it
infers it. The 9.24.30 starter app and Workflow Commons 2.6.0 (9.24.0)
have 82 flow calls taking 1-2 parameters and no mapping on any of them.

Fix: below 10.10, canon.LowerToProjectShape leaves a $currentObject (or
empty) argument out of every ParameterMappings list. An argument bound
through Variable is kept; the floor drops its empty Expression. Any other
expression has no form there, so it is refused, naming the parameter.

Measured: bug-test 1163-flow-arguments-on-mendix9.mdl on a fresh 9.24.30
app: exec ok, mx check 0 errors, mx convert changes no key on the page,
and describe -> exec is Unchanged. 29-datagrid on 9.24 now stops at DG15
(page variables, 10.17, a correct refusal); the DG2 NullReferenceException
in mx check is the separate CE0463 gap. Controls: 29-datagrid PASS on
10.24.24 and 11.13.0. The unit tests fail on the unfixed code with the
reported refusal.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
Symptom (#1163, 10-odata and 18-folder on 9.24.30): a published
OData service was refused at write time with "this Mendix 9.24.30 project
cannot store ODataPublish$PublishedODataService2.IncludeMetadataByDefault
(Mendix 10.8.0+)". By then the script's earlier statements were already
written.

Cause: not the property. mxcli writes the service as
ODataPublish$PublishedODataService2, a document type mendixmodelsdk
introduces in 10.4.0. Before 10.4 a published OData service is a
Rest$PublishedODataService, which is deleted in 10.4.0: a different
document with its own children, and mxcli does not write it. The
generated version data has property floors only, so the floor tripped
over the first versioned key. Measured with the floor off: mx check
9.24.30 cannot load the project ("TypeCacheUnknownTypeException: The
type cache does not contain a type with qualified name
ODataPublish$PublishedODataService2"). Lowering the property would have
made an unloadable app.

Fix: a version gate. `create [or modify] published odata service` needs
integration.published_odata_service, 10.4.0. `check -p` and exec's
pre-flight report it as MDL-VERSION01 before anything is written, with
a hint naming the 9.x document. Writing Rest$PublishedODataService for
9.x is not attempted here.

Measured: bug-test 1163-published-odata-below-10-4.mdl with `check -p`
on a fresh 9.24.30 app reports MDL-VERSION01. 10-odata and 18-folder on
9.24 now stop at the gate. Controls: both PASS on 10.24.24 and 11.13.0.
The new gate cases fail without the gate (0 violations).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
The 9.24 doctype run for #1163 split five refusals three ways:
a wrong floor in the generated data (AggregateFunction), a document type
the version lacks (PublishedODataService2, where lowering would have
produced an unloadable project), and an older shape that stores nothing
(a flow argument that is the enclosing object). Record what settled each
one: the SDK's version data, Studio Pro-authored bytes from a Marketplace
.mpk, and `mx convert -p` on the older version against a floor-off build,
comparing values as well as keys.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
Fix test-runner false pass, module role description loss, and Mendix 9/10 property rows
`git remote get-url` applies url.<base>.insteadOf, and doctorEnv left
the system, global and GIT_CONFIG_COUNT/KEY/VALUE configuration in
place. In a sandbox that rewrites [email protected]: to https://github.com/
through the environment, the SSH remote the tests add already read back
as HTTPS, and TestGitDoctor_SSHRemoteRewrittenByFix and
TestGitDoctor_SSHURLFormRewrittenByFix failed `make test` on main (#1670)
while CI was green. A ~/.gitconfig rewriting https to git@ fails the
same two tests the other way.

doctorEnv now sets GIT_CONFIG_GLOBAL to the null device,
GIT_CONFIG_NOSYSTEM=1 and GIT_CONFIG_COUNT=0. Control: before the fix
both tests fail under each rewrite separately (environment, and HOME
with an https->git@ insteadOf); with an empty HOME and no environment
rewrite they pass. After it, both rewrites pass.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
fix(test): git doctor tests no longer read the developer's git config
go.mod's toolchain line makes `go` (and `make fmt`) switch to go1.27.3,
but fmt-check ran a bare `gofmt`, which is whatever Go is installed. With
go1.24.7 installed, `make lint` failed main on a file CI accepts
(mdl/backend/mfmutator/splice_handler_test.go) and passed a file CI then
rejected (#1759).

GOFMT defaults to $(go env GOROOT)/bin/gofmt. Control: `make fmt-check
GOFMT=/usr/local/go/bin/gofmt` still fails on splice_handler_test.go;
the default passes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
`make build test lint` covers less than CI's build-and-test job gates
on: check-mdl, check-findings, check-skill-index, check-wiki-pages and
the rest ran only in CI unless someone remembered them. ci-local runs
the job's steps in its order (check-skill-mdl standing for its three
skill/docs MDL steps), without govulncheck, which the job installs
@latest over the network, and without the integration jobs, which need
mxbuild.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
Holds this repository's conventions for driving a PR Claude opened to
green: the user reviews before the PR is opened and it is merged on
green by default (merge commit, expectedHeadSha, branch restarted from
main afterwards); `make ci-local` before every push; the macOS
tunnel-seam job that is cancelled before it gets a runner, and so gets
one re-run; the conflicts this repo keeps hitting (CHANGELOG, generated
indexes and tables); and how to treat the AI PR Review comment's
Critical / Moderate / Minor sections.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011oK82sUVQEB3K5W5vh85wM
`git clone` never fetches refs/notes/*, so on a fresh clone or a CI
checkout `git doctor` reported every commit as "without a note" and
exited 1, while the same report said `[x] origin has
refs/notes/mx_metadata`. `--fix` then wrote computed notes over commits
the remote already annotates, and the push it recommends was rejected
as non-fast-forward. Anyone who forced that push would overwrite the
real notes on the remote.

The remote check already reads the remote's notes commit with
ls-remote; it is now passed on. If the local ref is not, or does not
contain, that commit, the commits are not judged. The finding is the
unfetched ref, given with the command that fetches it. `--fix` fetches
it as a fast-forward only, so a note that exists only locally is never
dropped, and stops if that fetch is refused.

Test: TestGitDoctor_UnfetchedNotesAreNotMissing (bare remote, pushed
note, fresh clone). Before the fix it fails with the reported symptom:
"[ ] 9450425668 without a note" next to "[x] origin has
refs/notes/mx_metadata", and --fix replaced the remote's note on HEAD.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011oK82tNcDLefAskVSzcKPm
…rewrite

`git doctor` read the remote with `git remote get-url`, which applies
url.<base>.insteadOf. With a global https -> git@ rule (a common
developer setup), an HTTPS remote read as SSH and the doctor exited 1.
`--fix` then set the configured URL to HTTPS on every run, which the
rule rewrote straight back, and dropped the URL's `.git` suffix along
the way. #1765 isolated the tests from this and left the
product question open; this answers it.

The check now reads `git config --get remote.<name>.url`, the value
--fix actually writes. An SSH remote is still found and rewritten when
a rule makes it read as HTTPS. When the effective URL differs, the
report gives it as an [i] line naming the insteadOf rule, which --fix
leaves alone. It is not counted as an issue: the rule applies to every
git command on the machine, and changing the remote cannot undo it.

Tests: TestGitDoctor_InsteadOfRewriteIsNotAnSSHRemote and
TestGitDoctor_ConfiguredSSHRemoteFoundBehindRewrite. Both use a rule in
the test repository's own config, on a host nobody rewrites. Before the
fix: "origin: [email protected]:org/app.git — SSH; Studio Pro needs
HTTPS", --fix changed the configured URL to
https://git.example.com/org/app, and the configured SSH remote was
missed. The two existing SSH tests also pass now in an environment that
rewrites github.com URLs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011oK82tNcDLefAskVSzcKPm
`mxcli git commit --dry-run` staged everything in the real index to
report it, then undid that with `git reset -q`. A reset goes back to
HEAD, not to the state before the run, so anything the user had staged
was unstaged. The docs promise that a dry run writes nothing.

A dry run now stages into a temporary copy of the index
(GIT_INDEX_FILE), so the real index is never touched: staged changes,
stat data and intent-to-add entries all stay as they were. A real
commit still uses the real index, as before. `gitOutInEnv` is the git
helper with extra environment.

Test: TestGitCommit_DryRunKeepsTheUsersStaging. Before the fix:
`staged after --dry-run = "", want exactly what the user had staged
(staged.txt)`. The existing dry-run test started from an empty index,
where the bug and the correct behaviour look the same.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011oK82tNcDLefAskVSzcKPm
`mxcli git commit` dropped any path with a segment named
`.mendix-cache`, `deployment`, `theme-cache` or `.mxcli`, at any depth.
So `resources/deployment/endpoints.xml` was silently left out of the
commit. The only trace was a count in "Left out 1 file(s) Studio Pro
never commits".

Those folders are the project's own: Studio Pro, mxbuild and mxcli
create them directly beside the .mpr. isJunkPath now matches them there
only. When the project sits in a subfolder of the repository, that
subfolder is the anchor, taken from gitNoteRepo.MPRPath. The lock,
backup and user-settings file names still match at any level.

Tests: TestGitCommit_NestedFolderNamedLikeJunkIsCommitted and
TestGitCommit_JunkAnchoredAtProjectInSubfolder. Before the fix both
fail: "resources/deployment/endpoints.xml is not the project's own
folder and must be committed", and the same for
javascriptsource/theme-cache/, app/resources/deployment/ and
tools/deployment/. The project's own deployment/ and theme-cache/ stay
out, as the existing spaced-path test also checks.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011oK82tNcDLefAskVSzcKPm
Add the steward skill and `make ci-local`; fmt-check uses the toolchain's gofmt
fix(git): doctor and commit — unfetched notes, insteadOf, dry-run staging, nested build folders
@ako
ako merged commit 26c19dc into mendixlabs:main Oct 29, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants