feat(cli): allow prebuild images for langgraph deploy (#8100)

Fixes #

adds a --image flag to `langgraph deploy`, allows a user to `langgraph
build` a custom image and then deploy this specific image with
`langgraph deploy`.

---------

Co-authored-by: hari-dhanushkodi <203702815+hari-dhanushkodi@users.noreply.github.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
hari-dhanushkodi
2026-06-25 03:32:42 +00:00
committed by GitHub
co-authored by hari-dhanushkodi open-swe[bot] <open-swe@users.noreply.github.com>
parent 8c9d59c842
commit 3df6f50ad5
3 changed files with 173 additions and 47 deletions
@@ -1,3 +1,4 @@
import asyncio
import base64
import io
import json
@@ -6,9 +7,11 @@ import sys
from unittest.mock import MagicMock
import click
import click.exceptions
import httpx
import pytest
import langgraph_cli.deploy as deploy_mod
from langgraph_cli.deploy import (
_call_host_backend_with_optional_tenant,
_create_host_backend_client,
@@ -19,6 +22,7 @@ from langgraph_cli.deploy import (
_resolve_env_path,
_resolve_pushed_image_digest,
_smith_dashboard_base_url,
_validate_prebuilt_image,
normalize_image_tag,
normalize_name,
)
@@ -95,6 +99,65 @@ class TestNormalizeImageTag:
normalize_image_tag("has space")
class _FakeRunner:
def run(self, coro):
return asyncio.run(coro)
class TestValidatePrebuiltImage:
def test_accepts_linux_amd64(self, monkeypatch):
calls = []
async def fake_subp_exec(*args, **kwargs):
calls.append((args, kwargs))
return "linux/amd64\n", None
monkeypatch.setattr(deploy_mod, "subp_exec", fake_subp_exec)
_validate_prebuilt_image(_FakeRunner(), "repo/app:tag", verbose=False)
assert calls == [
(
(
"docker",
"image",
"inspect",
"--format",
"{{.Os}}/{{.Architecture}}",
"repo/app:tag",
),
{"verbose": False, "collect": True},
)
]
def test_missing_docker_binary_raises_actionable_error(self, monkeypatch):
async def fake_subp_exec(*args, **kwargs):
raise FileNotFoundError("docker")
monkeypatch.setattr(deploy_mod, "subp_exec", fake_subp_exec)
with pytest.raises(click.ClickException, match="Docker is required"):
_validate_prebuilt_image(_FakeRunner(), "repo/app:tag", verbose=False)
def test_missing_image_raises_actionable_error(self, monkeypatch):
async def fake_subp_exec(*args, **kwargs):
raise click.exceptions.Exit(1)
monkeypatch.setattr(deploy_mod, "subp_exec", fake_subp_exec)
with pytest.raises(click.ClickException, match="not found locally"):
_validate_prebuilt_image(_FakeRunner(), "missing:tag", verbose=False)
def test_rejects_non_amd64_platform(self, monkeypatch):
async def fake_subp_exec(*args, **kwargs):
return "linux/arm64\n", None
monkeypatch.setattr(deploy_mod, "subp_exec", fake_subp_exec)
with pytest.raises(click.ClickException, match="requires linux/amd64"):
_validate_prebuilt_image(_FakeRunner(), "repo/app:arm", verbose=False)
class TestParseEnvFromConfig:
def test_env_dict(self, tmp_path):
config_path = tmp_path / "langgraph.json"