Skip to content

Commit

Permalink
Add windows CLI testing
Browse files Browse the repository at this point in the history
  • Loading branch information
hinthornw committed Nov 21, 2024
1 parent 5247952 commit 8dba54f
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 72 deletions.
62 changes: 60 additions & 2 deletions .github/workflows/_integration_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
python-version:
- "3.10"
- "3.11"
name: "CLI integration test"
name: "CLI integration test (Linux)"
defaults:
run:
working-directory: libs/cli
Expand Down Expand Up @@ -71,4 +71,62 @@ jobs:
working-directory: libs/cli/js-examples
run: |
langgraph build -t langgraph-test-e
windows-build:
runs-on: windows-latest
name: "CLI integration test (Windows)"
defaults:
run:
working-directory: libs/cli
shell: bash
steps:
- uses: actions/checkout@v4
- name: Get changed files
id: changed-files
uses: Ana06/[email protected]
with:
filter: "libs/cli/**"
- name: Set up Python 3.11 + Poetry ${{ env.POETRY_VERSION }}
if: steps.changed-files.outputs.all
uses: "./.github/actions/poetry_setup"
with:
python-version: "3.11"
poetry-version: ${{ env.POETRY_VERSION }}
cache-key: integration-test-cli-windows
- name: Setup env
if: steps.changed-files.outputs.all
working-directory: libs/cli/examples
run: cp .env.example .env
- name: Install cli globally
if: steps.changed-files.outputs.all
run: pip install -e .
- name: Build and test service A
if: steps.changed-files.outputs.all
working-directory: libs/cli/examples
run: |
langgraph build -t langgraph-test-a --base-image "langchain/langgraph-trial"
cp .env.example .envg
timeout 60 python ../../../.github/scripts/run_langgraph_cli_test.py -c langgraph.json -t langgraph-test-a
- name: Build and test service B
if: steps.changed-files.outputs.all
working-directory: libs/cli/examples/graphs
run: |
langgraph build -t langgraph-test-b --base-image "langchain/langgraph-trial"
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-b
- name: Build and test service C
if: steps.changed-files.outputs.all
working-directory: libs/cli/examples/graphs_reqs_a
run: |
langgraph build -t langgraph-test-c --base-image "langchain/langgraph-trial"
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-c
- name: Build and test service D
if: steps.changed-files.outputs.all
working-directory: libs/cli/examples/graphs_reqs_b
run: |
langgraph build -t langgraph-test-d --base-image "langchain/langgraph-trial"
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-d
- name: Build JS service
if: steps.changed-files.outputs.all
working-directory: libs/cli/js-examples
run: |
langgraph build -t langgraph-test-e
150 changes: 80 additions & 70 deletions libs/cli/langgraph_cli/config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import json
import os
import pathlib
import textwrap
from typing import NamedTuple, Optional, TypedDict, Union
Expand Down Expand Up @@ -192,66 +191,76 @@ def check_reserved(name: str, ref: str):
resolved = config_path.parent / local_dep

# validate local dependency
if not resolved.exists():
raise FileNotFoundError(f"Could not find local dependency: {resolved}")
elif not resolved.is_dir():
raise NotADirectoryError(
f"Local dependency must be a directory: {resolved}"
)
elif not resolved.is_relative_to(config_path.parent):
raise ValueError(
f"Local dependency '{resolved}' must be a subdirectory of '{config_path.parent}'"
)
try:
if not resolved.exists():
raise FileNotFoundError(f"Could not find local dependency: {resolved}")
elif not resolved.is_dir():
raise NotADirectoryError(
f"Local dependency must be a directory: {resolved}"
)
elif not resolved.is_relative_to(config_path.parent):
raise ValueError(
f"Local dependency '{resolved}' must be a subdirectory of '{config_path.parent}'"
)

# if it's installable, add it to local_pkgs
# otherwise, add it to faux_pkgs, and create a pyproject.toml
files = os.listdir(resolved)
if "pyproject.toml" in files:
real_pkgs[resolved] = local_dep
if local_dep == ".":
working_dir = f"/deps/{resolved.name}"
elif "setup.py" in files:
real_pkgs[resolved] = local_dep
if local_dep == ".":
working_dir = f"/deps/{resolved.name}"
else:
if any(file == "__init__.py" for file in files):
# flat layout
if "-" in resolved.name:
raise ValueError(
f"Package name '{resolved.name}' contains a hyphen. "
"Rename the directory to use it as flat-layout package."
)
check_reserved(resolved.name, local_dep)
container_path = f"/deps/__outer_{resolved.name}/{resolved.name}"
# if it's installable, add it to local_pkgs
# otherwise, add it to faux_pkgs, and create a pyproject.toml
try:
files = list(resolved.iterdir())
file_names = [f.name for f in files]
except (PermissionError, OSError) as e:
raise click.UsageError(
f"Cannot access directory {resolved}: {str(e)}"
) from None

