Skip to content

[DMD-1833] Require the replaced body fields on rebase; keyword-only MR optional fields - #606

Merged
padak merged 2 commits into
ms/dmd-1833from
padak/dmd-1833-rebase-required-fields
Aug 18, 2026
Merged

[DMD-1833] Require the replaced body fields on rebase; keyword-only MR optional fields#606
padak merged 2 commits into
ms/dmd-1833from
padak/dmd-1833-rebase-required-fields

Conversation

@padak

@padak padak commented Aug 18, 2026

Copy link
Copy Markdown
Member

What

Addresses finding 1 and finding 3 from my review of #556. Targets ms/dmd-1833, not main — merge this into your branch (or cherry-pick, or close it and do it your way; the analysis is the point, not the patch).

Only touches things my review flagged. No new endpoints, no behavior change beyond the two below.

1. rebase_config: the whole replaced body becomes required

/rebase replaces a configuration rather than patching it, so an omitted key is not "leave unchanged" but "take the server-side default". Your own RFC documents those defaults (lines 109-110, from RebaseRequest::validateDiff): diff.configuration{}, diff.isDisabledfalse.

With both optional, this call is the natural one to write — it passes exactly the fields the signature marks as required:

client.rebase_config(component_id, config_id, branch_id=123, version=7, name="X", rows=[...])

and it sends {"version": 7, "diff": {"name": "X", "rows": [...]}}. The backend fills in the rest, so the rebased config loses its whole parameters block and a disabled config comes back enabled. Merge the MR and that reaches production.

name and rows are required precisely to make that class of loss unrepresentable (D6). This extends the same reasoning to the two fields where the backend substitutes content instead of rejecting the request.

The same applies to description, which the first commit left optional and the second makes required after checking the source (see below). It stays str | NoneNone is a legitimate resolved value meaning "the config ends up with no description", and it still omits the key rather than sending an explicit null, which costs no expressiveness because the two are indistinguishable server-side. What it no longer has is a default, because that default was the silent-loss path.

change_description is the one genuine optional: it is not part of the replaced body, and null selects a default rebase message rather than clearing anything.

D1 in the RFC is updated to say why presence detection is right for update_config (which patches) and wrong here (which replaces), rather than reading as an unexplained inconsistency later.

2. _optional_mr_fields becomes keyword-only

The helper exists so the create and update bodies "cannot drift" — but both call sites passed five arguments positionally and four are str | None. A transposition of auto_merge_strategy and auto_merge_at type-checks cleanly, passes ruff and ty, and surfaces only as a backend 422; the existing coverage asserts autoMergeStrategy for create only. * in the signature closes it the way _billing_get hardcodes its verb — a guarantee no source-scanning test has to catch.

Not included

The open question, now resolved (second commit)

The first commit left description optional and flagged that I could not tell from the RFC whether an absent diff.description preserves the previous description or nulls it. Devin's review flagged the same gap. Rather than leave it to you, I checked the Connection source:

  • RebaseRequest::mapValidatedDatadescription: isset($diff->description) ? (string) $diff->description : null, so a missing key becomes null.
  • ConfigurationRebaseService"The resolved config body ($name/$description/$configuration/$isDisabled) is the complete 3-way diff result and fully replaces version 2's body."

So an omitted description is written as null: the same silent-loss mode, and description belongs in the required set. That is the second commit. The same two sources also confirm the first commit's premise rather than just the RFC's summary of it, and they draw the line for change_description, which is absent from that four-field tuple and documented as null → default rebase message.

Testing

  • make check green: 5774 passed, 12 skipped, ty clean (only the 3 pre-existing warnings), all repo gates OK.
  • New tests: the replaced body fields always reach the wire; omitting any of them is a TypeError at the call site rather than silent loss on the wire (the test iterates the whole set, so an optional-with-default reintroduced on any one of them fails); every _optional_mr_fields parameter is KEYWORD_ONLY.
  • The keyword-only test asserts on inspect.signature rather than making a deliberately-wrong call — a positional call is a static error too, so writing one would mean fighting ty to prove ty is right.
  • test_keep_rebase_omits_unset_optionals_and_sends_empty_rows was updated: it can no longer be about is_disabled, so it now covers description / change_description.

