diff --git a/libs/cli/langgraph_cli/deploy.py b/libs/cli/langgraph_cli/deploy.py index e561585cf..b21e09e49 100644 --- a/libs/cli/langgraph_cli/deploy.py +++ b/libs/cli/langgraph_cli/deploy.py @@ -157,7 +157,10 @@ class Listener: namespaces: tuple[str, ...] @classmethod - def from_resource(cls, resource: Mapping[str, object]) -> "Listener": + def from_resource(cls, resource: Mapping[str, object]) -> "Listener | None": + identifier = str(resource.get("id") or "") + if not identifier: + return None compute_config = resource.get("compute_config") namespaces = ( compute_config.get("k8s_namespaces") @@ -165,7 +168,7 @@ class Listener: else None ) return cls( - str(resource.get("id", "")), + identifier, str(resource.get("compute_id", "")), tuple(str(namespace) for namespace in namespaces) if isinstance(namespaces, list) @@ -1445,7 +1448,8 @@ def _available_listeners(client: HostBackendClient) -> tuple[Listener, ...]: resources = _call_host_backend_with_optional_tenant( client, lambda c: c.list_listeners() ) - return tuple(Listener.from_resource(resource) for resource in resources) + listeners = (Listener.from_resource(resource) for resource in resources) + return tuple(listener for listener in listeners if listener is not None) def _ensure_customer_registry_source(existing: ExistingDeployment) -> None: @@ -1510,7 +1514,7 @@ class RemoteBuildSource: class CustomerRegistrySource: reference: ImageReference prebuilt_image: str | None - placement: RequestedPlacement = RequestedPlacement() + placement: RequestedPlacement def run(self, ctx: DeployContext) -> DeployOutcome: if isinstance(ctx.selector, ById): @@ -1648,7 +1652,9 @@ def _select_source( reference = _push_reference(push_to, tag) if image is None: _require_local_docker() - return CustomerRegistrySource(reference, image, placement) + return CustomerRegistrySource( + reference=reference, prebuilt_image=image, placement=placement + ) if image and remote_build_flag is True: raise click.UsageError("--image cannot be combined with --remote builds.") use_remote_build, local_build_error = _resolve_build_mode( diff --git a/libs/cli/langgraph_cli/host_backend.py b/libs/cli/langgraph_cli/host_backend.py index b7c3a7eea..535c84c4e 100644 --- a/libs/cli/langgraph_cli/host_backend.py +++ b/libs/cli/langgraph_cli/host_backend.py @@ -280,7 +280,8 @@ class HostBackendClient: return _resources( self._request( "GET", - f"/v2/deployments/{deployment_id}/revisions?limit={limit}", + f"/v2/deployments/{deployment_id}/revisions", + params={"limit": limit}, ) ) 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 04e053232..3d54b1c76 100644 --- a/libs/cli/tests/unit_tests/cli/test_deploy_command.py +++ b/libs/cli/tests/unit_tests/cli/test_deploy_command.py @@ -914,3 +914,21 @@ def test_a_truncated_listener_page_says_so(deploy_project: DeployProject) -> Non assert result.exit_code != 0 assert "first 100" in result.output + + +def test_a_listener_without_an_id_is_ignored(deploy_project: DeployProject) -> None: + deploy_project.control_plane.listeners = [ + {"compute_id": "broken", "compute_config": {"k8s_namespaces": ["agents"]}}, + LISTENER, + ] + + result = deploy_project.run( + "--push-to", PUSH_REPOSITORY, host_url=CLOUD_CONTROL_PLANE_URL + ) + + assert result.exit_code == 0, result.output + assert deploy_project.control_plane.bodies[CREATE_DEPLOYMENT]["source_config"] == { + "resource_spec": {}, + "listener_id": "listener-1", + "listener_config": {"k8s_namespace": "agents"}, + } diff --git a/libs/cli/tests/unit_tests/test_deploy_helpers.py b/libs/cli/tests/unit_tests/test_deploy_helpers.py index e9e479aa2..ee732216c 100644 --- a/libs/cli/tests/unit_tests/test_deploy_helpers.py +++ b/libs/cli/tests/unit_tests/test_deploy_helpers.py @@ -626,7 +626,9 @@ class TestSelectSource: {"push_to": REPOSITORY}, True, CustomerRegistrySource( - ImageReference(REPOSITORY, "latest"), prebuilt_image=None + reference=ImageReference(REPOSITORY, "latest"), + prebuilt_image=None, + placement=RequestedPlacement(), ), id="push_to_selects_the_external_source_with_the_default_tag", ), @@ -634,7 +636,9 @@ class TestSelectSource: {"push_to": f"{REPOSITORY}:v2"}, True, CustomerRegistrySource( - ImageReference(REPOSITORY, "v2"), prebuilt_image=None + reference=ImageReference(REPOSITORY, "v2"), + prebuilt_image=None, + placement=RequestedPlacement(), ), id="push_to_keeps_a_tag_given_in_the_reference", ), @@ -642,7 +646,9 @@ class TestSelectSource: {"push_to": REPOSITORY, "tag": "v3"}, True, CustomerRegistrySource( - ImageReference(REPOSITORY, "v3"), prebuilt_image=None + reference=ImageReference(REPOSITORY, "v3"), + prebuilt_image=None, + placement=RequestedPlacement(), ), id="tag_flag_composes_with_push_to", ), @@ -650,7 +656,9 @@ class TestSelectSource: {"push_to": REPOSITORY, "image": "app:dev"}, False, CustomerRegistrySource( - ImageReference(REPOSITORY, "latest"), prebuilt_image="app:dev" + reference=ImageReference(REPOSITORY, "latest"), + prebuilt_image="app:dev", + placement=RequestedPlacement(), ), id="prebuilt_image_is_retagged_for_push_to_without_docker_checks", ), @@ -661,9 +669,9 @@ class TestSelectSource: }, True, CustomerRegistrySource( - ImageReference(REPOSITORY, "latest"), - None, - RequestedPlacement("listener-1", "agents"), + reference=ImageReference(REPOSITORY, "latest"), + prebuilt_image=None, + placement=RequestedPlacement("listener-1", "agents"), ), id="push_to_carries_the_requested_placement", ), @@ -952,6 +960,8 @@ class TestListener: Listener("listener-1", "", ()), id="only_an_id", ), + pytest.param({"compute_id": "c"}, None, id="no_id_is_not_a_listener"), + pytest.param({"id": ""}, None, id="empty_id_is_not_a_listener"), ], ) def test_from_resource_reads_the_control_plane_shape(self, resource, expected):