Files
crewAI/lib/cli/src/crewai_cli/deploy/archive.py
João Moura 4cbfbdb232
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Vulnerability Scan / pip-audit (push) Has been cancelled
Build uv cache / build-cache (3.10) (push) Has been cancelled
Build uv cache / build-cache (3.11) (push) Has been cancelled
Build uv cache / build-cache (3.12) (push) Has been cancelled
Build uv cache / build-cache (3.13) (push) Has been cancelled
Keep JSON crew projects and deploy archives Python-free (#6228)
* fix: scaffold deployable json crews

* fix: keep json crew scaffolds python-free

* fix: keep json deploy archives python-free

* fix: tighten json crew deploy validation

* fix: address json crew pr checks

* fix: clear langsmith audit advisory
2026-06-22 13:22:46 -03:00

144 lines
3.8 KiB
Python

from __future__ import annotations
from pathlib import Path
import shutil
import tempfile
import zipfile
from crewai_cli import git
_EXCLUDED_DIRS = {
".crewai",
".git",
".mypy_cache",
".pytest_cache",
".ruff_cache",
".tox",
".venv",
"__pycache__",
"build",
"dist",
"env",
"venv",
}
_EXCLUDED_FILES = {
".DS_Store",
".env",
}
_ALLOWED_ENV_EXAMPLES = {
".env.example",
".env.sample",
}
_EXCLUDED_SUFFIXES = {
".pyc",
".pyo",
}
def create_project_zip(
project_name: str,
*,
project_dir: Path | None = None,
repository: git.Repository | None = None,
) -> Path:
"""Create a deployable ZIP archive for a CrewAI project."""
root = (project_dir or Path.cwd()).resolve()
files = _project_files(root, repository)
if not files:
raise ValueError("No deployable project files were found.")
staged_root = _stage_project(root, files)
archive_handle = tempfile.NamedTemporaryFile(
prefix=f"{project_name}-",
suffix=".zip",
delete=False,
)
archive_path = Path(archive_handle.name)
archive_handle.close()
try:
with zipfile.ZipFile(archive_path, "w", zipfile.ZIP_DEFLATED) as zip_file:
for relative_path in _walk_files(staged_root):
absolute_path = staged_root / relative_path
zip_file.write(absolute_path, relative_path.as_posix())
finally:
shutil.rmtree(staged_root, ignore_errors=True)
return archive_path
def _project_files(root: Path, repository: git.Repository | None = None) -> list[Path]:
"""Return project-relative files to include in the archive."""
if repository is not None:
return _repository_project_files(root, repository)
try:
repository = git.Repository(path=str(root), fetch=False)
except ValueError:
repository = None
if repository is not None:
return _repository_project_files(root, repository)
return [
path
for path in _walk_files(root)
if not _is_excluded(path) and _is_regular_file(root / path)
]
def _repository_project_files(root: Path, repository: git.Repository) -> list[Path]:
"""Return deployable files from Git while applying local safety excludes."""
files = [Path(path) for path in repository.deployable_files()]
return [
path
for path in files
if not _is_excluded(path) and _is_regular_file(root / path)
]
def _walk_files(root: Path) -> list[Path]:
"""List regular files below root as project-relative paths."""
return [
path.relative_to(root) for path in root.rglob("*") if _is_regular_file(path)
]
def _is_regular_file(path: Path) -> bool:
"""Return True for regular files, excluding symlinks to files."""
return path.is_file() and not path.is_symlink()
def _is_excluded(path: Path) -> bool:
"""Return True when a file should be omitted from deployment ZIPs."""
parts = set(path.parts)
if parts.intersection(_EXCLUDED_DIRS):
return True
name = path.name
if name in _EXCLUDED_FILES:
return True
if name.startswith(".env.") and name not in _ALLOWED_ENV_EXAMPLES:
return True
return path.suffix in _EXCLUDED_SUFFIXES
def _stage_project(root: Path, files: list[Path]) -> Path:
"""Copy archive files into a temporary staging directory."""
staging_root = Path(tempfile.mkdtemp(prefix="crewai-deploy-"))
try:
for relative_path in files:
source = root / relative_path
if not _is_regular_file(source):
continue
destination = staging_root / relative_path
destination.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(source, destination)
except Exception:
shutil.rmtree(staging_root, ignore_errors=True)
raise
return staging_root