…y MR fields

`/rebase` replaces a configuration rather than patching it, so an omitted key
is not "leave unchanged" but "take the server-side default". The RFC documents
those defaults: `diff.configuration` -> `{}` and `diff.isDisabled` -> `false`.
With both parameters optional, a caller resolving a conflict on a disabled
config and passing only the two fields the signature marks as required (`name`,
`rows`) would wipe the configuration body and re-enable the config -- and then
merge that into production. Both are now required, for the same reason `name`
and `rows` are.

`description` and `change_description` stay optional: the backend maps an
absent key to null / a default change message rather than substituting content.

Also makes `_optional_mr_fields` keyword-only. The helper exists so the create
and update bodies cannot drift, but four of its five parameters are
`str | None`, so a positional transposition was the one drift mode it could not
catch -- it type-checks cleanly and surfaces only as a backend 422.

Tests pin that both fields always reach the wire, that omitting either is a
TypeError at the call site rather than silent loss on the wire, and that every
`_optional_mr_fields` parameter is KEYWORD_ONLY.
@linear-code

linear-code Bot commented Aug 18, 2026

Copy link
Copy Markdown

DMD-1833

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

Open in Devin Review

Comment thread src/keboola_agent_cli/client/configs.py Outdated
…eplaced body

Resolves the open question the PR body left: an absent `diff.description` does
NOT preserve the previous description.

`RebaseRequest::mapValidatedData` maps a missing key to null, and
`ConfigurationRebaseService` documents `$name` / `$description` /
`$configuration` / `$isDisabled` as "the complete 3-way diff result" that
"fully replaces" the resolved version's body. So an omitted description is
written as null -- the same silent-loss failure mode this branch already fixed
for `configuration` and `is_disabled`.

`description` is now required but stays `str | None`: None is a legitimate
resolved value ("the config ends up with no description") and still omits the
key rather than sending an explicit null, which costs no expressiveness because
the two are indistinguishable server-side.

`change_description` stays optional -- it is not part of the replaced body
tuple, and null selects a default rebase message rather than clearing anything.

The required-field test now iterates every replaced body field rather than
naming two, so a future optional-with-default reintroduced on any of them fails.
@padak padak changed the title [DMD-1833] Require configuration/is_disabled on rebase; keyword-only MR optional fields [DMD-1833] Require the replaced body fields on rebase; keyword-only MR optional fields Aug 18, 2026
@padak

padak commented Aug 18, 2026

Copy link
Copy Markdown
Member Author

Heads-up so you are not waiting on a green tick that will never arrive: no CI runs on this PR. ci.yml is scoped to pull_request: branches: [main], and this one targets ms/dmd-1833, so no workflow is triggered at all — the empty check list is the filter, not a failure.

Verification here is therefore local only: make check green on 5920cc6 (5774 passed, 12 skipped, ty clean apart from the 3 pre-existing warnings, all drift gates OK). Once these commits land on ms/dmd-1833, #556 gets the real CI run against main.

