[DMD-1833] Require the replaced body fields on rebase; keyword-only MR optional fields - #606
Merged
Merged
Conversation
…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.
…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.
Member
Author
|
Heads-up so you are not waiting on a green tick that will never arrive: no CI runs on this PR. Verification here is therefore local only: |
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 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.
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.
What
Addresses finding 1 and finding 3 from my review of #556. Targets
ms/dmd-1833, notmain— 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/rebasereplaces 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, fromRebaseRequest::validateDiff):diff.configuration→{},diff.isDisabled→false.With both optional, this call is the natural one to write — it passes exactly the fields the signature marks as required:
and it sends
{"version": 7, "diff": {"name": "X", "rows": [...]}}. The backend fills in the rest, so the rebased config loses its wholeparametersblock and a disabled config comes back enabled. Merge the MR and that reaches production.nameandrowsare 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 staysstr | None—Noneis 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_descriptionis 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_fieldsbecomes keyword-onlyThe 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 ofauto_merge_strategyandauto_merge_attype-checks cleanly, passes ruff and ty, and surfaces only as a backend 422; the existing coverage assertsautoMergeStrategyforcreateonly.*in the signature closes it the way_billing_gethardcodes its verb — a guarantee no source-scanning test has to catch.Not included
merge()terminal-error guard) — yours in fix(client): raise on an already-failed Storage job instead of returning it #603, with a wider blast radius than I traced. Nothing here._do_requestwrite-retry hazard — agreed it is a separatehttp_baseconcern.The open question, now resolved (second commit)
The first commit left
descriptionoptional and flagged that I could not tell from the RFC whether an absentdiff.descriptionpreserves 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::mapValidatedData—description: 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
descriptionbelongs 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 forchange_description, which is absent from that four-field tuple and documented asnull → default rebase message.Testing
make checkgreen: 5774 passed, 12 skipped,tyclean (only the 3 pre-existing warnings), all repo gates OK.TypeErrorat 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_fieldsparameter isKEYWORD_ONLY.inspect.signaturerather than making a deliberately-wrong call — a positional call is a static error too, so writing one would mean fightingtyto provetyis right.test_keep_rebase_omits_unset_optionals_and_sends_empty_rowswas updated: it can no longer be aboutis_disabled, so it now coversdescription/change_description.