Skip to content

bundle/direct, libs/dms: fix DMS deploy/redeploy/destroy round-trip and record git info - #6139

Closed
pavloKozlov wants to merge 5 commits into
isaac/pr6052-fixesfrom
fix/dms-integral-double-state
Closed

bundle/direct, libs/dms: fix DMS deploy/redeploy/destroy round-trip and record git info#6139
pavloKozlov wants to merge 5 commits into
isaac/pr6052-fixesfrom
fix/dms-integral-double-state

Conversation

@pavloKozlov

@pavloKozlov pavloKozlov commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Four bugs blocked using DMS recording (experimental.record_deployment_history: true + direct engine) beyond the very first deploy. Each surfaced only on the read/update/delete paths that a net-new deploy never exercises. All four are fixed here and verified end to end against dogfood: a clean net-new deploy followed by an update now produces a two-version deployment (VERSION_COMPLETE_SUCCESS, previous_version_id chained) with no spurious failures, and destroy tears it down.

1. Integral doubles rejected on state readback

Second deploy failed at planning:

cannot plan resources.jobs.daily_sales_rollup: interpreting state:
unmarshalling into *jobs.JobSettings: json: cannot unmarshal string into Go value of type int

DMS round-trips state through a protobuf Struct (numbers are all double), so integer fields come back as 1.0 and the typed structs reject them. Fix: normalize integral doubles to integers at the DMS read boundary (fetchDeploymentResources).

2. Missing previous_version_id on the next version

failed to create deployment version: previous_version_id is outdated;
the deployment's most recent version is 1. Retry with previous_version_id 1. (400 INVALID_PARAMETER_VALUE)

The server uses previous_version_id for optimistic concurrency; the recorder never set it, and the SDK's Version struct lacked the field before v0.166.0. Fix: bump databricks-sdk-go v0.160.0 → v0.166.0 and set PreviousVersionId to the current last_version_id. The bump also drops the now server-assigned DeploymentId from CreateDeploymentRequest; the generated bundle-deployments command is regenerated accordingly.

3. Missing resource_id when recording a delete

recording operation ...: resource_id is required for OPERATION_ACTION_TYPE_DELETE operations (400 INVALID_PARAMETER_VALUE)

The delete path recorded with an empty resource ID; DMS requires it. The ID is in state but Destroy/DeleteState clear it first. Fix: capture the ID before deletion and pass it through.

4. Spurious deploy failure on CreateOperation response parse

recording operation ...: failed to unmarshal response body: invalid character '1' after top-level value

DMS returns sequence_id as a JSON string ("1", proto3 int64), but the SDK's Operation.SequenceId is an int64 it can't parse from a string (still true at v0.168.0). It fired only on responses carrying sequence_id, so deploys failed at random on a call the server accepted. The CLI discards this response. Fix: tolerate a response-deserialization error (a 2xx — the SDK maps status >= 400 to *apierr.APIError before reading the body); keep failing on API errors and transport errors.

5. git_info never recorded on versions

The DMS Version carries git_info (origin_url, branch, commit) but the recorder never set it, so it came back empty on every version. The data is already resolved into bundle.git.* by the LoadGitDetails mutator (initialize phase, before any recorder), or set by the user under bundle.git in databricks.yml. Fix: map bundle.git onto the version's GitInfo; send nil when no git details are known so DMS records no git info rather than empty strings. Verified end to end — deploying from the bundle-test git checkout recorded origin_url: https://github.com/pavloKozlov/bundle-test.git and commit: e62c645….

6. DMS state field changed from JSON object to string

The service changed Operation.state/Resource.state from a JSON object to an opaque UTF-8 JSON string it stores unchanged (no longer parsed through a protobuf Value). Sending an object now fails: Invalid value: {"state":{...}} for expected type: STRING (400 MALFORMED_REQUEST). Fix: bump databricks-sdk-go v0.166.0 → v0.169.0 (where both fields are string) and send/read state as a string. This is also the server-side resolution of #1: state is no longer round-tripped through a protobuf Struct, so integers survive; the normalization from #1 is kept to guard legacy state.

