mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-01 05:08:12 +00:00
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
* 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
144 lines
3.8 KiB
Python
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
|