if "pyproject.toml" in file_names:
real_pkgs[resolved] = local_dep
if local_dep == ".":
working_dir = f"/deps/{resolved.name}"
elif "setup.py" in file_names:
real_pkgs[resolved] = local_dep
if local_dep == ".":
working_dir = f"/deps/{resolved.name}"
else:
# src layout
container_path = f"/deps/__outer_{resolved.name}/src"
for file in files:
rfile = resolved / file
if (
rfile.is_dir()
and file != "__pycache__"
and not file.startswith(".")
):
try:
for subfile in os.listdir(rfile):
if subfile.endswith(".py"):
check_reserved(file, local_dep)
break
except PermissionError:
pass
faux_pkgs[resolved] = (local_dep, container_path)
if local_dep == ".":
working_dir = container_path
if "requirements.txt" in files:
rfile = resolved / "requirements.txt"
pip_reqs.append(
(
pathlib.PurePosixPath(rfile.relative_to(config_path.parent)),
f"{container_path}/requirements.txt",
if any(file == "__init__.py" for file in file_names):
# flat layout
if "-" in resolved.name:
raise ValueError(
f"Package name '{resolved.name}' contains a hyphen. "
"Rename the directory to use it as flat-layout package."
)
check_reserved(resolved.name, local_dep)
container_path = f"/deps/__outer_{resolved.name}/{resolved.name}"
else:
# src layout
container_path = f"/deps/__outer_{resolved.name}/src"
for file in files:
if (
file.is_dir()
and file.name != "__pycache__"
and not file.name.startswith(".")
):
try:
subfiles = list(file.iterdir())
if any(f.name.endswith(".py") for f in subfiles):
check_reserved(file.name, local_dep)
except (PermissionError, OSError):
continue
faux_pkgs[resolved] = (local_dep, container_path)
if local_dep == ".":
working_dir = container_path
if "requirements.txt" in file_names:
rfile = resolved / "requirements.txt"
pip_reqs.append(
(
pathlib.PurePosixPath(
rfile.relative_to(config_path.parent)
),
f"{container_path}/requirements.txt",
)
)
)
except (PermissionError, OSError) as e:
raise click.UsageError(f"Cannot access path {resolved}: {str(e)}") from e

return LocalDeps(pip_reqs, real_pkgs, faux_pkgs, working_dir)

Expand Down Expand Up @@ -313,17 +322,17 @@ def python_config_to_docker(config_path: pathlib.Path, config: Config, base_imag

pip_pkgs_str = f"RUN {pip_install} {' '.join(pypi_deps)}" if pypi_deps else ""
if local_deps.pip_reqs:
pip_reqs_str = os.linesep.join(
pip_reqs_str = "\n".join(
f"ADD {reqpath} {destpath}" for reqpath, destpath in local_deps.pip_reqs
)
pip_reqs_str += f'{os.linesep}RUN {pip_install} {" ".join("-r " + r for _,r in local_deps.pip_reqs)}'

pip_reqs_str += (
f'\nRUN {pip_install} {" ".join("-r " + r for _,r in local_deps.pip_reqs)}'
)
else:
pip_reqs_str = ""

# https://setuptools.pypa.io/en/latest/userguide/datafiles.html#package-data
# https://til.simonwillison.net/python/pyproject
faux_pkgs_str = f"{os.linesep}{os.linesep}".join(
faux_pkgs_str = "\n\n".join(
f"""ADD {relpath} {destpath}
RUN set -ex && \\
for line in '[project]' \\
Expand All @@ -335,12 +344,12 @@ def python_config_to_docker(config_path: pathlib.Path, config: Config, base_imag
done"""
for fullpath, (relpath, destpath) in local_deps.faux_pkgs.items()
)
local_pkgs_str = os.linesep.join(
local_pkgs_str = "\n".join(
f"ADD {relpath} /deps/{fullpath.name}"
for fullpath, relpath in local_deps.real_pkgs.items()
)

installs = f"{os.linesep}{os.linesep}".join(
installs = "\n\n".join(
filter(
None,
[
Expand All @@ -352,18 +361,19 @@ def python_config_to_docker(config_path: pathlib.Path, config: Config, base_imag
],
)
)

_workdir = f"WORKDIR {local_deps.working_dir}" if local_deps.working_dir else ""
dockerfile_lines = "\n".join(config["dockerfile_lines"])
return f"""FROM {base_image}:{config['python_version']}
{os.linesep.join(config["dockerfile_lines"])}
{dockerfile_lines}
{installs}
RUN {pip_install} -e /deps/*
ENV LANGSERVE_GRAPHS='{json.dumps(config["graphs"])}'
{f"WORKDIR {local_deps.working_dir}" if local_deps.working_dir else ""}"""
{_workdir}"""


def node_config_to_docker(config_path: pathlib.Path, config: Config, base_image: str):
Expand All @@ -390,10 +400,10 @@ def test_file(file_name):
install_cmd = "npm ci"
else:
install_cmd = "npm i"

dockerfile_lines = "\n".join(config["dockerfile_lines"])
return f"""FROM {base_image}:{config['node_version']}
{os.linesep.join(config["dockerfile_lines"])}
{dockerfile_lines}
ADD . {faux_path}
Expand Down

0 comments on commit 8dba54f

Please sign in to comment.