mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-09-21 18:36:47 +00:00
fix(cli): treat staging and temp-file failures as archive errors
`create_project_zip` only wrapped the ZIP write, so an `OSError` while staging files or creating the temporary archive escaped as a bare `OSError` and the deploy command recorded it as `unexpected` instead of `zip_error`. The archive boundary now covers staging, temp-file creation and the write; the staging directory is removed on every path, and no partial archive is left behind. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -51,33 +51,25 @@ def create_project_zip(
|
||||
project_dir: Path | None = None,
|
||||
repository: git.Repository | None = None,
|
||||
) -> Path:
|
||||
"""Create a deployable ZIP archive for a CrewAI project."""
|
||||
"""Create a deployable ZIP archive for a CrewAI project.
|
||||
|
||||
Raises:
|
||||
ArchiveError: Nothing deployable was found, or the project could not be
|
||||
staged or written to the archive. No partial archive is left behind.
|
||||
"""
|
||||
root = (project_dir or Path.cwd()).resolve()
|
||||
files = _project_files(root, repository)
|
||||
if not files:
|
||||
raise ArchiveError("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())
|
||||
staged_root = _stage_project(root, files)
|
||||
try:
|
||||
return _write_archive(project_name, staged_root)
|
||||
finally:
|
||||
shutil.rmtree(staged_root, ignore_errors=True)
|
||||
except (OSError, zipfile.BadZipFile) as exc:
|
||||
archive_path.unlink(missing_ok=True)
|
||||
raise ArchiveError(f"Could not build the project ZIP: {exc}") from exc
|
||||
finally:
|
||||
shutil.rmtree(staged_root, ignore_errors=True)
|
||||
|
||||
return archive_path
|
||||
|
||||
|
||||
def _project_files(root: Path, repository: git.Repository | None = None) -> list[Path]:
|
||||
@@ -153,3 +145,24 @@ def _stage_project(root: Path, files: list[Path]) -> Path:
|
||||
shutil.rmtree(staging_root, ignore_errors=True)
|
||||
raise
|
||||
return staging_root
|
||||
|
||||
|
||||
def _write_archive(project_name: str, staged_root: Path) -> Path:
|
||||
"""Zip the staged tree into a new temporary file, removing it if the write fails."""
|
||||
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())
|
||||
except (OSError, zipfile.BadZipFile):
|
||||
archive_path.unlink(missing_ok=True)
|
||||
raise
|
||||
return archive_path
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import zipfile
|
||||
|
||||
import pytest
|
||||
@@ -333,3 +334,32 @@ def test_create_project_zip_wraps_a_write_failure_and_removes_the_partial_file(
|
||||
create_project_zip("demo", project_dir=tmp_path)
|
||||
|
||||
assert created and not created[0].exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"failing_step",
|
||||
[
|
||||
"crewai_cli.deploy.archive.shutil.copy2",
|
||||
"crewai_cli.deploy.archive.tempfile.NamedTemporaryFile",
|
||||
],
|
||||
)
|
||||
def test_create_project_zip_wraps_staging_and_temp_file_failures_and_cleans_up(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, failing_step: str
|
||||
):
|
||||
"""A full disk while staging or creating the archive is an archive failure too."""
|
||||
project = tmp_path / "project"
|
||||
project.mkdir()
|
||||
(project / "pyproject.toml").write_text("[project]\nname = 'demo'\n")
|
||||
scratch = tmp_path / "scratch"
|
||||
scratch.mkdir()
|
||||
monkeypatch.setattr(tempfile, "tempdir", str(scratch))
|
||||
|
||||
def fail(*args, **kwargs):
|
||||
raise OSError("disk full")
|
||||
|
||||
monkeypatch.setattr(failing_step, fail)
|
||||
|
||||
with pytest.raises(ArchiveError, match="Could not build the project ZIP: disk full"):
|
||||
create_project_zip("demo", project_dir=project)
|
||||
|
||||
assert list(scratch.iterdir()) == []
|
||||
|
||||
Reference in New Issue
Block a user