mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-17 21:25:46 +02:00
fix(cli): add monorepo commands to dockerfile
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
co-authored by
open-swe[bot] <open-swe@users.noreply.github.com>
parent
49ae27c2ae
commit
f3de8ed373
@@ -15,7 +15,11 @@ from langgraph_cli.analytics import log_command
|
||||
from langgraph_cli.config import Config
|
||||
from langgraph_cli.constants import DEFAULT_CONFIG, DEFAULT_PORT
|
||||
from langgraph_cli.deploy import deploy
|
||||
from langgraph_cli.docker import DockerCapabilities, build_docker_image
|
||||
from langgraph_cli.docker import (
|
||||
DockerCapabilities,
|
||||
build_docker_image,
|
||||
get_config_to_docker_build_context,
|
||||
)
|
||||
from langgraph_cli.exec import Runner, subp_exec
|
||||
from langgraph_cli.progress import Progress
|
||||
from langgraph_cli.templates import TEMPLATE_HELP_STRING, create_new
|
||||
@@ -546,6 +550,14 @@ tests
|
||||
)
|
||||
@OPT_API_VERSION
|
||||
@OPT_ENGINE_RUNTIME_MODE
|
||||
@click.option(
|
||||
"--install-command",
|
||||
help="Custom install command to run from the build context root. If not provided, auto-detects based on package manager files.",
|
||||
)
|
||||
@click.option(
|
||||
"--build-command",
|
||||
help="Custom build command to run from the langgraph.json directory. If not provided, uses default build process.",
|
||||
)
|
||||
@log_command
|
||||
def dockerfile(
|
||||
save_path: str,
|
||||
@@ -554,9 +566,24 @@ def dockerfile(
|
||||
base_image: str | None = None,
|
||||
api_version: str | None = None,
|
||||
engine_runtime_mode: str = "combined_queue_worker",
|
||||
install_command: str | None = None,
|
||||
build_command: str | None = None,
|
||||
) -> None:
|
||||
from click import secho
|
||||
|
||||
if install_command and langgraph_cli.config.has_disallowed_build_command_content(
|
||||
install_command
|
||||
):
|
||||
raise click.UsageError(
|
||||
"install_command contains disallowed characters or patterns."
|
||||
)
|
||||
if build_command and langgraph_cli.config.has_disallowed_build_command_content(
|
||||
build_command
|
||||
):
|
||||
raise click.UsageError(
|
||||
"build_command contains disallowed characters or patterns."
|
||||
)
|
||||
|
||||
save_path = pathlib.Path(save_path).absolute()
|
||||
secho(f"🔍 Validating configuration at path: {config}", fg="yellow")
|
||||
config_json = langgraph_cli.config.validate_config_file(config)
|
||||
@@ -569,12 +596,21 @@ def dockerfile(
|
||||
config_json, engine_runtime_mode=engine_runtime_mode
|
||||
)
|
||||
|
||||
build_context = get_config_to_docker_build_context(
|
||||
config_json,
|
||||
install_command=install_command,
|
||||
build_command=build_command,
|
||||
)
|
||||
|
||||
secho(f"📝 Generating Dockerfile at {save_path}", fg="yellow")
|
||||
dockerfile_content, additional_contexts = langgraph_cli.config.config_to_docker(
|
||||
config_path=config,
|
||||
config=config_json,
|
||||
base_image=effective_base_image,
|
||||
api_version=api_version,
|
||||
install_command=install_command,
|
||||
build_command=build_command,
|
||||
build_context=build_context,
|
||||
)
|
||||
with open(str(save_path), "w", encoding="utf-8") as f:
|
||||
f.write(dockerfile_content)
|
||||
|
||||
@@ -330,6 +330,20 @@ def compose(
|
||||
return compose_str
|
||||
|
||||
|
||||
def get_config_to_docker_build_context(
|
||||
config_json: dict,
|
||||
install_command: str | None = None,
|
||||
build_command: str | None = None,
|
||||
) -> str | None:
|
||||
"""Return the build context passed to config_to_docker, if overridden."""
|
||||
is_js_project = config_json.get("node_version") and not config_json.get(
|
||||
"python_version"
|
||||
)
|
||||
if is_js_project and (build_command or install_command):
|
||||
return str(pathlib.Path.cwd())
|
||||
return None
|
||||
|
||||
|
||||
def build_docker_image(
|
||||
runner,
|
||||
set: Callable[[str], None],
|
||||
@@ -365,16 +379,12 @@ def build_docker_image(
|
||||
"-t",
|
||||
tag,
|
||||
]
|
||||
# determine build context: use current directory for JS projects, config parent for Python
|
||||
is_js_project = config_json.get("node_version") and not config_json.get(
|
||||
"python_version"
|
||||
config_to_docker_build_context = get_config_to_docker_build_context(
|
||||
config_json,
|
||||
install_command=install_command,
|
||||
build_command=build_command,
|
||||
)
|
||||
# build/install commands only apply to JS projects for now
|
||||
# without install/build command, JS projects will follow the old behavior
|
||||
if is_js_project and (build_command or install_command):
|
||||
build_context = str(pathlib.Path.cwd())
|
||||
else:
|
||||
build_context = str(config.parent)
|
||||
build_context = config_to_docker_build_context or str(config.parent)
|
||||
|
||||
# Deep copy to avoid mutating the caller's config (config_to_docker
|
||||
# rewrites graph paths to container-internal paths in place).
|
||||
@@ -386,7 +396,7 @@ def build_docker_image(
|
||||
api_version=api_version,
|
||||
install_command=install_command,
|
||||
build_command=build_command,
|
||||
build_context=build_context,
|
||||
build_context=config_to_docker_build_context,
|
||||
)
|
||||
# add additional_contexts
|
||||
if additional_contexts:
|
||||
|
||||
@@ -1088,6 +1088,82 @@ def test_dockerfile_command_with_api_version_nodejs() -> None:
|
||||
assert "FROM langchain/langgraphjs-api:0.2.74-node20" in dockerfile
|
||||
|
||||
|
||||
def test_dockerfile_command_nodejs_monorepo_commands() -> None:
|
||||
runner = CliRunner()
|
||||
|
||||
with runner.isolated_filesystem():
|
||||
root = pathlib.Path.cwd()
|
||||
config_dir = root / "apps" / "agent"
|
||||
graph_path = config_dir / "src" / "agent.ts"
|
||||
graph_path.parent.mkdir(parents=True)
|
||||
graph_path.touch()
|
||||
(root / "package.json").write_text(
|
||||
json.dumps({"packageManager": "pnpm@10.0.0"})
|
||||
)
|
||||
(root / "pnpm-lock.yaml").touch()
|
||||
(root / "pnpm-workspace.yaml").write_text('packages:\n - "apps/*"\n')
|
||||
config_path = config_dir / "langgraph.json"
|
||||
config_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"node_version": "20",
|
||||
"graphs": {"agent": "src/agent.ts:graph"},
|
||||
"image_distro": "wolfi",
|
||||
}
|
||||
)
|
||||
)
|
||||
save_path = root / "Dockerfile"
|
||||
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[
|
||||
"dockerfile",
|
||||
str(save_path),
|
||||
"--config",
|
||||
str(config_path),
|
||||
"--install-command",
|
||||
"pnpm install --frozen-lockfile",
|
||||
"--build-command",
|
||||
"pnpm run build",
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
assert save_path.exists()
|
||||
dockerfile = save_path.read_text()
|
||||
container_root = f"/deps/{root.name}"
|
||||
assert f"ADD . {container_root}" in dockerfile
|
||||
assert f"WORKDIR {container_root}" in dockerfile
|
||||
assert "RUN pnpm install --frozen-lockfile" in dockerfile
|
||||
assert f"WORKDIR {container_root}/apps/agent" in dockerfile
|
||||
assert "RUN pnpm run build" in dockerfile
|
||||
|
||||
|
||||
def test_dockerfile_command_rejects_disallowed_nodejs_commands() -> None:
|
||||
runner = CliRunner()
|
||||
config_content = {
|
||||
"node_version": "20",
|
||||
"graphs": {"agent": "agent.js:graph"},
|
||||
}
|
||||
|
||||
with temporary_config_folder(config_content) as temp_dir:
|
||||
(temp_dir / "agent.js").touch()
|
||||
for option in ("--install-command", "--build-command"):
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[
|
||||
"dockerfile",
|
||||
str(temp_dir / "Dockerfile"),
|
||||
"--config",
|
||||
str(temp_dir / "config.json"),
|
||||
option,
|
||||
"npm install; echo bad",
|
||||
],
|
||||
)
|
||||
assert result.exit_code != 0
|
||||
assert "contains disallowed characters or patterns" in result.output
|
||||
|
||||
|
||||
def test_build_command_with_api_version() -> None:
|
||||
"""Test the 'build' command with --api-version flag."""
|
||||
runner = CliRunner()
|
||||
|
||||
Reference in New Issue
Block a user