7. Failed operations recorded no error

When a resource failed to apply (e.g. the jobs API rejecting an unsupported node type), the CLI recorded no operation at all and completed the version as VERSION_COMPLETE_FAILURE with no error_message anywhere — so DMS history showed that a deploy failed but never why. Fix: record a FAILED operation carrying the error (OPERATION_STATUS_FAILED + Operation.error_message, both already in the SDK) when Deploy/Destroy fails. recordFailure bypasses the queue's prior-error short-circuit so the stopping failure is still recorded; best-effort at the call site so it never masks the underlying deploy error. Verified: the failing i9.xlarge deploy now records status: OPERATION_STATUS_FAILED with error_message: "...Node type i9.xlarge is not supported...".

Tests

  • dstate: TestFetchDeploymentResourcesNormalizesIntegralDoubles, TestNormalizeIntegralNumbers, TestNormalizeIntegralNumbersEmptyInputUnchanged.
  • dms: recorder_test.go asserts PreviousVersionId on subsequent vs first/missing-record deploys.
  • direct: TestOperationRecorderToleratesResponseDeserializationError, TestOperationRecorderPropagatesAPIError, TestOperationRecorderPropagatesTransportError.
  • dms: TestRecorderRecordsGitInfo, TestRecorderGitInfoNilWhenAbsent.

Known follow-ups (not in this PR)

  • Bugs 1 and 4 are really SDK-side type mismatches (proto3 string-encoded int64 vs Go int64); the CLI-side handling here is defensive. The SDK models could be fixed upstream.
  • bundle destroy prints Destroy complete! and then errors with a 404 re-resolving the just-deleted deployment node — cosmetic, teardown succeeds. Left for a separate change.

Base branch

Targets isaac/pr6052-fixes (#6094), where the DMS code lives (not yet on main).

This pull request and its description were written by Isaac.

DMS round-trips recorded resource state through a protobuf Struct, whose
only numeric type is double, so integers are served back fractional (e.g.
"max_concurrent_runs": 1.0). The typed resource structs unmarshal those
fields as int and reject the fractional form, so any deploy that reads
existing state back from DMS - i.e. every deploy after the first - failed
with:

  cannot plan resources.jobs.daily_sales_rollup: interpreting state:
  unmarshalling into *jobs.JobSettings: json: cannot unmarshal string
  into Go value of type int

(The "string" in the message is the SDK unmarshaller's quote-and-retry
fallback kicking in after the numeric parse fails.)

Normalize integral doubles back to integers at the single DMS read
boundary in fetchDeploymentResources, before the state reaches the typed
resource structs. Genuinely fractional numbers are left untouched.

Co-authored-by: Isaac
Recording a second version for an existing DMS deployment failed:

  Error: failed to create deployment version: previous_version_id is
  outdated; the deployment's most recent version is 1. Retry with
  previous_version_id 1. (400 INVALID_PARAMETER_VALUE)

The server uses previous_version_id for optimistic concurrency, but the
recorder never set it. The field was also absent from the SDK's
bundledeployments.Version until v0.166.0, so bump the SDK to pick it up
and set it to the deployment's current last_version_id (empty for the
first version, which has no predecessor).

The SDK bump also drops the now server-assigned DeploymentId field from
CreateDeploymentRequest; regenerate the bundle-deployments command
accordingly (create-deployment no longer takes a positional ID).

Co-authored-by: Isaac
@pavloKozlov pavloKozlov changed the title bundle/direct: normalize integral doubles when reading DMS state bundle/direct, libs/dms: fix DMS state readback and version-conflict on redeploy Aug 3, 2026
Destroying a bundle with DMS recording enabled failed:

  Error: recording operation for resources.jobs.daily_sales_rollup with
  the deployment metadata service: resource_id is required for
  OPERATION_ACTION_TYPE_DELETE operations (400 INVALID_PARAMETER_VALUE)

