mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-08-10 08:21:54 +00:00
fix(devtools): validate effective canary pins
This commit is contained in:
@@ -4,6 +4,7 @@ from collections.abc import Mapping
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
@@ -1438,12 +1439,126 @@ _PYPI_POLL_INTERVAL: Final[int] = 15
|
||||
_PYPI_POLL_TIMEOUT: Final[int] = 600
|
||||
|
||||
|
||||
def _has_exact_crewai_pin(content: str, version: str) -> bool:
|
||||
"""Return whether text contains an exact CrewAI dependency pin."""
|
||||
pattern = re.compile(
|
||||
rf"\bcrewai(?:\[[^\]\s\"']+\])?=={re.escape(version)}(?=$|[\s\"'])"
|
||||
)
|
||||
return pattern.search(content) is not None
|
||||
_CREWAI_REQUIREMENT_PATTERN: Final[re.Pattern[str]] = re.compile(
|
||||
r"^crewai(?:\s*\[[^\]]+\])?(?![\w-])"
|
||||
r"\s*(?:(?P<operator>===|==|~=|!=|>=|<=|>|<)\s*"
|
||||
r"(?P<version>[^\s;]+))?",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def _crewai_requirement_pin(requirement: str) -> str | None:
|
||||
"""Return an exact CrewAI pin, or an empty string for a non-exact pin."""
|
||||
match = _CREWAI_REQUIREMENT_PATTERN.match(requirement.strip())
|
||||
if not match:
|
||||
return None
|
||||
if match.group("operator") != "==":
|
||||
return ""
|
||||
return match.group("version") or ""
|
||||
|
||||
|
||||
def _pyproject_crewai_requirements(content: str) -> list[tuple[str, str]]:
|
||||
"""Collect active CrewAI dependency requirements from pyproject content."""
|
||||
requirements: list[tuple[str, str]] = []
|
||||
doc = tomlkit.parse(content)
|
||||
for key in ("dependencies", "optional-dependencies"):
|
||||
deps = doc.get("project", {}).get(key)
|
||||
if deps is None:
|
||||
continue
|
||||
dep_lists = deps.values() if isinstance(deps, Mapping) else [deps]
|
||||
for dep_list in dep_lists:
|
||||
for dep in dep_list:
|
||||
spec = str(dep)
|
||||
pin = _crewai_requirement_pin(spec)
|
||||
if pin is not None:
|
||||
requirements.append((spec, pin))
|
||||
return requirements
|
||||
|
||||
|
||||
def _workflow_run_commands(content: str) -> list[str]:
|
||||
"""Extract scalar and block ``run`` command values from workflow YAML text."""
|
||||
lines = content.splitlines()
|
||||
commands: list[str] = []
|
||||
index = 0
|
||||
while index < len(lines):
|
||||
line = lines[index]
|
||||
stripped = line.lstrip()
|
||||
if stripped.startswith("- run:"):
|
||||
stripped = stripped[2:].lstrip()
|
||||
if not stripped.startswith("run:"):
|
||||
index += 1
|
||||
continue
|
||||
|
||||
indent = len(line) - len(line.lstrip())
|
||||
value = stripped.removeprefix("run:").strip()
|
||||
if value not in {"|", "|-", "|+", ">", ">-", ">+"}:
|
||||
commands.append(value)
|
||||
index += 1
|
||||
continue
|
||||
|
||||
block: list[str] = []
|
||||
index += 1
|
||||
while index < len(lines):
|
||||
block_line = lines[index]
|
||||
if (
|
||||
block_line.strip()
|
||||
and len(block_line) - len(block_line.lstrip()) <= indent
|
||||
):
|
||||
break
|
||||
block.append(block_line.strip())
|
||||
index += 1
|
||||
commands.append("\n".join(block))
|
||||
return commands
|
||||
|
||||
|
||||
def _workflow_crewai_requirements(content: str) -> list[tuple[str, str]]:
|
||||
"""Collect CrewAI requirements from executable workflow install commands."""
|
||||
requirements: list[tuple[str, str]] = []
|
||||
for command in _workflow_run_commands(content):
|
||||
normalized = command.replace("\\\n", " ").replace("\n", " ; ")
|
||||
lexer = shlex.shlex(normalized, posix=True, punctuation_chars=";&|")
|
||||
lexer.whitespace_split = True
|
||||
lexer.commenters = "#"
|
||||
try:
|
||||
tokens = list(lexer)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
index = 0
|
||||
while index < len(tokens):
|
||||
command_lengths = (
|
||||
(tokens[index : index + 3] == ["uv", "pip", "install"], 3),
|
||||
(tokens[index : index + 2] == ["uv", "add"], 2),
|
||||
(
|
||||
tokens[index : index + 2]
|
||||
in (["pip", "install"], ["pip3", "install"]),
|
||||
2,
|
||||
),
|
||||
(
|
||||
tokens[index : index + 4]
|
||||
in (
|
||||
["python", "-m", "pip", "install"],
|
||||
["python3", "-m", "pip", "install"],
|
||||
),
|
||||
4,
|
||||
),
|
||||
)
|
||||
install_length = next(
|
||||
(length for matched, length in command_lengths if matched),
|
||||
0,
|
||||
)
|
||||
if not install_length:
|
||||
index += 1
|
||||
continue
|
||||
|
||||
index += install_length
|
||||
while index < len(tokens) and tokens[index] not in {";", "&&", "||", "|"}:
|
||||
argument = tokens[index]
|
||||
pin = _crewai_requirement_pin(argument)
|
||||
if pin is not None:
|
||||
requirements.append((argument, pin))
|
||||
index += 1
|
||||
return requirements
|
||||
|
||||
|
||||
def _validate_deployment_repo_crewai_pin(
|
||||
@@ -1451,21 +1566,25 @@ def _validate_deployment_repo_crewai_pin(
|
||||
pyproject_content: str,
|
||||
version: str,
|
||||
) -> None:
|
||||
"""Fail unless a deployment canary contains the requested CrewAI pin."""
|
||||
if _has_exact_crewai_pin(pyproject_content, version):
|
||||
return
|
||||
"""Fail unless every effective canary CrewAI requirement has the exact pin."""
|
||||
requirements = _pyproject_crewai_requirements(pyproject_content)
|
||||
|
||||
workflows_dir = repo_dir / ".github" / "workflows"
|
||||
if workflows_dir.exists():
|
||||
for workflow in workflows_dir.iterdir():
|
||||
if workflow.suffix in (".yml", ".yaml") and _has_exact_crewai_pin(
|
||||
workflow.read_text(), version
|
||||
):
|
||||
return
|
||||
if workflow.suffix in (".yml", ".yaml"):
|
||||
requirements.extend(_workflow_crewai_requirements(workflow.read_text()))
|
||||
|
||||
raise RuntimeError(
|
||||
f"No exact CrewAI {version} dependency pin found in {repo_dir.name}"
|
||||
)
|
||||
if not requirements:
|
||||
raise RuntimeError(f"No effective CrewAI dependency found in {repo_dir.name}")
|
||||
|
||||
mismatches = [spec for spec, pin in requirements if pin != version]
|
||||
if mismatches:
|
||||
found = ", ".join(repr(spec) for spec in mismatches)
|
||||
raise RuntimeError(
|
||||
f"CrewAI dependencies in {repo_dir.name} must all pin {version}; "
|
||||
f"found {found}"
|
||||
)
|
||||
|
||||
|
||||
def _update_deployment_test_repo(repo: str, version: str, is_prerelease: bool) -> None:
|
||||
|
||||
@@ -6,7 +6,6 @@ from textwrap import dedent
|
||||
from crewai_devtools import cli as devtools_cli
|
||||
from crewai_devtools.cli import (
|
||||
_DEFAULT_WORKSPACE_PACKAGES,
|
||||
_has_exact_crewai_pin,
|
||||
_pin_crewai_deps,
|
||||
_repin_crewai_install,
|
||||
_validate_deployment_repo_crewai_pin,
|
||||
@@ -35,15 +34,8 @@ def test_release_updates_crew_and_flow_canary_repositories(monkeypatch) -> None:
|
||||
]
|
||||
|
||||
|
||||
def test_exact_crewai_pin_accepts_plain_and_extra_dependencies() -> None:
|
||||
assert _has_exact_crewai_pin('"crewai==2.0.0"', "2.0.0")
|
||||
assert _has_exact_crewai_pin('"crewai[tools]==2.0.0"', "2.0.0")
|
||||
assert not _has_exact_crewai_pin('"crewai>=2.0.0"', "2.0.0")
|
||||
assert not _has_exact_crewai_pin('"crewai==2.0.0a1"', "2.0.0")
|
||||
|
||||
|
||||
def test_deployment_repo_validation_rejects_missing_crewai_pin(tmp_path: Path) -> None:
|
||||
with pytest.raises(RuntimeError, match=r"No exact CrewAI 2\.0\.0 dependency pin"):
|
||||
with pytest.raises(RuntimeError, match="No effective CrewAI dependency"):
|
||||
_validate_deployment_repo_crewai_pin(
|
||||
tmp_path,
|
||||
'[project]\ndependencies = ["requests>=2"]\n',
|
||||
@@ -63,6 +55,71 @@ def test_deployment_repo_validation_accepts_workflow_pin(tmp_path: Path) -> None
|
||||
)
|
||||
|
||||
|
||||
def test_deployment_repo_validation_rejects_mixed_versions(tmp_path: Path) -> None:
|
||||
workflows = tmp_path / ".github" / "workflows"
|
||||
workflows.mkdir(parents=True)
|
||||
(workflows / "test.yml").write_text('run: uv pip install "crewai[a2a]==2.0.0"\n')
|
||||
|
||||
with pytest.raises(RuntimeError, match=r"must all pin 2\.0\.0"):
|
||||
_validate_deployment_repo_crewai_pin(
|
||||
tmp_path,
|
||||
'[project]\ndependencies = ["crewai==1.0.0"]\n',
|
||||
"2.0.0",
|
||||
)
|
||||
|
||||
|
||||
def test_deployment_repo_validation_ignores_comments_and_echo(tmp_path: Path) -> None:
|
||||
workflows = tmp_path / ".github" / "workflows"
|
||||
workflows.mkdir(parents=True)
|
||||
(workflows / "test.yml").write_text(
|
||||
'run: echo "crewai==2.0.0"\n# run: pip install crewai==2.0.0\n'
|
||||
)
|
||||
|
||||
with pytest.raises(RuntimeError, match=r"must all pin 2\.0\.0"):
|
||||
_validate_deployment_repo_crewai_pin(
|
||||
tmp_path,
|
||||
(
|
||||
"# documented pin: crewai==2.0.0\n"
|
||||
'[project]\ndependencies = ["crewai>=1.0"]\n'
|
||||
),
|
||||
"2.0.0",
|
||||
)
|
||||
|
||||
|
||||
def test_deployment_repo_validation_accepts_spaced_extras_and_marker(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
_validate_deployment_repo_crewai_pin(
|
||||
tmp_path,
|
||||
(
|
||||
"[project]\ndependencies = [\n"
|
||||
" \"crewai[tools, embeddings]==2.0.0; python_version >= '3.10'\",\n"
|
||||
"]\n"
|
||||
),
|
||||
"2.0.0",
|
||||
)
|
||||
|
||||
|
||||
def test_deployment_repo_validation_reads_multiline_workflow_install(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
workflows = tmp_path / ".github" / "workflows"
|
||||
workflows.mkdir(parents=True)
|
||||
(workflows / "test.yml").write_text(
|
||||
"steps:\n"
|
||||
" - name: Install\n"
|
||||
" run: |\n"
|
||||
" uv pip install \\\n"
|
||||
" \"crewai[tools, embeddings]==2.0.0; python_version >= '3.10'\"\n"
|
||||
)
|
||||
|
||||
_validate_deployment_repo_crewai_pin(
|
||||
tmp_path,
|
||||
'[project]\ndependencies = ["requests>=2"]\n',
|
||||
"2.0.0",
|
||||
)
|
||||
|
||||
|
||||
class TestUpdatePyprojectVersion:
|
||||
def test_updates_version(self, tmp_path: Path) -> None:
|
||||
pyproject = tmp_path / "pyproject.toml"
|
||||
|
||||
Reference in New Issue
Block a user