From b809832ea34cdc5abd8d89baf79db404f214077c Mon Sep 17 00:00:00 2001 From: Hugo Durand Date: Tue, 22 Sep 2026 14:43:45 -0400 Subject: [PATCH] feat(cli): refuse listener flags where placement cannot apply --- libs/cli/langgraph_cli/deploy.py | 18 +++++++++++++--- .../unit_tests/cli/test_deploy_command.py | 21 +++++++++++++++++++ .../tests/unit_tests/test_deploy_helpers.py | 10 +++++++++ 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/libs/cli/langgraph_cli/deploy.py b/libs/cli/langgraph_cli/deploy.py index 892046647..236566f45 100644 --- a/libs/cli/langgraph_cli/deploy.py +++ b/libs/cli/langgraph_cli/deploy.py @@ -219,14 +219,14 @@ class RequestedPlacement: return listeners[0] raise click.UsageError( "This workspace has several listeners. Choose one with " - f"--listener-id:\n{_describe(listeners)}" + f"--listener-id:\n{_describe_listeners(listeners)}" ) for listener in listeners: if listener.id == self.listener_id: return listener raise click.UsageError( f"Listener {self.listener_id} was not found in this workspace. " - f"Available listeners:\n{_describe(listeners)}" + f"Available listeners:\n{_describe_listeners(listeners)}" ) def _namespace(self, listener: Listener) -> str: @@ -250,7 +250,7 @@ class RequestedPlacement: return self.k8s_namespace -def _describe(listeners: Sequence[Listener]) -> str: +def _describe_listeners(listeners: Sequence[Listener]) -> str: return "\n".join( f" {listener.id} cluster {listener.compute_id} " f"namespaces: {', '.join(listener.namespaces)}" @@ -1502,6 +1502,13 @@ class CustomerRegistrySource: self, ctx: DeployContext, existing: ExistingDeployment, step: int ) -> DeployOutcome: _ensure_customer_registry_source(existing) + if self.placement.requested: + raise click.UsageError( + f"Deployment {existing.id} already exists, and its listener and " + "namespace are fixed when the deployment is created. Drop " + "--listener-id and --k8s-namespace, or use a different --name to " + "create a new deployment." + ) image_uri, step = self._publish(ctx, step) _log_deploy_step(step, f"Updating deployment {existing.id}") updated = ctx.client.update_deployment( @@ -1593,6 +1600,11 @@ def _select_source( remote_build_flag: bool | None, placement: RequestedPlacement, ) -> DeploymentSource: + if push_to is None and placement.requested: + raise click.UsageError( + "--listener-id and --k8s-namespace only apply when creating a " + "deployment with --push-to." + ) if push_to is not None: if remote_build_flag is True: raise click.UsageError("--push-to cannot be combined with --remote.") diff --git a/libs/cli/tests/unit_tests/cli/test_deploy_command.py b/libs/cli/tests/unit_tests/cli/test_deploy_command.py index 5cff266b1..e148ae009 100644 --- a/libs/cli/tests/unit_tests/cli/test_deploy_command.py +++ b/libs/cli/tests/unit_tests/cli/test_deploy_command.py @@ -789,3 +789,24 @@ def test_updating_a_deployment_never_looks_up_listeners( assert result.exit_code == 0, result.output assert LIST_LISTENERS not in deploy_project.timeline + + +def test_listener_flags_are_refused_on_an_existing_deployment( + deploy_project: DeployProject, +) -> None: + deploy_project.control_plane.listeners = [LISTENER] + deploy_project.control_plane.existing_deployments = [ + {"id": "dep-ext", "name": "my-app", "source": "external_docker"} + ] + + result = deploy_project.run( + "--push-to", + PUSH_REPOSITORY, + "--listener-id", + "listener-1", + host_url=CLOUD_CONTROL_PLANE_URL, + ) + + assert result.exit_code != 0 + assert "fixed when the deployment is created" in result.output + assert deploy_project.docker.verbs() == [] diff --git a/libs/cli/tests/unit_tests/test_deploy_helpers.py b/libs/cli/tests/unit_tests/test_deploy_helpers.py index 820aa7b8e..524b9fc5a 100644 --- a/libs/cli/tests/unit_tests/test_deploy_helpers.py +++ b/libs/cli/tests/unit_tests/test_deploy_helpers.py @@ -740,6 +740,16 @@ class TestSelectSource: "--image cannot be combined with --remote builds.", id="image_with_remote", ), + pytest.param( + {"placement": RequestedPlacement(listener_id="listener-1")}, + "only apply when creating a deployment with --push-to", + id="listener_without_push_to", + ), + pytest.param( + {"placement": RequestedPlacement(k8s_namespace="agents")}, + "only apply when creating a deployment with --push-to", + id="namespace_without_push_to", + ), ], ) def test_conflicting_flags_are_rejected(self, monkeypatch, flags, message):