mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-09-22 01:25:06 +02:00
[CLI] Validate node version (#2489)
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import json
|
||||
import pathlib
|
||||
import shutil
|
||||
import sys
|
||||
@@ -354,8 +353,7 @@ def build(
|
||||
with Runner() as runner, Progress(message="Pulling...") as set:
|
||||
if shutil.which("docker") is None:
|
||||
raise click.UsageError("Docker not installed") from None
|
||||
with open(config) as f:
|
||||
config_json = langgraph_cli.config.validate_config(json.load(f))
|
||||
config_json = langgraph_cli.config.validate_config_file(config)
|
||||
_build(
|
||||
runner, set, config, config_json, base_image, pull, tag, docker_build_args
|
||||
)
|
||||
@@ -435,8 +433,7 @@ tests
|
||||
def dockerfile(save_path: str, config: pathlib.Path, add_docker_compose: bool) -> None:
|
||||
save_path = pathlib.Path(save_path).absolute()
|
||||
secho(f"🔍 Validating configuration at path: {config}", fg="yellow")
|
||||
with open(config, encoding="utf-8") as f:
|
||||
config_json = langgraph_cli.config.validate_config(json.load(f))
|
||||
config_json = langgraph_cli.config.validate_config_file(config)
|
||||
secho("✅ Configuration validated!", fg="green")
|
||||
|
||||
secho(f"📝 Generating Dockerfile at {save_path}", fg="yellow")
|
||||
@@ -575,7 +572,7 @@ def dev(
|
||||
host: str,
|
||||
port: int,
|
||||
no_reload: bool,
|
||||
config: str,
|
||||
config: pathlib.Path,
|
||||
n_jobs_per_worker: Optional[int],
|
||||
no_browser: bool,
|
||||
debug_port: Optional[int],
|
||||
@@ -601,12 +598,9 @@ def dev(
|
||||
"Please ensure langgraph-cli is installed with the 'inmem' extra: pip install -U \"langgraph-cli[inmem]\""
|
||||
) from None
|
||||
|
||||
import json
|
||||
config_json = langgraph_cli.config.validate_config_file(config)
|
||||
|
||||
with open(config, encoding="utf-8") as f:
|
||||
config_data = json.load(f)
|
||||
|
||||
graphs = config_data.get("graphs", {})
|
||||
graphs = config_json.get("graphs", {})
|
||||
run_server(
|
||||
host,
|
||||
port,
|
||||
@@ -674,8 +668,7 @@ def prepare(
|
||||
debugger_base_url: Optional[str] = None,
|
||||
postgres_uri: Optional[str] = None,
|
||||
):
|
||||
with open(config_path) as f:
|
||||
config = langgraph_cli.config.validate_config(json.load(f))
|
||||
config_json = langgraph_cli.config.validate_config_file(config_path)
|
||||
# pull latest images
|
||||
if pull:
|
||||
runner.run(
|
||||
@@ -683,9 +676,9 @@ def prepare(
|
||||
"docker",
|
||||
"pull",
|
||||
(
|
||||
f"langchain/langgraphjs-api:{config['node_version']}"
|
||||
if config.get("node_version")
|
||||
else f"langchain/langgraph-api:{config['python_version']}"
|
||||
f"langchain/langgraphjs-api:{config_json['node_version']}"
|
||||
if config_json.get("node_version")
|
||||
else f"langchain/langgraph-api:{config_json['python_version']}"
|
||||
),
|
||||
verbose=verbose,
|
||||
)
|
||||
@@ -694,7 +687,7 @@ def prepare(
|
||||
args, stdin = prepare_args_and_stdin(
|
||||
capabilities=capabilities,
|
||||
config_path=config_path,
|
||||
config=config,
|
||||
config=config_json,
|
||||
docker_compose=docker_compose,
|
||||
port=port,
|
||||
watch=watch,
|
||||
|
||||
@@ -6,6 +6,9 @@ from typing import NamedTuple, Optional, TypedDict, Union
|
||||
|
||||
import click
|
||||
|
||||
MIN_NODE_VERSION = "20"
|
||||
MIN_PYTHON_VERSION = "3.11"
|
||||
|
||||
|
||||
class Config(TypedDict):
|
||||
python_version: str
|
||||
@@ -17,9 +20,6 @@ class Config(TypedDict):
|
||||
env: Union[dict[str, str], str]
|
||||
|
||||
|
||||
MIN_PYTHON_VERSION = "3.11"
|
||||
|
||||
|
||||
def _parse_version(version_str: str) -> tuple[int, int]:
|
||||
"""Parse a version string into a tuple of (major, minor)."""
|
||||
try:
|
||||
@@ -29,6 +29,19 @@ def _parse_version(version_str: str) -> tuple[int, int]:
|
||||
raise click.UsageError(f"Invalid version format: {version_str}") from None
|
||||
|
||||
|
||||
def _parse_node_version(version_str: str) -> int:
|
||||
"""Parse a Node.js version string into a major version number."""
|
||||
try:
|
||||
if "." in version_str:
|
||||
raise ValueError("Node.js version must be major version only")
|
||||
return int(version_str)
|
||||
except ValueError:
|
||||
raise click.UsageError(
|
||||
f"Invalid Node.js version format: {version_str}. "
|
||||
"Use major version only (e.g., '20')."
|
||||
) from None
|
||||
|
||||
|
||||
def validate_config(config: Config) -> Config:
|
||||
config = (
|
||||
{
|
||||
@@ -49,11 +62,17 @@ def validate_config(config: Config) -> Config:
|
||||
)
|
||||
|
||||
if config.get("node_version"):
|
||||
if config["node_version"] not in ("20",):
|
||||
raise click.UsageError(
|
||||
f"Unsupported Node.js version: {config['node_version']}. "
|
||||
"Currently only `node_version: \"20\"` is supported."
|
||||
)
|
||||
node_version = config["node_version"]
|
||||
try:
|
||||
major = _parse_node_version(node_version)
|
||||
min_major = _parse_node_version(MIN_NODE_VERSION)
|
||||
if major < min_major:
|
||||
raise click.UsageError(
|
||||
f"Node.js version {node_version} is not supported. "
|
||||
f"Minimum required version is {MIN_NODE_VERSION}."
|
||||
)
|
||||
except ValueError as e:
|
||||
raise click.UsageError(str(e)) from None
|
||||
|
||||
if config.get("python_version"):
|
||||
pyversion = config["python_version"]
|
||||
@@ -85,6 +104,48 @@ def validate_config(config: Config) -> Config:
|
||||
return config
|
||||
|
||||
|
||||
def validate_config_file(config_path: pathlib.Path) -> Config:
|
||||
with open(config_path) as f:
|
||||
config = json.load(f)
|
||||
validated = validate_config(config)
|
||||
# Enforce the package.json doesn't enforce an
|
||||
# incompatible Node.js version
|
||||
if validated.get("node_version"):
|
||||
package_json_path = config_path.parent / "package.json"
|
||||
if package_json_path.is_file():
|
||||
try:
|
||||
with open(package_json_path) as f:
|
||||
package_json = json.load(f)
|
||||
if "engines" in package_json:
|
||||
engines = package_json["engines"]
|
||||
if any(engine != "node" for engine in engines.keys()):
|
||||
raise click.UsageError(
|
||||
"Only 'node' engine is supported in package.json engines."
|
||||
f" Got engines: {list(engines.keys())}"
|
||||
)
|
||||
if engines:
|
||||
node_version = engines["node"]
|
||||
try:
|
||||
major = _parse_node_version(node_version)
|
||||
min_major = _parse_node_version(MIN_NODE_VERSION)
|
||||
if major < min_major:
|
||||
raise click.UsageError(
|
||||
f"Node.js version in package.json engines must be >= {MIN_NODE_VERSION} "
|
||||
f"(major version only), got '{node_version}'. Minor/patch versions "
|
||||
"(like '20.x.y') are not supported to prevent deployment issues "
|
||||
"when new Node.js versions are released."
|
||||
)
|
||||
except ValueError as e:
|
||||
raise click.UsageError(str(e)) from None
|
||||
|
||||
except json.JSONDecodeError:
|
||||
raise click.UsageError(
|
||||
"Invalid package.json found in langgraph "
|
||||
f"config directory {package_json_path}: file is not valid JSON"
|
||||
) from None
|
||||
return validated
|
||||
|
||||
|
||||
class LocalDeps(NamedTuple):
|
||||
pip_reqs: list[tuple[pathlib.Path, str]]
|
||||
real_pkgs: dict[pathlib.Path, str]
|
||||
|
||||
Reference in New Issue
Block a user