diff --git a/lib/cli/pyproject.toml b/lib/cli/pyproject.toml index 63f28b4ee..42418d4d9 100644 --- a/lib/cli/pyproject.toml +++ b/lib/cli/pyproject.toml @@ -9,7 +9,7 @@ authors = [ requires-python = ">=3.10, <3.14" dependencies = [ "crewai-core==1.14.6", - "click~=8.1.7", + "click>=8.1.7,<9", "pydantic>=2.11.9,<2.13", "pydantic-settings~=2.10.1", "appdirs~=1.4.4", diff --git a/lib/cli/tests/test_click_compatibility.py b/lib/cli/tests/test_click_compatibility.py new file mode 100644 index 000000000..3dbd8bda3 --- /dev/null +++ b/lib/cli/tests/test_click_compatibility.py @@ -0,0 +1,131 @@ +"""Tests for click dependency compatibility. + +Regression tests for https://github.com/crewAIInc/crewAI/issues/6002 +The click dependency was previously pinned to ~=8.1.7 (i.e. >=8.1.7,<8.2.0) +which prevented users from upgrading to click 8.2+ as required by their +security policies. The constraint has been widened to >=8.1.7,<9 to allow +newer click 8.x releases while still guarding against a future major version +break. +""" + +from importlib.metadata import requires +from pathlib import Path + +import click +import pytest +from click.testing import CliRunner +from packaging.requirements import Requirement + + +# --------------------------------------------------------------------------- +# Verify the runtime click version satisfies the declared constraint +# --------------------------------------------------------------------------- + +def _get_click_requirement_from_pyproject(package_dir: str) -> Requirement: + """Parse the click requirement directly from a pyproject.toml file.""" + import tomli + + pyproject_path = Path(__file__).resolve().parents[3] / package_dir / "pyproject.toml" + with open(pyproject_path, "rb") as f: + data = tomli.load(f) + deps = data["project"]["dependencies"] + for dep in deps: + req = Requirement(dep) + if req.name == "click": + return req + raise ValueError(f"click not found in {pyproject_path}") + + +@pytest.mark.parametrize( + "package_dir", + [ + "lib/crewai", + "lib/cli", + "lib/devtools", + ], +) +def test_click_constraint_allows_8_3_3(package_dir: str): + """The declared click constraint must accept click 8.3.3 (issue #6002).""" + req = _get_click_requirement_from_pyproject(package_dir) + # packaging's Requirement.specifier supports `__contains__` for version checks + assert "8.3.3" in req.specifier, ( + f"{package_dir}: click constraint {req.specifier} does not allow 8.3.3" + ) + + +@pytest.mark.parametrize( + "package_dir", + [ + "lib/crewai", + "lib/cli", + "lib/devtools", + ], +) +def test_click_constraint_allows_8_1_7(package_dir: str): + """The declared click constraint must still accept the original minimum (8.1.7).""" + req = _get_click_requirement_from_pyproject(package_dir) + assert "8.1.7" in req.specifier, ( + f"{package_dir}: click constraint {req.specifier} does not allow 8.1.7" + ) + + +@pytest.mark.parametrize( + "package_dir", + [ + "lib/crewai", + "lib/cli", + "lib/devtools", + ], +) +def test_click_constraint_rejects_next_major(package_dir: str): + """The declared click constraint must reject click 9.0.0.""" + req = _get_click_requirement_from_pyproject(package_dir) + assert "9.0.0" not in req.specifier, ( + f"{package_dir}: click constraint {req.specifier} should not allow 9.0.0" + ) + + +# --------------------------------------------------------------------------- +# Verify the installed click version works with the CLI +# --------------------------------------------------------------------------- + +def test_click_version_is_compatible(): + """The installed click version must be within the 8.x range.""" + major = int(click.__version__.split(".")[0]) + assert major == 8, f"Expected click 8.x, got {click.__version__}" + + +def test_cli_runner_works_with_installed_click(): + """Smoke-test: CliRunner from the installed click can invoke a trivial command.""" + + @click.command() + @click.option("--name", default="world") + def hello(name: str) -> None: + click.echo(f"Hello {name}!") + + runner = CliRunner() + result = runner.invoke(hello, ["--name", "crewai"]) + assert result.exit_code == 0 + assert "Hello crewai!" in result.output + + +def test_cli_group_works_with_installed_click(): + """Smoke-test: click.group, click.option, click.argument all work.""" + + @click.group() + def grp() -> None: + pass + + @grp.command() + @click.argument("task") + @click.option("--verbose", is_flag=True) + def run(task: str, verbose: bool) -> None: + if verbose: + click.echo(f"Running {task} (verbose)") + else: + click.echo(f"Running {task}") + + runner = CliRunner() + result = runner.invoke(grp, ["run", "test-task", "--verbose"]) + assert result.exit_code == 0 + assert "Running test-task (verbose)" in result.output diff --git a/lib/crewai/pyproject.toml b/lib/crewai/pyproject.toml index fac0b19e1..ff1d61b7f 100644 --- a/lib/crewai/pyproject.toml +++ b/lib/crewai/pyproject.toml @@ -29,7 +29,7 @@ dependencies = [ "python-dotenv>=1.2.2,<2", "pyjwt>=2.9.0,<3", # Configuration and Utils - "click~=8.1.7", + "click>=8.1.7,<9", "appdirs~=1.4.4", "jsonref~=1.1.0", "json-repair~=0.25.2", diff --git a/lib/devtools/pyproject.toml b/lib/devtools/pyproject.toml index 88938fa5c..98ba51f59 100644 --- a/lib/devtools/pyproject.toml +++ b/lib/devtools/pyproject.toml @@ -10,7 +10,7 @@ requires-python = ">=3.10, <3.14" classifiers = ["Private :: Do Not Upload"] private = true dependencies = [ - "click~=8.1.7", + "click>=8.1.7,<9", "tomlkit~=0.13.2", "openai>=1.83.0,<3", "python-dotenv>=1.2.2,<2", diff --git a/uv.lock b/uv.lock index 7dae3c0ed..b124afcab 100644 --- a/uv.lock +++ b/uv.lock @@ -13,7 +13,7 @@ resolution-markers = [ ] [options] -exclude-newer = "2026-05-29T06:38:49.259217Z" +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. exclude-newer-span = "P3D" [manifest] @@ -1389,7 +1389,7 @@ requires-dist = [ { name = "boto3", marker = "extra == 'aws'", specifier = "~=1.42.79" }, { name = "boto3", marker = "extra == 'bedrock'", specifier = "~=1.42.79" }, { name = "chromadb", specifier = "~=1.1.0" }, - { name = "click", specifier = "~=8.1.7" }, + { name = "click", specifier = ">=8.1.7,<9" }, { name = "crewai-cli", editable = "lib/cli" }, { name = "crewai-core", editable = "lib/crewai-core" }, { name = "crewai-files", marker = "extra == 'file-processing'", editable = "lib/crewai-files" }, @@ -1459,7 +1459,7 @@ dependencies = [ requires-dist = [ { name = "appdirs", specifier = "~=1.4.4" }, { name = "certifi" }, - { name = "click", specifier = "~=8.1.7" }, + { name = "click", specifier = ">=8.1.7,<9" }, { name = "crewai-core", editable = "lib/crewai-core" }, { name = "cryptography", specifier = ">=42.0" }, { name = "httpx", specifier = "~=0.28.1" }, @@ -1523,7 +1523,7 @@ dependencies = [ [package.metadata] requires-dist = [ - { name = "click", specifier = "~=8.1.7" }, + { name = "click", specifier = ">=8.1.7,<9" }, { name = "openai", specifier = ">=1.83.0,<3" }, { name = "pygithub", specifier = "~=1.59.1" }, { name = "python-dotenv", specifier = ">=1.2.2,<2" },