@padak
padak merged commit 0b88991 into ms/dmd-1833 Aug 18, 2026
@padak
padak deleted the padak/dmd-1833-rebase-required-fields branch August 18, 2026 22:35
martinsifra pushed a commit that referenced this pull request Aug 18, 2026
…R optional fields (#606)

* fix(client): require configuration/is_disabled on rebase, keyword-only MR fields

`/rebase` replaces a configuration rather than patching it, so an omitted key
is not "leave unchanged" but "take the server-side default". The RFC documents
those defaults: `diff.configuration` -> `{}` and `diff.isDisabled` -> `false`.
With both parameters optional, a caller resolving a conflict on a disabled
config and passing only the two fields the signature marks as required (`name`,
`rows`) would wipe the configuration body and re-enable the config -- and then
merge that into production. Both are now required, for the same reason `name`
and `rows` are.

`description` and `change_description` stay optional: the backend maps an
absent key to null / a default change message rather than substituting content.

Also makes `_optional_mr_fields` keyword-only. The helper exists so the create
and update bodies cannot drift, but four of its five parameters are
`str | None`, so a positional transposition was the one drift mode it could not
catch -- it type-checks cleanly and surfaces only as a backend 422.

Tests pin that both fields always reach the wire, that omitting either is a
TypeError at the call site rather than silent loss on the wire, and that every
`_optional_mr_fields` parameter is KEYWORD_ONLY.

* fix(client): require description on rebase too -- it is part of the replaced body

Resolves the open question the PR body left: an absent `diff.description` does
NOT preserve the previous description.

`RebaseRequest::mapValidatedData` maps a missing key to null, and
`ConfigurationRebaseService` documents `$name` / `$description` /
`$configuration` / `$isDisabled` as "the complete 3-way diff result" that
"fully replaces" the resolved version's body. So an omitted description is
written as null -- the same silent-loss failure mode this branch already fixed
for `configuration` and `is_disabled`.

`description` is now required but stays `str | None`: None is a legitimate
resolved value ("the config ends up with no description") and still omits the
key rather than sending an explicit null, which costs no expressiveness because
the two are indistinguishable server-side.

`change_description` stays optional -- it is not part of the replaced body
tuple, and null selects a default rebase message rather than clearing anything.

The required-field test now iterates every replaced body field rather than
naming two, so a future optional-with-default reintroduced on any of them fails.
padak added a commit that referenced this pull request Aug 18, 2026
The Testing section still described the pre-#606 behaviour -- `rebase_config`
sending `is_disabled=False` but omitting `is_disabled=None`. `is_disabled` is
`bool` now, so `None` is not expressible at all.

My miss in #606: D1 and the signature table were updated, this bullet was not.
It talks about the parameters by describing their behaviour rather than naming
`rebase_config`'s signature, so grepping for the symbol did not surface it.
padak added a commit that referenced this pull request Aug 18, 2026
#608)

The Testing section still described the pre-#606 behaviour -- rebase_config sending is_disabled=False but omitting is_disabled=None. is_disabled is bool since #606, so None is not expressible at all. The bullet now describes what the method actually does.
padak added a commit that referenced this pull request Aug 20, 2026
…85.1

The version bump to 0.86.0 already landed on main (#615), but the release
notes it produces were incomplete in two ways.

Missing entries. PR #616 (`token list`, plus the retry-policy and
exceptionId changes) carried no changelog note at all -- its commit message
says "No version bump: this lands in a stack of PRs released as one version.
The (since v0.86.0) doc tags assume 0.86.0 and the bump PR must confirm
that", and the bump PR did not. `make changelog-check` cannot catch this: it
verifies every published GitHub release has an entry, not that every merged
PR has a note. #556/#606 (merge-request endpoints, Layer 3) and #610
(winget job disabled) were likewise unannounced. All four are added.

Phantom 0.85.1. pyproject went 0.85.0 -> 0.85.1 (#614) -> 0.86.0 (#615)
without a tag in between, so 0.85.1 exists only as a changelog bucket -- no
release, no artifact, nobody running it. `format_whats_new` shows the notes
of the *target* version only, so every user upgrading 0.85.0 -> 0.86.0 would
have silently missed those four fixes (Azure ciphertext prefix, the
`parameters` wrapper, GCP/Azure sync ciphertext, the encrypt-values docs).
The bucket is folded into 0.86.0 verbatim.

The same phantom leaked into the agent-facing version gates, which is the
worse half: `keboola-expert.md` told users to "upgrade to 0.85.1+" and four
gotchas.md entries were tagged `(since v0.85.1)` -- a version nobody can
install. Retagged to 0.86.0, along with two source comments.

Three of the new notes had to lead with a shorter sentence to satisfy
`test_newest_release_notes_are_not_truncated` (the headline is the note's
first sentence, capped at 160 chars).

No behaviour change; documentation and release metadata only.
padak added a commit that referenced this pull request Aug 20, 2026
…85.1 (#619)

* chore(release): complete the 0.86.0 changelog and drop the phantom 0.85.1

The version bump to 0.86.0 already landed on main (#615), but the release
notes it produces were incomplete in two ways.

Missing entries. PR #616 (`token list`, plus the retry-policy and
exceptionId changes) carried no changelog note at all -- its commit message
says "No version bump: this lands in a stack of PRs released as one version.
The (since v0.86.0) doc tags assume 0.86.0 and the bump PR must confirm
that", and the bump PR did not. `make changelog-check` cannot catch this: it
verifies every published GitHub release has an entry, not that every merged
PR has a note. #556/#606 (merge-request endpoints, Layer 3) and #610
(winget job disabled) were likewise unannounced. All four are added.

Phantom 0.85.1. pyproject went 0.85.0 -> 0.85.1 (#614) -> 0.86.0 (#615)
without a tag in between, so 0.85.1 exists only as a changelog bucket -- no
release, no artifact, nobody running it. `format_whats_new` shows the notes
of the *target* version only, so every user upgrading 0.85.0 -> 0.86.0 would
have silently missed those four fixes (Azure ciphertext prefix, the
`parameters` wrapper, GCP/Azure sync ciphertext, the encrypt-values docs).
The bucket is folded into 0.86.0 verbatim.

The same phantom leaked into the agent-facing version gates, which is the
worse half: `keboola-expert.md` told users to "upgrade to 0.85.1+" and four
gotchas.md entries were tagged `(since v0.85.1)` -- a version nobody can
install. Retagged to 0.86.0, along with two source comments.

Three of the new notes had to lead with a shorter sentence to satisfy
`test_newest_release_notes_are_not_truncated` (the headline is the note's
first sentence, capped at 160 chars).

No behaviour change; documentation and release metadata only.

* fix(changelog): correct the serve route and give every 0.86.0 note a recognised prefix

Two findings from Devin's review of this PR, plus one they could not see.

The serve route for `token list` was cited as `GET /tokens/{project}`. Both
halves are wrong: the router carries `prefix="/token"` (singular) and the
operation is registered at `/{project}/list`, so the real path is
`GET /token/{project}/list` -- confirmed against the runtime OpenAPI schema,
not the source, because that is what a caller actually hits. Worth noting the
review's proposed correction (`/tokens/{project}/list`) is itself wrong on the
prefix; taking it verbatim would have swapped one 404 for another.

`CI:` is not a recognised note prefix. `_PREFIX_STYLES` / `_PREFIX_RE` in
commands/changelog.py define the set, the module docstring states the contract,
and an unrecognised label renders unhighlighted. Retitled to `Note:`, which
also reads better: the winget job being disabled has a user-facing consequence
(WinGet users stay on the last published version), so burying it under a dim
`Internal:` would understate it.

The finding Devin could not report: the four notification notes carried by
#615/#618 have no prefix at all. They were outside this PR's diff, so no
reviewer looking at the diff would flag them -- but they ship in the same
release block and break the same contract, leaving half of v0.86.0 rendering
flat. Prefixed `New:` / `Note:` with no change of meaning. Every 0.86.0 note
now matches `_PREFIX_RE`, verified by asserting over the live CHANGELOG rather
than by reading.

Each replacement is written to disk on its own. Running several in one script
means a later failed assert discards the earlier successful writes, which is
precisely how #618's stale "server-side ?event=" claim survived its own fix
pass.
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.

1 participant