fix(cli): add monorepo commands to dockerfile

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
hari-dhanushkodi
2026-07-20 17:26:01 +00:00
co-authored by open-swe[bot] <open-swe@users.noreply.github.com>
parent 49ae27c2ae
commit f3de8ed373
3 changed files with 133 additions and 11 deletions
+37 -1
View File
@@ -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)
+20 -10
View File
@@ -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:
+76
View File
@@ -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()