mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-23 16:12:25 +02:00
- Added a new `create_archive` function to generate a tarball of the project source, including handling of .dockerignore patterns. - Enhanced the `deploy` function to support remote builds, allowing deployment without local Docker availability. - Introduced a `--remote` option in the CLI to control remote build behavior. - Updated the HostBackendClient with methods for requesting upload URLs and updating deployments with source tarballs.
134 lines
4.1 KiB
Python
134 lines
4.1 KiB
Python
"""Create a tarball of project source for remote builds."""
|
|
|
|
import os
|
|
import pathlib
|
|
import tarfile
|
|
import tempfile
|
|
|
|
import click
|
|
|
|
|
|
_WARN_SIZE = 50 * 1024 * 1024 # 50 MB
|
|
_MAX_SIZE = 200 * 1024 * 1024 # 200 MB
|
|
|
|
|
|
def _tar_filter(tarinfo: tarfile.TarInfo) -> tarfile.TarInfo | None:
|
|
"""Strip symlinks, hardlinks, and traversal paths from archive."""
|
|
if tarinfo.issym() or tarinfo.islnk():
|
|
return None
|
|
if ".." in tarinfo.name.split("/"):
|
|
return None
|
|
return tarinfo
|
|
|
|
|
|
def _read_ignore_patterns(context_dir: pathlib.Path) -> list[str]:
|
|
"""Read .dockerignore patterns if present."""
|
|
dockerignore = context_dir / ".dockerignore"
|
|
if dockerignore.is_file():
|
|
patterns = []
|
|
for line in dockerignore.read_text().splitlines():
|
|
line = line.strip()
|
|
if line and not line.startswith("#"):
|
|
patterns.append(line)
|
|
return patterns
|
|
return []
|
|
|
|
|
|
def _should_ignore(rel_path: str, patterns: list[str]) -> bool:
|
|
"""Check if a relative path matches any dockerignore pattern."""
|
|
import fnmatch
|
|
|
|
rel_path = rel_path.replace(os.sep, "/")
|
|
|
|
for pattern in patterns:
|
|
negate = pattern.startswith("!")
|
|
if negate:
|
|
pattern = pattern[1:]
|
|
|
|
if fnmatch.fnmatch(rel_path, pattern) or fnmatch.fnmatch(
|
|
rel_path, f"**/{pattern}"
|
|
):
|
|
if negate:
|
|
return False
|
|
return True
|
|
|
|
parts = rel_path.split("/")
|
|
for i in range(len(parts)):
|
|
partial = "/".join(parts[: i + 1])
|
|
if fnmatch.fnmatch(partial, pattern):
|
|
if negate:
|
|
return False
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def create_archive(
|
|
config_path: pathlib.Path,
|
|
) -> tuple[str, int]:
|
|
"""Create a .tar.gz archive of the project source.
|
|
|
|
Returns (archive_path, file_size).
|
|
The archive root is config.parent (the directory containing langgraph.json).
|
|
"""
|
|
context_dir = config_path.parent.resolve()
|
|
config_filename = config_path.name
|
|
ignore_patterns = _read_ignore_patterns(context_dir)
|
|
|
|
tmp_dir = tempfile.mkdtemp(prefix="langgraph-deploy-")
|
|
archive_path = os.path.join(tmp_dir, "source.tar.gz")
|
|
|
|
with tarfile.open(archive_path, "w:gz") as tar:
|
|
for root, dirs, files in os.walk(context_dir):
|
|
rel_root = os.path.relpath(root, context_dir)
|
|
if rel_root == ".":
|
|
rel_root = ""
|
|
|
|
dirs[:] = [
|
|
d
|
|
for d in dirs
|
|
if not _should_ignore(
|
|
os.path.join(rel_root, d) if rel_root else d, ignore_patterns
|
|
)
|
|
]
|
|
|
|
for f in files:
|
|
rel_path = os.path.join(rel_root, f) if rel_root else f
|
|
if _should_ignore(rel_path, ignore_patterns):
|
|
continue
|
|
full_path = os.path.join(root, f)
|
|
arcname = rel_path.replace(os.sep, "/")
|
|
info = tar.gettarinfo(full_path, arcname=arcname)
|
|
filtered = _tar_filter(info)
|
|
if filtered is None:
|
|
continue
|
|
with open(full_path, "rb") as fobj:
|
|
tar.addfile(filtered, fobj)
|
|
|
|
file_size = os.path.getsize(archive_path)
|
|
|
|
# Validate config file is at archive root
|
|
with tarfile.open(archive_path, "r:gz") as tar:
|
|
names = tar.getnames()
|
|
if config_filename not in names:
|
|
os.unlink(archive_path)
|
|
raise click.ClickException(
|
|
f"Archive validation failed: {config_filename} not found at archive root"
|
|
)
|
|
|
|
if file_size > _MAX_SIZE:
|
|
os.unlink(archive_path)
|
|
raise click.ClickException(
|
|
f"Source archive is {file_size / 1_048_576:.1f} MB, which exceeds the 200 MB limit. "
|
|
"Check your .dockerignore for large files (model weights, data, node_modules, .venv)."
|
|
)
|
|
|
|
if file_size > _WARN_SIZE:
|
|
click.secho(
|
|
f" Warning: source archive is {file_size / 1_048_576:.1f} MB. "
|
|
"Consider adding large files to .dockerignore.",
|
|
fg="yellow",
|
|
)
|
|
|
|
return archive_path, file_size
|