The delete path recorded the operation with an empty resource ID, but DMS
requires resource_id on a DELETE. The ID is available in state, but both
Destroy and DeleteState remove it before the record call, so capture it
beforehand and pass it through.

Co-authored-by: Isaac
Recording an operation intermittently failed the deploy with:

  Error: recording operation for resources.jobs.daily_sales_rollup with
  the deployment metadata service: failed to unmarshal response body:
  invalid character '1' after top-level value

DMS serves sequence_id as a JSON string ("1") per proto3 int64 encoding,
but bundledeployments.Operation.SequenceId is an int64 the SDK cannot
parse from a string (still true as of SDK v0.168.0). It surfaces only on
responses that carry sequence_id, so deploys failed at random on a call
the server actually accepted.

The CLI discards the CreateOperation response, so a failure to deserialize
it does not mean the operation was not recorded. The SDK maps status >= 400
to *apierr.APIError before reading the body, so a "failed to unmarshal
response body" error means a 2xx: tolerate exactly that, and keep failing
on API errors and transport errors (where the request may not have been
recorded).

Co-authored-by: Isaac
@pavloKozlov pavloKozlov changed the title bundle/direct, libs/dms: fix DMS state readback and version-conflict on redeploy bundle/direct, libs/dms: fix DMS deploy/redeploy/destroy round-trip bugs Aug 3, 2026
The DMS Version carries a GitInfo (origin_url, branch, commit), but the
recorder never populated it, so git_info came back empty on every recorded
version. The data is already resolved by the LoadGitDetails mutator (run in
the initialize phase, before any recorder) into bundle.git.*, or set
explicitly by the user under bundle.git in databricks.yml.

Map bundle.git onto the version's GitInfo when creating it. Send nil when
no git details are known (the bundle is not in a git repository) so DMS
records no git info rather than empty strings.

Co-authored-by: Isaac
@pavloKozlov pavloKozlov changed the title bundle/direct, libs/dms: fix DMS deploy/redeploy/destroy round-trip bugs bundle/direct, libs/dms: fix DMS round-trip bugs and record git info Aug 3, 2026
@eng-dev-ecosystem-bot

Copy link
Copy Markdown
Collaborator

Integration test report

Commit: e6dc076

Run: 30804494731

Env 💚​RECOVERED 🙈​SKIP ✅​pass 🙈​skip Time
💚​ aws linux 4 4 322 1068 4:24
💚​ aws windows 4 4 324 1066 4:33
💚​ azure linux 4 4 322 1067 4:12
💚​ azure windows 4 4 324 1065 4:25
💚​ gcp linux 1 5 321 1069 4:15
💚​ gcp windows 1 5 323 1067 8:12
8 interesting tests: 4 RECOVERED, 4 SKIP
Test Name aws linux aws windows azure linux azure windows gcp linux gcp windows
💚​ TestAccept 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R
🙈​ TestAccept/bundle/invariant/no_drift 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/vector_search_endpoints/drift/recreated_same_name 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/vector_search_indexes/recreate/embedding_dimension 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/ssh/connection 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
💚​ TestFetchRepositoryInfoAPI_FromRepo 💚​R 💚​R 💚​R 💚​R 🙈​S 🙈​S
💚​ TestFetchRepositoryInfoAPI_FromRepo/root 💚​R 💚​R 💚​R 💚​R
💚​ TestFetchRepositoryInfoAPI_FromRepo/subdir 💚​R 💚​R 💚​R 💚​R
Top 3 slowest tests (at least 2 minutes):
duration env testname
6:59 gcp windows TestAccept
3:19 azure windows TestAccept
3:10 aws windows TestAccept

@pavloKozlov pavloKozlov closed this Aug 4, 2026
@pavloKozlov pavloKozlov changed the title bundle/direct, libs/dms: fix DMS round-trip bugs and record git info bundle/direct, libs/dms: fix DMS deploy/redeploy/destroy round-trip and record git info Aug 4, 2026
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