Skip to content

mz-deploy: version with the database, and notice on version skew - #38931

Open
sjwiesman wants to merge 3 commits into
MaterializeInc:mainfrom
sjwiesman:mz-deploy-unified-version
Open

sjwiesman wants to merge 3 commits into
MaterializeInc:mainfrom
sjwiesman:mz-deploy-unified-version

Conversation

@sjwiesman

Copy link
Copy Markdown
Contributor

mz-deploy was versioned independently at 0.7.0, hand-bumped and shipped from its own mz-deploy-v* git tags through a manually triggered pipeline. That made it impossible to tell from mz-deploy --version which server releases a CLI understands, nothing detected when a CLI was driving a server it did not match, and nothing checked that a release tag agreed with the version in Cargo.toml.

This gives mz-deploy the Materialize version, publishes it from the release train, and has it say something when it connects to a server it does not match.

Three commits, in an order that matters: the notice would fire on every connection and the sandbox would name a nonexistent image if the version change did not land first.

1. Version with the release train

src/mz-deploy joins bin/bump-version, so the crate tracks environmentd. The three tarball steps move into ci/deploy/pipeline.template.yml behind if: build.tag != "", since that pipeline also runs on every push to main with no tag to parse. ci/deploy_mz-deploy/pipeline.template.yml and MzDeployVersion are deleted.

The Linux targets now extract the binary from the mz-deploy image the release build already produced rather than recompiling it, so the tarball ships exactly what CI tested. macOS still builds from source, having no image.

Release candidates publish a versioned tarball but no longer move the mz-deploy-latest-* redirect, which is the URL our install docs hand to users and so has to name a GA build. TarballUploader.deploy_tarball gains an update_latest flag, defaulting to the existing behavior for mz and mz-debug.

Deploys now assert the release tag matches src/mz-deploy/Cargo.toml, the guard ci/deploy_mz/linux.py already had.

This also resolves an existing inconsistency: src/mz-deploy/ci/mzbuild.yml defaults to publish, so materialize/mz-deploy:v26.44.0 was already being published containing a binary that reported v0.7.0.

2. Notice on version skew

The server already announces its build as an mz_version ParameterStatus during the pgwire handshake, so the comparison costs no round trip. The value is read off the Connection before the connection future is spawned. dbt-materialize reads the same parameter.

Only major and minor are compared. Patch releases do not change the surface the CLI drives, and ignoring the pre-release keeps a dev build quiet against the release it was cut from, which would otherwise mean a notice on every command during local development.

The notice never changes the exit code, and is suppressed when the peer announces nothing parseable: the handshake has already succeeded by then, and a proxy that drops the parameter is not worth warning about on every command. It fires at most once per process, which matters because test --no-docker opens a connection per test. The Once is claimed only when a notice is warranted, so a matching first connection cannot mask a later mismatched one.

Sandbox connections skip the check via a new Client::connect_sandbox. The sandbox runs an image tag the CLI picked, so skew there says nothing about the user's deployment target, and wait_for_container polls with a fresh connect up to 30 times.

3. Sandbox defaults to this build's version

test and explain ran against latest, so local runs were checked against whatever had most recently been released rather than against the server the project deploys to, and the two drifted for a whole release cycle. The sandbox image now defaults to v{version}, falling back to latest on a pre-release build since no such image is published. cloud was split out of the unset arm so it keeps meaning "whatever Cloud runs now".

Tests

Unit tests in client::version_skew cover patch-only differences, dev and rc builds against their release, both mismatch directions, a major difference, the helm-chart suffix, and absent or unparseable values. Unit tests in config cover the pre-release image fallback, an explicit pin, cloud, and the unset default. A new version_check mzcompose workflow asserts that a server on the same version draws no notice, which is the false-positive case that would otherwise be noise for every user.

