mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-09-20 16:47:55 +02:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46e524d96c |
@@ -1,5 +1,7 @@
|
|||||||
"""CLI entrypoint for LangGraph API server."""
|
"""CLI entrypoint for LangGraph API server."""
|
||||||
|
|
||||||
|
import difflib
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import shutil
|
import shutil
|
||||||
@@ -165,6 +167,10 @@ def cli():
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _format_json(value: object) -> str:
|
||||||
|
return json.dumps(value, indent=2, sort_keys=True) + os.linesep
|
||||||
|
|
||||||
|
|
||||||
@OPT_RECREATE
|
@OPT_RECREATE
|
||||||
@OPT_PULL
|
@OPT_PULL
|
||||||
@OPT_PORT
|
@OPT_PORT
|
||||||
@@ -606,6 +612,160 @@ def dockerfile(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.group(
|
||||||
|
"build-spec",
|
||||||
|
help="🧾 Export or verify a machine-readable build specification for hermetic builds.",
|
||||||
|
)
|
||||||
|
def build_spec() -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@OPT_CONFIG
|
||||||
|
@click.option(
|
||||||
|
"--base-image",
|
||||||
|
help="Base image to use for the LangGraph API server. Defaults to langchain/langgraph-api or langchain/langgraphjs-api.",
|
||||||
|
)
|
||||||
|
@OPT_API_VERSION
|
||||||
|
@click.option(
|
||||||
|
"--install-command",
|
||||||
|
help="Custom install command (Node projects only). If omitted, auto-detects based on package manager files.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--build-command",
|
||||||
|
help="Custom build command to run from the langgraph.json directory (Node projects only).",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--output",
|
||||||
|
"-o",
|
||||||
|
type=click.Path(
|
||||||
|
exists=False,
|
||||||
|
file_okay=True,
|
||||||
|
dir_okay=False,
|
||||||
|
resolve_path=True,
|
||||||
|
path_type=pathlib.Path,
|
||||||
|
),
|
||||||
|
help="Path to write the build spec JSON. If omitted, prints to stdout.",
|
||||||
|
)
|
||||||
|
@build_spec.command("export")
|
||||||
|
@log_command
|
||||||
|
def build_spec_export(
|
||||||
|
config: pathlib.Path,
|
||||||
|
base_image: str | None,
|
||||||
|
api_version: str | None,
|
||||||
|
install_command: str | None,
|
||||||
|
build_command: str | None,
|
||||||
|
output: pathlib.Path | None,
|
||||||
|
) -> None:
|
||||||
|
config_json = langgraph_cli.config.validate_config_file(config)
|
||||||
|
warn_non_wolfi_distro(config_json)
|
||||||
|
|
||||||
|
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):
|
||||||
|
build_context = str(pathlib.Path.cwd())
|
||||||
|
else:
|
||||||
|
build_context = str(config.parent)
|
||||||
|
|
||||||
|
spec = langgraph_cli.config.config_to_build_spec(
|
||||||
|
config_path=config,
|
||||||
|
config=config_json,
|
||||||
|
base_image=base_image,
|
||||||
|
api_version=api_version,
|
||||||
|
install_command=install_command,
|
||||||
|
build_command=build_command,
|
||||||
|
build_context=build_context,
|
||||||
|
)
|
||||||
|
rendered = _format_json(spec)
|
||||||
|
|
||||||
|
if output is None:
|
||||||
|
click.echo(rendered, nl=False)
|
||||||
|
return
|
||||||
|
|
||||||
|
output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(output, "w", encoding="utf-8") as f:
|
||||||
|
f.write(rendered)
|
||||||
|
secho(f"✅ Created build spec: {output}", fg="green")
|
||||||
|
|
||||||
|
|
||||||
|
@OPT_CONFIG
|
||||||
|
@click.argument(
|
||||||
|
"spec_path",
|
||||||
|
type=click.Path(
|
||||||
|
exists=True,
|
||||||
|
file_okay=True,
|
||||||
|
dir_okay=False,
|
||||||
|
resolve_path=True,
|
||||||
|
path_type=pathlib.Path,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--base-image",
|
||||||
|
help="Base image to use for the LangGraph API server. Defaults to langchain/langgraph-api or langchain/langgraphjs-api.",
|
||||||
|
)
|
||||||
|
@OPT_API_VERSION
|
||||||
|
@click.option(
|
||||||
|
"--install-command",
|
||||||
|
help="Custom install command (Node projects only). If omitted, auto-detects based on package manager files.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--build-command",
|
||||||
|
help="Custom build command to run from the langgraph.json directory (Node projects only).",
|
||||||
|
)
|
||||||
|
@build_spec.command("verify")
|
||||||
|
@log_command
|
||||||
|
def build_spec_verify(
|
||||||
|
config: pathlib.Path,
|
||||||
|
spec_path: pathlib.Path,
|
||||||
|
base_image: str | None,
|
||||||
|
api_version: str | None,
|
||||||
|
install_command: str | None,
|
||||||
|
build_command: str | None,
|
||||||
|
) -> None:
|
||||||
|
config_json = langgraph_cli.config.validate_config_file(config)
|
||||||
|
warn_non_wolfi_distro(config_json)
|
||||||
|
|
||||||
|
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):
|
||||||
|
build_context = str(pathlib.Path.cwd())
|
||||||
|
else:
|
||||||
|
build_context = str(config.parent)
|
||||||
|
|
||||||
|
expected = langgraph_cli.config.config_to_build_spec(
|
||||||
|
config_path=config,
|
||||||
|
config=config_json,
|
||||||
|
base_image=base_image,
|
||||||
|
api_version=api_version,
|
||||||
|
install_command=install_command,
|
||||||
|
build_command=build_command,
|
||||||
|
build_context=build_context,
|
||||||
|
)
|
||||||
|
expected_str = _format_json(expected)
|
||||||
|
|
||||||
|
with open(spec_path, encoding="utf-8") as f:
|
||||||
|
actual = json.load(f)
|
||||||
|
actual_str = _format_json(actual)
|
||||||
|
|
||||||
|
if actual_str == expected_str:
|
||||||
|
secho(f"✅ Build spec is in sync: {spec_path}", fg="green")
|
||||||
|
return
|
||||||
|
|
||||||
|
diff = "\n".join(
|
||||||
|
difflib.unified_diff(
|
||||||
|
actual_str.splitlines(),
|
||||||
|
expected_str.splitlines(),
|
||||||
|
fromfile=str(spec_path),
|
||||||
|
tofile="generated",
|
||||||
|
lineterm="",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
raise click.ClickException(
|
||||||
|
f"Build spec is out of sync: {spec_path}\n\n{diff}"
|
||||||
|
) from None
|
||||||
|
|
||||||
|
|
||||||
@click.option(
|
@click.option(
|
||||||
"--host",
|
"--host",
|
||||||
default="127.0.0.1",
|
default="127.0.0.1",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import pathlib
|
|||||||
import re
|
import re
|
||||||
import textwrap
|
import textwrap
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from typing import Literal, NamedTuple
|
from typing import Any, Literal, NamedTuple
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
@@ -874,6 +874,55 @@ def get_build_tools_to_uninstall(config: Config) -> tuple[str]:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_langgraph_env(
|
||||||
|
config: Config, *, runtime: Literal["python", "node"]
|
||||||
|
) -> dict[str, str]:
|
||||||
|
"""Build runtime environment variables that are serialized from config."""
|
||||||
|
env_map: dict[str, str] = {}
|
||||||
|
|
||||||
|
if (store_config := config.get("store")) is not None:
|
||||||
|
env_map["LANGGRAPH_STORE"] = json.dumps(store_config)
|
||||||
|
|
||||||
|
if (auth_config := config.get("auth")) is not None:
|
||||||
|
env_map["LANGGRAPH_AUTH"] = json.dumps(auth_config)
|
||||||
|
|
||||||
|
if (encryption_config := config.get("encryption")) is not None:
|
||||||
|
env_map["LANGGRAPH_ENCRYPTION"] = json.dumps(encryption_config)
|
||||||
|
|
||||||
|
if (http_config := config.get("http")) is not None:
|
||||||
|
env_map["LANGGRAPH_HTTP"] = json.dumps(http_config)
|
||||||
|
|
||||||
|
if (webhooks_config := config.get("webhooks")) is not None:
|
||||||
|
env_map["LANGGRAPH_WEBHOOKS"] = json.dumps(webhooks_config)
|
||||||
|
|
||||||
|
if (checkpointer_config := config.get("checkpointer")) is not None:
|
||||||
|
env_map["LANGGRAPH_CHECKPOINTER"] = json.dumps(checkpointer_config)
|
||||||
|
|
||||||
|
# Keep Python/Node behavior consistent with current Dockerfile generation:
|
||||||
|
# Python emits UI vars when explicitly set (including empty dict), while
|
||||||
|
# Node currently emits them only when truthy.
|
||||||
|
if runtime == "python":
|
||||||
|
if (ui := config.get("ui")) is not None:
|
||||||
|
env_map["LANGGRAPH_UI"] = json.dumps(ui)
|
||||||
|
if (ui_config := config.get("ui_config")) is not None:
|
||||||
|
env_map["LANGGRAPH_UI_CONFIG"] = json.dumps(ui_config)
|
||||||
|
else:
|
||||||
|
if ui := config.get("ui"):
|
||||||
|
env_map["LANGGRAPH_UI"] = json.dumps(ui)
|
||||||
|
if ui_config := config.get("ui_config"):
|
||||||
|
env_map["LANGGRAPH_UI_CONFIG"] = json.dumps(ui_config)
|
||||||
|
|
||||||
|
env_map["LANGSERVE_GRAPHS"] = json.dumps(config["graphs"])
|
||||||
|
return env_map
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_workdir(dockerfile: str) -> str | None:
|
||||||
|
for line in dockerfile.splitlines():
|
||||||
|
if line.startswith("WORKDIR "):
|
||||||
|
return line.removeprefix("WORKDIR ").strip() or None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def python_config_to_docker(
|
def python_config_to_docker(
|
||||||
config_path: pathlib.Path,
|
config_path: pathlib.Path,
|
||||||
config: Config,
|
config: Config,
|
||||||
@@ -1006,36 +1055,8 @@ ADD {relpath} /deps/{name}
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
env_vars = []
|
env_map = _build_langgraph_env(config, runtime="python")
|
||||||
|
env_vars = [f"ENV {key}='{value}'" for key, value in env_map.items()]
|
||||||
if (store_config := config.get("store")) is not None:
|
|
||||||
env_vars.append(f"ENV LANGGRAPH_STORE='{json.dumps(store_config)}'")
|
|
||||||
|
|
||||||
if (auth_config := config.get("auth")) is not None:
|
|
||||||
env_vars.append(f"ENV LANGGRAPH_AUTH='{json.dumps(auth_config)}'")
|
|
||||||
|
|
||||||
if (encryption_config := config.get("encryption")) is not None:
|
|
||||||
env_vars.append(f"ENV LANGGRAPH_ENCRYPTION='{json.dumps(encryption_config)}'")
|
|
||||||
|
|
||||||
if (http_config := config.get("http")) is not None:
|
|
||||||
env_vars.append(f"ENV LANGGRAPH_HTTP='{json.dumps(http_config)}'")
|
|
||||||
|
|
||||||
# Inject webhooks configuration if provided
|
|
||||||
if (webhooks_config := config.get("webhooks")) is not None:
|
|
||||||
env_vars.append(f"ENV LANGGRAPH_WEBHOOKS='{json.dumps(webhooks_config)}'")
|
|
||||||
|
|
||||||
if (checkpointer_config := config.get("checkpointer")) is not None:
|
|
||||||
env_vars.append(
|
|
||||||
f"ENV LANGGRAPH_CHECKPOINTER='{json.dumps(checkpointer_config)}'"
|
|
||||||
)
|
|
||||||
|
|
||||||
if (ui := config.get("ui")) is not None:
|
|
||||||
env_vars.append(f"ENV LANGGRAPH_UI='{json.dumps(ui)}'")
|
|
||||||
|
|
||||||
if (ui_config := config.get("ui_config")) is not None:
|
|
||||||
env_vars.append(f"ENV LANGGRAPH_UI_CONFIG='{json.dumps(ui_config)}'")
|
|
||||||
|
|
||||||
env_vars.append(f"ENV LANGSERVE_GRAPHS='{json.dumps(config['graphs'])}'")
|
|
||||||
|
|
||||||
js_inst_str: str = ""
|
js_inst_str: str = ""
|
||||||
if (config.get("ui") or config.get("node_version")) and local_deps.working_dir:
|
if (config.get("ui") or config.get("node_version")) and local_deps.working_dir:
|
||||||
@@ -1137,36 +1158,8 @@ def node_config_to_docker(
|
|||||||
|
|
||||||
image_str = docker_tag(config, base_image, api_version)
|
image_str = docker_tag(config, base_image, api_version)
|
||||||
|
|
||||||
env_vars: list[str] = []
|
env_map = _build_langgraph_env(config, runtime="node")
|
||||||
|
env_vars = [f"ENV {key}='{value}'" for key, value in env_map.items()]
|
||||||
if (store_config := config.get("store")) is not None:
|
|
||||||
env_vars.append(f"ENV LANGGRAPH_STORE='{json.dumps(store_config)}'")
|
|
||||||
|
|
||||||
if (auth_config := config.get("auth")) is not None:
|
|
||||||
env_vars.append(f"ENV LANGGRAPH_AUTH='{json.dumps(auth_config)}'")
|
|
||||||
|
|
||||||
if (encryption_config := config.get("encryption")) is not None:
|
|
||||||
env_vars.append(f"ENV LANGGRAPH_ENCRYPTION='{json.dumps(encryption_config)}'")
|
|
||||||
|
|
||||||
if (http_config := config.get("http")) is not None:
|
|
||||||
env_vars.append(f"ENV LANGGRAPH_HTTP='{json.dumps(http_config)}'")
|
|
||||||
|
|
||||||
# Inject webhooks configuration if provided
|
|
||||||
if (webhooks_config := config.get("webhooks")) is not None:
|
|
||||||
env_vars.append(f"ENV LANGGRAPH_WEBHOOKS='{json.dumps(webhooks_config)}'")
|
|
||||||
|
|
||||||
if (checkpointer_config := config.get("checkpointer")) is not None:
|
|
||||||
env_vars.append(
|
|
||||||
f"ENV LANGGRAPH_CHECKPOINTER='{json.dumps(checkpointer_config)}'"
|
|
||||||
)
|
|
||||||
|
|
||||||
if ui := config.get("ui"):
|
|
||||||
env_vars.append(f"ENV LANGGRAPH_UI='{json.dumps(ui)}'")
|
|
||||||
|
|
||||||
if ui_config := config.get("ui_config"):
|
|
||||||
env_vars.append(f"ENV LANGGRAPH_UI_CONFIG='{json.dumps(ui_config)}'")
|
|
||||||
|
|
||||||
env_vars.append(f"ENV LANGSERVE_GRAPHS='{json.dumps(config['graphs'])}'")
|
|
||||||
|
|
||||||
# For monorepo support, we need to handle install and build commands differently
|
# For monorepo support, we need to handle install and build commands differently
|
||||||
if build_context:
|
if build_context:
|
||||||
@@ -1292,6 +1285,50 @@ def config_to_docker(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def config_to_build_spec(
|
||||||
|
config_path: pathlib.Path,
|
||||||
|
config: Config,
|
||||||
|
*,
|
||||||
|
base_image: str | None = None,
|
||||||
|
api_version: str | None = None,
|
||||||
|
install_command: str | None = None,
|
||||||
|
build_command: str | None = None,
|
||||||
|
build_context: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Generate a machine-readable, versioned build specification."""
|
||||||
|
# Normalize via JSON round-trip to avoid mutating the caller's dictionary.
|
||||||
|
normalized: Config = json.loads(json.dumps(config))
|
||||||
|
is_node_runtime = bool(
|
||||||
|
normalized.get("node_version") and not normalized.get("python_version")
|
||||||
|
)
|
||||||
|
runtime: Literal["python", "node"] = "node" if is_node_runtime else "python"
|
||||||
|
resolved_base_image = docker_tag(normalized, base_image, api_version)
|
||||||
|
|
||||||
|
dockerfile, additional_contexts = config_to_docker(
|
||||||
|
config_path=config_path,
|
||||||
|
config=normalized,
|
||||||
|
base_image=base_image,
|
||||||
|
api_version=api_version,
|
||||||
|
install_command=install_command,
|
||||||
|
build_command=build_command,
|
||||||
|
build_context=build_context,
|
||||||
|
)
|
||||||
|
env_map = _build_langgraph_env(normalized, runtime=runtime)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"schema_version": 1,
|
||||||
|
"kind": "langgraph.build_spec",
|
||||||
|
"runtime": runtime,
|
||||||
|
"resolved_base_image": resolved_base_image,
|
||||||
|
"build_context": build_context,
|
||||||
|
"additional_contexts": additional_contexts,
|
||||||
|
"env": env_map,
|
||||||
|
"graphs": normalized["graphs"],
|
||||||
|
"working_dir": _extract_workdir(dockerfile),
|
||||||
|
"dockerfile": dockerfile,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def config_to_compose(
|
def config_to_compose(
|
||||||
config_path: pathlib.Path,
|
config_path: pathlib.Path,
|
||||||
config: Config,
|
config: Config,
|
||||||
|
|||||||
@@ -287,6 +287,148 @@ def test_version_option() -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_spec_export_command_python_to_stdout() -> None:
|
||||||
|
runner = CliRunner()
|
||||||
|
config_content = {
|
||||||
|
"python_version": "3.11",
|
||||||
|
"image_distro": "wolfi",
|
||||||
|
"graphs": {"agent": "agent.py:graph"},
|
||||||
|
"dependencies": ["."],
|
||||||
|
}
|
||||||
|
with temporary_config_folder(config_content) as temp_dir:
|
||||||
|
(temp_dir / "agent.py").touch()
|
||||||
|
result = runner.invoke(
|
||||||
|
cli,
|
||||||
|
["build-spec", "export", "--config", str(temp_dir / "config.json")],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0, result.output
|
||||||
|
spec = json.loads(result.output)
|
||||||
|
assert spec["schema_version"] == 1
|
||||||
|
assert spec["kind"] == "langgraph.build_spec"
|
||||||
|
assert spec["runtime"] == "python"
|
||||||
|
assert spec["resolved_base_image"] == "langchain/langgraph-api:3.11-wolfi"
|
||||||
|
assert spec["env"]["LANGSERVE_GRAPHS"] == '{"agent": "agent.py:graph"}'
|
||||||
|
assert spec["working_dir"] is not None
|
||||||
|
assert spec["working_dir"].startswith("/deps/")
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_spec_export_command_node_to_file() -> None:
|
||||||
|
runner = CliRunner()
|
||||||
|
config_content = {
|
||||||
|
"node_version": "20",
|
||||||
|
"image_distro": "wolfi",
|
||||||
|
"graphs": {"agent": "src/agent.ts:graph"},
|
||||||
|
"dependencies": ["."],
|
||||||
|
}
|
||||||
|
with temporary_config_folder(config_content) as temp_dir:
|
||||||
|
spec_path = temp_dir / "buildspec.json"
|
||||||
|
(temp_dir / "src").mkdir(parents=True, exist_ok=True)
|
||||||
|
(temp_dir / "src" / "agent.ts").touch()
|
||||||
|
(temp_dir / "package.json").write_text("{}", encoding="utf-8")
|
||||||
|
|
||||||
|
result = runner.invoke(
|
||||||
|
cli,
|
||||||
|
[
|
||||||
|
"build-spec",
|
||||||
|
"export",
|
||||||
|
"--config",
|
||||||
|
str(temp_dir / "config.json"),
|
||||||
|
"--output",
|
||||||
|
str(spec_path),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0, result.output
|
||||||
|
assert spec_path.exists()
|
||||||
|
with open(spec_path, encoding="utf-8") as f:
|
||||||
|
spec = json.load(f)
|
||||||
|
assert spec["runtime"] == "node"
|
||||||
|
assert spec["resolved_base_image"] == "langchain/langgraphjs-api:20-wolfi"
|
||||||
|
assert spec["working_dir"] is not None
|
||||||
|
assert spec["working_dir"].startswith("/deps/")
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_spec_verify_command_in_sync() -> None:
|
||||||
|
runner = CliRunner()
|
||||||
|
config_content = {
|
||||||
|
"python_version": "3.11",
|
||||||
|
"image_distro": "wolfi",
|
||||||
|
"graphs": {"agent": "agent.py:graph"},
|
||||||
|
"dependencies": ["."],
|
||||||
|
}
|
||||||
|
with temporary_config_folder(config_content) as temp_dir:
|
||||||
|
spec_path = temp_dir / "buildspec.json"
|
||||||
|
(temp_dir / "agent.py").touch()
|
||||||
|
export_result = runner.invoke(
|
||||||
|
cli,
|
||||||
|
[
|
||||||
|
"build-spec",
|
||||||
|
"export",
|
||||||
|
"--config",
|
||||||
|
str(temp_dir / "config.json"),
|
||||||
|
"--output",
|
||||||
|
str(spec_path),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert export_result.exit_code == 0, export_result.output
|
||||||
|
|
||||||
|
verify_result = runner.invoke(
|
||||||
|
cli,
|
||||||
|
[
|
||||||
|
"build-spec",
|
||||||
|
"verify",
|
||||||
|
str(spec_path),
|
||||||
|
"--config",
|
||||||
|
str(temp_dir / "config.json"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert verify_result.exit_code == 0, verify_result.output
|
||||||
|
assert "Build spec is in sync" in verify_result.output
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_spec_verify_command_out_of_sync() -> None:
|
||||||
|
runner = CliRunner()
|
||||||
|
config_content = {
|
||||||
|
"python_version": "3.11",
|
||||||
|
"image_distro": "wolfi",
|
||||||
|
"graphs": {"agent": "agent.py:graph"},
|
||||||
|
"dependencies": ["."],
|
||||||
|
}
|
||||||
|
with temporary_config_folder(config_content) as temp_dir:
|
||||||
|
spec_path = temp_dir / "buildspec.json"
|
||||||
|
(temp_dir / "agent.py").touch()
|
||||||
|
export_result = runner.invoke(
|
||||||
|
cli,
|
||||||
|
[
|
||||||
|
"build-spec",
|
||||||
|
"export",
|
||||||
|
"--config",
|
||||||
|
str(temp_dir / "config.json"),
|
||||||
|
"--output",
|
||||||
|
str(spec_path),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert export_result.exit_code == 0, export_result.output
|
||||||
|
|
||||||
|
with open(spec_path, encoding="utf-8") as f:
|
||||||
|
spec = json.load(f)
|
||||||
|
spec["runtime"] = "node"
|
||||||
|
with open(spec_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(spec, f, indent=2, sort_keys=True)
|
||||||
|
|
||||||
|
verify_result = runner.invoke(
|
||||||
|
cli,
|
||||||
|
[
|
||||||
|
"build-spec",
|
||||||
|
"verify",
|
||||||
|
str(spec_path),
|
||||||
|
"--config",
|
||||||
|
str(temp_dir / "config.json"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert verify_result.exit_code != 0
|
||||||
|
assert "Build spec is out of sync" in verify_result.output
|
||||||
|
|
||||||
|
|
||||||
def test_dockerfile_command_basic() -> None:
|
def test_dockerfile_command_basic() -> None:
|
||||||
"""Test the 'dockerfile' command with basic configuration."""
|
"""Test the 'dockerfile' command with basic configuration."""
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
|
|||||||
Reference in New Issue
Block a user