From 6e81e6420c27b2663ad8910e60693236b203383d Mon Sep 17 00:00:00 2001 From: lorenzejay Date: Wed, 5 Aug 2026 14:21:17 -0700 Subject: [PATCH] fix(devtools): validate canary CrewAI pins --- lib/devtools/src/crewai_devtools/cli.py | 32 +++++++++++++++++++++++++ lib/devtools/tests/test_toml_updates.py | 31 ++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/lib/devtools/src/crewai_devtools/cli.py b/lib/devtools/src/crewai_devtools/cli.py index 418f59a3b..f3e8a3cd1 100644 --- a/lib/devtools/src/crewai_devtools/cli.py +++ b/lib/devtools/src/crewai_devtools/cli.py @@ -1438,6 +1438,36 @@ _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 + + +def _validate_deployment_repo_crewai_pin( + repo_dir: Path, + 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 + + 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 + + raise RuntimeError( + f"No exact CrewAI {version} dependency pin found in {repo_dir.name}" + ) + + def _update_deployment_test_repo(repo: str, version: str, is_prerelease: bool) -> None: """Update a deployment test repo to pin the new crewai version. @@ -1474,6 +1504,8 @@ def _update_deployment_test_repo(repo: str, version: str, is_prerelease: bool) - f"[green]✓[/green] Updated crewai pin in {wf.relative_to(repo_dir)}" ) + _validate_deployment_repo_crewai_pin(repo_dir, new_content, version) + if not pyproject_changed and not updated_workflows: console.print("[yellow]Nothing to update; skipping commit and PR.[/yellow]") return diff --git a/lib/devtools/tests/test_toml_updates.py b/lib/devtools/tests/test_toml_updates.py index aec1be12a..14853c8ae 100644 --- a/lib/devtools/tests/test_toml_updates.py +++ b/lib/devtools/tests/test_toml_updates.py @@ -6,12 +6,15 @@ 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, update_pyproject_dependencies, update_pyproject_version, update_template_dependencies, ) +import pytest def test_release_updates_crew_and_flow_canary_repositories(monkeypatch) -> None: @@ -32,6 +35,34 @@ 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"): + _validate_deployment_repo_crewai_pin( + tmp_path, + '[project]\ndependencies = ["requests>=2"]\n', + "2.0.0", + ) + + +def test_deployment_repo_validation_accepts_workflow_pin(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') + + _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"