Needs a decision before this ships

  • Homebrew tap. The tap pins an exact version and checksums and cannot use the latest redirect, so it now needs refreshing on the release cadence, from GA tags only. Automating a tap PR would need a Buildkite secret with write access to the tap, which does not exist today. No owner yet.
  • Archiving the deploy-mz-deploy Buildkite pipeline needs Buildkite admin; that config lives outside the repo. With the template deleted, a manual trigger would fail at upload.
  • mz-deploy failures can now redden a release build, since deploy is forced async: false on tags. Arguably correct, but worth sign-off from the release rotation.

Release note

This release will make mz-deploy share a version with Materialize, and warn when the CLI's version does not match the server it connects to.

Follow-up

DockerRuntime reuses an existing container by name without comparing its image to the configured one, so a sandbox left over from before the default tag moved silently stays on the old version. Pre-existing, surfaced by commit 3.

🤖 Generated with Claude Code

mz-deploy was versioned independently at 0.7.0, hand-bumped and shipped from
its own mz-deploy-v* git tags through a manually triggered pipeline. That made
it impossible to tell from `mz-deploy --version` which server releases a CLI
understands, and nothing checked that a release tag agreed with the version in
Cargo.toml.

Give mz-deploy the Materialize version and publish it from the release train.

src/mz-deploy is added to bin/bump-version, so the crate tracks environmentd
from here on. The three tarball steps move into ci/deploy/pipeline.template.yml
behind `if: build.tag != ""`, since that pipeline also runs on every push to
main with no tag to derive a version from. The Linux targets now extract the
binary from the mz-deploy image the release build already produced rather than
recompiling it, so the tarball ships exactly what CI tested; macOS still builds
from source because it has no image.

Release candidates publish a versioned tarball but no longer move the
`mz-deploy-latest-*` redirect, which is the URL our install docs hand to users
and so has to name a GA build. This adds an `update_latest` flag to
TarballUploader.deploy_tarball, defaulting to the existing behavior for mz and
mz-debug.

Deploys now assert that the release tag matches src/mz-deploy/Cargo.toml, the
guard ci/deploy_mz/linux.py already had.

Note this also resolves an existing inconsistency: src/mz-deploy/ci/mzbuild.yml
defaults to publish, so materialize/mz-deploy:v26.44.0 was already being
published containing a binary that reported v0.7.0.

The legacy mz-deploy-v0.* tags are left in place. They do not start with `v`,
so git.get_version_tags ignores them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sjwiesman
sjwiesman marked this pull request as ready for review September 18, 2026 17:04
@sjwiesman
sjwiesman requested review from a team as code owners September 18, 2026 17:04
@sjwiesman
sjwiesman requested a review from bosconi September 18, 2026 17:05
sjwiesman and others added 2 commits September 18, 2026 12:08
mz-deploy and the server now share a version, but they still deploy
separately, so a CLI can be driving a server it does not match. Nothing
detected that, and the drift surfaced as a confusing failure partway through a
deploy rather than as something the user could act on up front.

Compare the two at connect and emit a notice on a mismatch.

The server already announces its build as an `mz_version` ParameterStatus
during the pgwire handshake, so this costs no round trip; the value is read off
the `Connection` before the connection future is spawned. dbt-materialize reads
the same parameter.

Only the major and minor versions are compared. Patch releases do not change
the surface the CLI drives, and ignoring the pre-release keeps a dev build
quiet against the release it was cut from, which would otherwise mean a notice
on every command during local development.

The notice never changes the exit code, and is suppressed when the peer
announces nothing parseable: the handshake has already succeeded by then, and a
proxy that drops the parameter is not worth warning about on every command. It
fires at most once per process, which matters because `test --no-docker` opens
a connection per test. The `Once` is claimed only when a notice is warranted,
so a matching first connection cannot mask a later mismatched one.

Sandbox connections skip the check via a new `Client::connect_sandbox`. The
sandbox runs an image tag this CLI picked, so skew there says nothing about the
user's deployment target, and `wait_for_container` polls with a fresh connect
up to 30 times.

`BUILD_INFO` and `VERSION` move from the binary into the library so the client
layer can reach them.

