diff --git a/docs/release-notes/2_18_0_summary.md b/docs/release-notes/2_18_0_summary.md
new file mode 100644
index 000000000..a0b2b184d
--- /dev/null
+++ b/docs/release-notes/2_18_0_summary.md
@@ -0,0 +1,13 @@
+## Major Changes
+
+### Table of Contents
+
+- **[VTOrc `--cell` flag auto-applied on Vitess v24+](#vtorc-cell-flag)**
+
+### VTOrc `--cell` flag auto-applied on Vitess v24+
+
+VTOrc deployments now receive `--cell=| ` automatically when the configured image tag parses to Vitess v24 or newer. This is required for Vitess v25+ where `--cell` is a hard requirement ([vitessio/vitess#20048](https://github.com/vitessio/vitess/pull/20048)) and was introduced in v24 ([vitessio/vitess#19047](https://github.com/vitessio/vitess/pull/19047)).
+
+The flag is **only** emitted when the version is parseable from the image tag (e.g. `vitess/lite:v24.0.0-mysql80`). Rolling tags such as `vitess/lite:mysql80` or `vitess/lite:latest`, or digest-only references, do not get the flag — pin a versioned tag, or set `cell` explicitly via `vitessOrchestrator.extraFlags` if you need it.
+
+Pre-v24 users are unaffected.
diff --git a/go.mod b/go.mod
index e1c38f1b8..2be1392c7 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,7 @@ go 1.26.2
require (
github.com/ahmetb/gen-crd-api-reference-docs v0.3.0
+ github.com/blang/semver/v4 v4.0.0
github.com/google/uuid v1.6.0
github.com/planetscale/operator-sdk-libs v0.0.0-20220216002626-1af183733234
github.com/prometheus/client_golang v1.23.2
@@ -85,7 +86,6 @@ require (
github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 // indirect
github.com/aws/smithy-go v1.24.3 // indirect
github.com/beorn7/perks v1.0.1 // indirect
- github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 // indirect
diff --git a/pkg/operator/vitess/version.go b/pkg/operator/vitess/version.go
new file mode 100644
index 000000000..f66a57336
--- /dev/null
+++ b/pkg/operator/vitess/version.go
@@ -0,0 +1,46 @@
+/*
+Copyright 2026 PlanetScale Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package vitess
+
+import (
+ "strings"
+
+ "github.com/blang/semver/v4"
+)
+
+// MajorVersionFromImage parses the Vitess major version from a Docker image
+// reference like "vitess/lite:v24.0.0-mysql80". Returns (major, true) when
+// the image carries a SemVer-compatible tag and (0, false) otherwise
+// (rolling tags such as "mysql80" or "latest", digests, or empty input).
+func MajorVersionFromImage(image string) (int, bool) {
+ if image == "" {
+ return 0, false
+ }
+ // Drop digest portion if present (e.g. "repo:tag@sha256:...").
+ if at := strings.IndexByte(image, '@'); at >= 0 {
+ image = image[:at]
+ }
+ colon := strings.LastIndexByte(image, ':')
+ if colon < 0 {
+ return 0, false
+ }
+ v, err := semver.ParseTolerant(image[colon+1:])
+ if err != nil {
+ return 0, false
+ }
+ return int(v.Major), true
+}
diff --git a/pkg/operator/vitess/version_test.go b/pkg/operator/vitess/version_test.go
new file mode 100644
index 000000000..471586fa0
--- /dev/null
+++ b/pkg/operator/vitess/version_test.go
@@ -0,0 +1,119 @@
+/*
+Copyright 2026 PlanetScale Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package vitess
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestMajorVersionFromImage(t *testing.T) {
+ tests := []struct {
+ name string
+ image string
+ want int
+ wantOK bool
+ }{
+ {
+ name: "clean v24 tag",
+ image: "vitess/lite:v24.0.0",
+ want: 24,
+ wantOK: true,
+ },
+ {
+ name: "v24 rc with mysql suffix",
+ image: "vitess/lite:v24.0.0-rc1-mysql80",
+ want: 24,
+ wantOK: true,
+ },
+ {
+ name: "v23 with mysql suffix",
+ image: "vitess/lite:v23.0.5-mysql80",
+ want: 23,
+ wantOK: true,
+ },
+ {
+ name: "large major version",
+ image: "vitess/lite:v100.2.3",
+ want: 100,
+ wantOK: true,
+ },
+ {
+ name: "tag and digest both present",
+ image: "vitess/lite:v24.0.0@sha256:abc",
+ want: 24,
+ wantOK: true,
+ },
+ {
+ name: "registry prefix with version",
+ image: "registry.example.com/vitess/lite:v25.0.0-mysql80",
+ want: 25,
+ wantOK: true,
+ },
+ {
+ name: "rolling mysql tag",
+ image: "vitess/lite:mysql80",
+ want: 0,
+ wantOK: false,
+ },
+ {
+ name: "latest tag",
+ image: "vitess/lite:latest",
+ want: 0,
+ wantOK: false,
+ },
+ {
+ name: "digest only",
+ image: "vitess/lite@sha256:abc123",
+ want: 0,
+ wantOK: false,
+ },
+ {
+ name: "no tag",
+ image: "vitess/lite",
+ want: 0,
+ wantOK: false,
+ },
+ {
+ name: "empty",
+ image: "",
+ want: 0,
+ wantOK: false,
+ },
+ {
+ name: "tag without v prefix still parses as semver",
+ image: "vitess/lite:24.0.0",
+ want: 24,
+ wantOK: true,
+ },
+ {
+ name: "tag with only major still parses (tolerant)",
+ image: "vitess/lite:v24",
+ want: 24,
+ wantOK: true,
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ got, ok := MajorVersionFromImage(tc.image)
+ assert.Equal(t, tc.wantOK, ok)
+ assert.Equal(t, tc.want, got)
+ })
+ }
+}
diff --git a/pkg/operator/vtorc/deployment.go b/pkg/operator/vtorc/deployment.go
index 013328171..26ad54143 100644
--- a/pkg/operator/vtorc/deployment.go
+++ b/pkg/operator/vtorc/deployment.go
@@ -210,7 +210,7 @@ func UpdateDeployment(obj *appsv1.Deployment, spec *Spec) {
}
func (spec *Spec) flags() vitess.Flags {
- return vitess.Flags{
+ flags := vitess.Flags{
"topo_implementation": spec.GlobalLockserver.Implementation,
"topo_global_server_address": spec.GlobalLockserver.Address,
"topo_global_root": spec.GlobalLockserver.RootPath,
@@ -220,4 +220,11 @@ func (spec *Spec) flags() vitess.Flags {
"logtostderr": true,
}
+ // --cell was introduced in Vitess v24 and is required in v25+. Only emit
+ // it when we can confirm the image is on a supported version; on rolling
+ // or unparseable tags users can still force it via ExtraFlags.
+ if major, ok := vitess.MajorVersionFromImage(spec.Image); ok && major >= 24 {
+ flags["cell"] = spec.Cell
+ }
+ return flags
}
diff --git a/pkg/operator/vtorc/deployment_test.go b/pkg/operator/vtorc/deployment_test.go
new file mode 100644
index 000000000..35a2f584a
--- /dev/null
+++ b/pkg/operator/vtorc/deployment_test.go
@@ -0,0 +1,122 @@
+/*
+Copyright 2026 PlanetScale Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package vtorc
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ planetscalev2 "planetscale.dev/vitess-operator/pkg/apis/planetscale/v2"
+)
+
+func newSpecForTest(image, cell string) *Spec {
+ return &Spec{
+ GlobalLockserver: planetscalev2.VitessLockserverParams{
+ Implementation: "etcd2",
+ Address: "etcd:2379",
+ RootPath: "/vitess/global",
+ },
+ Keyspace: "commerce",
+ Shard: "-",
+ Cell: cell,
+ Image: image,
+ }
+}
+
+func TestSpecFlagsAlwaysIncludesBaseFlags(t *testing.T) {
+ spec := newSpecForTest("vitess/lite:v24.0.0-mysql80", "zone1")
+ flags := spec.flags()
+
+ for _, key := range []string{
+ "topo_implementation",
+ "topo_global_server_address",
+ "topo_global_root",
+ "port",
+ "clusters_to_watch",
+ "logtostderr",
+ } {
+ _, ok := flags[key]
+ assert.Truef(t, ok, "expected base flag %q to be present", key)
+ }
+
+ assert.Equal(t, "commerce/-", flags["clusters_to_watch"])
+}
+
+func TestSpecFlagsCellGatedByVitessVersion(t *testing.T) {
+ tests := []struct {
+ name string
+ image string
+ wantCell bool
+ }{
+ {
+ name: "v24 emits cell",
+ image: "vitess/lite:v24.0.0-mysql80",
+ wantCell: true,
+ },
+ {
+ name: "v25 emits cell",
+ image: "vitess/lite:v25.0.0-mysql80",
+ wantCell: true,
+ },
+ {
+ name: "v24 rc emits cell",
+ image: "vitess/lite:v24.0.0-rc1-mysql80",
+ wantCell: true,
+ },
+ {
+ name: "v23 does not emit cell",
+ image: "vitess/lite:v23.0.5-mysql80",
+ wantCell: false,
+ },
+ {
+ name: "v22 does not emit cell",
+ image: "vitess/lite:v22.0.0-mysql80",
+ wantCell: false,
+ },
+ {
+ name: "rolling mysql tag does not emit cell",
+ image: "vitess/lite:mysql80",
+ wantCell: false,
+ },
+ {
+ name: "latest tag does not emit cell",
+ image: "vitess/lite:latest",
+ wantCell: false,
+ },
+ {
+ name: "empty image does not emit cell",
+ image: "",
+ wantCell: false,
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ spec := newSpecForTest(tc.image, "zone1")
+ flags := spec.flags()
+
+ got, present := flags["cell"]
+ if tc.wantCell {
+ assert.True(t, present, "expected --cell flag to be present")
+ assert.Equal(t, "zone1", got)
+ } else {
+ assert.False(t, present, "expected --cell flag to be absent, got %v", got)
+ }
+ })
+ }
+}
diff --git a/test/endtoend/upgrade_test.sh b/test/endtoend/upgrade_test.sh
index ad469212f..f2d1809fd 100755
--- a/test/endtoend/upgrade_test.sh
+++ b/test/endtoend/upgrade_test.sh
@@ -271,6 +271,9 @@ checkSemiSyncSetup
checkMysqldExporterMetrics
# Initially too durability policy should be specified
verifyDurabilityPolicy "commerce" "semi_sync"
+# VTOrc --cell is gated on Vitess v24+ in the operator. The initial cluster
+# pins v24.0.0-rc1, so the flag must be on the vtorc pod.
+checkPodSpecBySelectorWithTimeout example "planetscale.com/component=vtorc" 1 "--cell=zone1"
upgradeToLatest
verifyVtGateVersion "25.0.0"
verifyResourceSpec
|