fix(devtools): validate effective canary pins

This commit is contained in:
lorenzejay
2026-08-05 14:41:52 -07:00
parent 27337e4a0a
commit 5e7a18e02a
2 changed files with 201 additions and 25 deletions

View File

@@ -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:

View File

@@ -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"