Tests: unit tests in `client::version_skew` cover patch-only differences, dev
and rc builds against their release, both mismatch directions, a major
difference, the helm-chart suffix, and absent or unparseable values. A new
`version_check` mzcompose workflow asserts that a server on the same version
draws no notice, which is the false-positive case that would otherwise be noise
for every user.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`test` and `explain` run against an ephemeral Materialize container, and with
no `mz_version` configured that container was always `latest`. So local runs
were checked against whatever had most recently been released rather than
against the server the project actually deploys to, and the two drifted apart
for the whole of a release cycle.

Default the sandbox image to this build's own version instead. Now that
mz-deploy carries the Materialize version, `v{version}` names the matching
published image.

A pre-release build has no such image, so it falls back to `latest`. Without
that, `test` would break for every developer working against a dev build.

`cloud` no longer shares an arm with the unset case. It means whatever
Materialize Cloud runs now, which `latest` tracks, so folding the two together
would have quietly redefined it. An explicit `mz_version` pin and the
`--docker-image` override are unaffected.

Tests: unit tests in `config` cover the pre-release fallback, an explicit pin,
`cloud`, and the unset default.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sjwiesman
sjwiesman force-pushed the mz-deploy-unified-version branch from a57beb1 to 71e3547 Compare September 18, 2026 17:09
@def-

def- commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

QA LLM Review

1. MEDIUM — the new sandbox default never applies to projects created by mz-deploy new

src/mz-deploy/src/cli/scaffold/project.toml:1

The scaffold template hardcodes mz_version = "cloud", and this PR routes Some("cloud") to materialize/materialized:latest. Every project created the documented way therefore keeps the old latest behaviour, so the drift commit 3 exists to remove persists for all new projects, and the new doc bullet ("Defaults to the version of mz-deploy itself, so local runs match the server you deploy to") is wrong for them.

Details

ProjectSettings::docker_image (src/mz-deploy/src/config.rs:204) only reaches default_docker_image() on the None arm:

match self.mz_version.as_deref() {
    Some("cloud") => format!("{DOCKER_IMAGE_BASE}:{LATEST_TAG}"),
    None => default_docker_image(),
    Some(tag) => format!("{DOCKER_IMAGE_BASE}:{tag}"),
}

mz-deploy new writes project.toml verbatim from src/mz-deploy/src/cli/scaffold/project.toml (new_project.rs:27,191), whose first line is mz_version = "cloud". doc/user/content/developer-tools/mz-deploy/get-started.md:67 presents mz-deploy new as the way to start a project, so the populated value is what users get: test and explain keep running against the newest GA release rather than against this build's version, silently, exactly as before.

Dropping the mz_version line from the scaffold template (leaving a commented-out example, since the key is still documented) makes the new default reachable. If cloud really is the intended default for new projects, then the doc bullet and the commit message should say so instead.

Note that the test projects under test/mz-deploy/projects/ omit mz_version, so the mzcompose suite exercises the None arm and never notices the scaffolded value.

2. LOW — workflow_version_check passes with no connection at all

test/mz-deploy/mzcompose.py:3042

mz-deploy debug maps a failed connect to RemoteOutput::Failure and still returns Ok(()) (src/mz-deploy/src/cli/commands/debug.rs:177), so both assertions in the new workflow hold when the CLI never reaches materialized. The test can go green while providing zero coverage of the version check.

Details

Both assertions are satisfied by a run that printed Failed to connect to materialized:6875: the exit code is 0 and, with no connection, version_skew::report is never called so "than the server" is trivially absent. The workflow is the only integration coverage the feature gets, and the connection it rides on is the one whose cluster-pin plumbing this PR refactors (pin_server_cluster: boolServerCluster), so a silent connect failure here is not far-fetched.

Asserting that the remote side actually succeeded closes it, for example by also requiring "Environment" in combined (the RemoteOutput::Success rendering) before checking for the notice.

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