refactor: replace regex with tomlkit in devtools CLI
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Vulnerability Scan / pip-audit (push) Has been cancelled
Build uv cache / build-cache (3.10) (push) Has been cancelled
Build uv cache / build-cache (3.11) (push) Has been cancelled
Build uv cache / build-cache (3.12) (push) Has been cancelled
Build uv cache / build-cache (3.13) (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled

This commit is contained in:
Greyson LaLonde
2026-04-08 19:52:51 +08:00
committed by GitHub
parent f4c0667d34
commit fc9280ccf6
6 changed files with 365 additions and 39 deletions

View File

@@ -11,7 +11,7 @@ classifiers = ["Private :: Do Not Upload"]
private = true
dependencies = [
"click~=8.1.7",
"toml~=0.10.2",
"tomlkit~=0.13.2",
"openai>=1.83.0,<3",
"python-dotenv~=1.1.1",
"pygithub~=1.59.1",
@@ -25,6 +25,10 @@ release = "crewai_devtools.cli:release"
docs-check = "crewai_devtools.docs_check:docs_check"
devtools = "crewai_devtools.cli:main"
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "--noconftest"
[tool.uv]
exclude-newer = "3 days"

View File

@@ -1,8 +1,8 @@
"""Development tools for version bumping and git automation."""
from collections.abc import Mapping
import os
from pathlib import Path
import re
import subprocess
import sys
import tempfile
@@ -18,6 +18,7 @@ from rich.console import Console
from rich.markdown import Markdown
from rich.panel import Panel
from rich.prompt import Confirm
import tomlkit
from crewai_devtools.docs_check import docs_check
from crewai_devtools.prompts import RELEASE_NOTES_PROMPT, TRANSLATE_RELEASE_NOTES_PROMPT
@@ -169,18 +170,17 @@ def update_pyproject_version(file_path: Path, new_version: str) -> bool:
if not file_path.exists():
return False
content = file_path.read_text()
new_content = re.sub(
r'^(version\s*=\s*")[^"]+(")',
rf"\g<1>{new_version}\2",
content,
count=1,
flags=re.MULTILINE,
)
if new_content != content:
file_path.write_text(new_content)
return True
return False
doc = tomlkit.parse(file_path.read_text())
project = doc.get("project")
if project is None:
return False
old_version = project.get("version")
if old_version is None or old_version == new_version:
return False
project["version"] = new_version
file_path.write_text(tomlkit.dumps(doc))
return True
_DEFAULT_WORKSPACE_PACKAGES: Final[list[str]] = [
@@ -473,6 +473,14 @@ def update_changelog(
return True
def _is_crewai_dep(spec: str) -> bool:
"""Return True if *spec* is a ``crewai`` or ``crewai[...]`` dependency."""
if not spec.startswith("crewai"):
return False
rest = spec[6:] # after "crewai"
return len(rest) > 0 and rest[0] in ("[", "=", ">", "<", "~", "!")
def _pin_crewai_deps(content: str, version: str) -> str:
"""Replace crewai dependency version pins in a pyproject.toml string.
@@ -486,11 +494,21 @@ def _pin_crewai_deps(content: str, version: str) -> str:
Returns:
Transformed content.
"""
return re.sub(
r'"crewai(\[tools\])?(==|>=)[^"]*"',
lambda m: f'"crewai{(m.group(1) or "")!s}=={version}"',
content,
)
doc = tomlkit.parse(content)
for key in ("dependencies", "optional-dependencies"):
deps = doc.get("project", {}).get(key)
if deps is None:
continue
# optional-dependencies is a table of lists; dependencies is a list
dep_lists = deps.values() if isinstance(deps, Mapping) else [deps]
for dep_list in dep_lists:
for i, dep in enumerate(dep_list):
s = str(dep)
if not _is_crewai_dep(s) or ("==" not in s and ">=" not in s):
continue
extras = s[6 : s.index("]") + 1] if "[" in s[6:7] else ""
dep_list[i] = f"crewai{extras}=={version}"
return tomlkit.dumps(doc)
def update_template_dependencies(templates_dir: Path, new_version: str) -> list[Path]:
@@ -1049,6 +1067,11 @@ _ENTERPRISE_EXTRA_PACKAGES: Final[tuple[str, ...]] = tuple(
for p in os.getenv("ENTERPRISE_EXTRA_PACKAGES", "").split(",")
if p.strip()
)
_ENTERPRISE_WORKFLOW_PATHS: Final[tuple[str, ...]] = tuple(
p.strip()
for p in os.getenv("ENTERPRISE_WORKFLOW_PATHS", "").split(",")
if p.strip()
)
def _update_enterprise_crewai_dep(pyproject_path: Path, version: str) -> bool:
@@ -1072,6 +1095,86 @@ def _update_enterprise_crewai_dep(pyproject_path: Path, version: str) -> bool:
return False
def _update_enterprise_workflows(repo_dir: Path, version: str) -> list[Path]:
"""Update crewai version pins in enterprise CI workflow files.
Applies ``_repin_crewai_install`` line-by-line on the raw file so
only version numbers change and all formatting is preserved.
Args:
repo_dir: Root of the cloned enterprise repo.
version: New crewai version string.
Returns:
List of workflow paths that were modified.
"""
updated: list[Path] = []
for rel_path in _ENTERPRISE_WORKFLOW_PATHS:
workflow = repo_dir / rel_path
if not workflow.exists():
continue
raw = workflow.read_text()
lines = raw.splitlines(keepends=True)
changed = False
for i, line in enumerate(lines):
if "crewai[" not in line:
continue
new_line = _repin_crewai_install(line, version)
if new_line != line:
lines[i] = new_line
changed = True
if changed:
new_raw = "".join(lines)
else:
new_raw = raw
if new_raw != raw:
workflow.write_text(new_raw)
updated.append(workflow)
return updated
def _repin_crewai_install(run_value: str, version: str) -> str:
"""Rewrite ``crewai[extras]==old`` pins in a shell command string.
Splits on the known ``crewai[`` prefix and reconstructs the pin
with the new version, avoiding regex.
Args:
run_value: The ``run:`` string from a workflow step.
version: New version to pin to.
Returns:
The updated string.
"""
result: list[str] = []
remainder = run_value
marker = "crewai["
while marker in remainder:
before, _, after = remainder.partition(marker)
result.append(before)
# after looks like: a2a]==1.14.0" ...
bracket_end = after.index("]")
extras = after[:bracket_end]
rest = after[bracket_end + 1 :]
if rest.startswith("=="):
# Find end of version — next quote or whitespace
ver_start = 2 # len("==")
ver_end = ver_start
while ver_end < len(rest) and rest[ver_end] not in ('"', "'", " ", "\n"):
ver_end += 1
result.append(f"crewai[{extras}]=={version}")
remainder = rest[ver_end:]
else:
result.append(f"crewai[{extras}]")
remainder = rest
result.append(remainder)
return "".join(result)
_DEPLOYMENT_TEST_REPO: Final[str] = "crewAIInc/crew_deployment_test"
_PYPI_POLL_INTERVAL: Final[int] = 15
@@ -1099,11 +1202,7 @@ def _update_deployment_test_repo(version: str, is_prerelease: bool) -> None:
pyproject = repo_dir / "pyproject.toml"
content = pyproject.read_text()
new_content = re.sub(
r'"crewai\[tools\]==[^"]+"',
f'"crewai[tools]=={version}"',
content,
)
new_content = _pin_crewai_deps(content, version)
if new_content == content:
console.print(
"[yellow]Warning:[/yellow] No crewai[tools] pin found to update"
@@ -1262,6 +1361,12 @@ def _release_enterprise(version: str, is_prerelease: bool, dry_run: bool) -> Non
f"[green]✓[/green] Updated crewai[tools] dep in {enterprise_dep_path}"
)
# --- update crewai pins in CI workflows ---
for wf in _update_enterprise_workflows(repo_dir, version):
console.print(
f"[green]✓[/green] Updated crewai pin in {wf.relative_to(repo_dir)}"
)
_wait_for_pypi("crewai", version)
console.print("\nSyncing workspace...")

View File

View File

@@ -0,0 +1,225 @@
"""Tests for TOML-based version and dependency update functions."""
from pathlib import Path
from textwrap import dedent
from crewai_devtools.cli import (
_pin_crewai_deps,
_repin_crewai_install,
update_pyproject_version,
)
# --- update_pyproject_version ---
class TestUpdatePyprojectVersion:
def test_updates_version(self, tmp_path: Path) -> None:
pyproject = tmp_path / "pyproject.toml"
pyproject.write_text(
dedent("""\
[project]
name = "my-pkg"
version = "1.0.0"
""")
)
assert update_pyproject_version(pyproject, "2.0.0") is True
assert 'version = "2.0.0"' in pyproject.read_text()
def test_returns_false_when_already_current(self, tmp_path: Path) -> None:
pyproject = tmp_path / "pyproject.toml"
pyproject.write_text(
dedent("""\
[project]
name = "my-pkg"
version = "1.0.0"
""")
)
assert update_pyproject_version(pyproject, "1.0.0") is False
def test_returns_false_when_no_project_section(self, tmp_path: Path) -> None:
pyproject = tmp_path / "pyproject.toml"
pyproject.write_text("[tool.ruff]\nline-length = 88\n")
assert update_pyproject_version(pyproject, "1.0.0") is False
def test_returns_false_when_version_is_dynamic(self, tmp_path: Path) -> None:
pyproject = tmp_path / "pyproject.toml"
pyproject.write_text(
dedent("""\
[project]
name = "my-pkg"
dynamic = ["version"]
""")
)
assert update_pyproject_version(pyproject, "1.0.0") is False
assert 'version = "1.0.0"' not in pyproject.read_text()
def test_returns_false_for_missing_file(self, tmp_path: Path) -> None:
assert update_pyproject_version(tmp_path / "nope.toml", "1.0.0") is False
def test_preserves_comments_and_formatting(self, tmp_path: Path) -> None:
content = dedent("""\
# This is important
[project]
name = "my-pkg"
version = "1.0.0" # current version
description = "A package"
""")
pyproject = tmp_path / "pyproject.toml"
pyproject.write_text(content)
update_pyproject_version(pyproject, "2.0.0")
result = pyproject.read_text()
assert "# This is important" in result
assert 'description = "A package"' in result
# --- _pin_crewai_deps ---
class TestPinCrewaiDeps:
def test_pins_exact_version(self) -> None:
content = dedent("""\
[project]
dependencies = [
"crewai==1.0.0",
]
""")
result = _pin_crewai_deps(content, "2.0.0")
assert '"crewai==2.0.0"' in result
def test_pins_minimum_version(self) -> None:
content = dedent("""\
[project]
dependencies = [
"crewai>=1.0.0",
]
""")
result = _pin_crewai_deps(content, "2.0.0")
assert '"crewai==2.0.0"' in result
assert ">=" not in result
def test_pins_with_tools_extra(self) -> None:
content = dedent("""\
[project]
dependencies = [
"crewai[tools]==1.0.0",
]
""")
result = _pin_crewai_deps(content, "2.0.0")
assert '"crewai[tools]==2.0.0"' in result
def test_leaves_unrelated_deps_alone(self) -> None:
content = dedent("""\
[project]
dependencies = [
"requests>=2.0",
"crewai==1.0.0",
"click~=8.1",
]
""")
result = _pin_crewai_deps(content, "2.0.0")
assert '"requests>=2.0"' in result
assert '"click~=8.1"' in result
def test_handles_optional_dependencies(self) -> None:
content = dedent("""\
[project]
dependencies = []
[project.optional-dependencies]
tools = [
"crewai[tools]>=1.0.0",
]
""")
result = _pin_crewai_deps(content, "3.0.0")
assert '"crewai[tools]==3.0.0"' in result
def test_handles_multiple_crewai_entries(self) -> None:
content = dedent("""\
[project]
dependencies = [
"crewai==1.0.0",
"crewai[tools]==1.0.0",
]
""")
result = _pin_crewai_deps(content, "2.0.0")
assert '"crewai==2.0.0"' in result
assert '"crewai[tools]==2.0.0"' in result
def test_preserves_arbitrary_extras(self) -> None:
content = dedent("""\
[project]
dependencies = [
"crewai[a2a]==1.0.0",
]
""")
result = _pin_crewai_deps(content, "2.0.0")
assert '"crewai[a2a]==2.0.0"' in result
def test_no_deps_returns_unchanged(self) -> None:
content = dedent("""\
[project]
name = "empty"
""")
result = _pin_crewai_deps(content, "2.0.0")
assert "empty" in result
def test_skips_crewai_without_version_specifier(self) -> None:
content = dedent("""\
[project]
dependencies = [
"crewai-tools~=1.0",
]
""")
result = _pin_crewai_deps(content, "2.0.0")
assert '"crewai-tools~=1.0"' in result
def test_skips_crewai_extras_without_pin(self) -> None:
content = dedent("""\
[project]
dependencies = [
"crewai[tools]",
]
""")
result = _pin_crewai_deps(content, "2.0.0")
assert '"crewai[tools]"' in result
assert "==" not in result
# --- _repin_crewai_install ---
class TestRepinCrewaiInstall:
def test_repins_a2a_extra(self) -> None:
result = _repin_crewai_install('uv pip install "crewai[a2a]==1.14.0"', "2.0.0")
assert result == 'uv pip install "crewai[a2a]==2.0.0"'
def test_repins_tools_extra(self) -> None:
result = _repin_crewai_install('uv pip install "crewai[tools]==1.0.0"', "3.0.0")
assert result == 'uv pip install "crewai[tools]==3.0.0"'
def test_leaves_unrelated_commands_alone(self) -> None:
cmd = "uv pip install requests"
assert _repin_crewai_install(cmd, "2.0.0") == cmd
def test_handles_multiple_pins(self) -> None:
cmd = 'pip install "crewai[a2a]==1.0.0" "crewai[tools]==1.0.0"'
result = _repin_crewai_install(cmd, "2.0.0")
assert result == 'pip install "crewai[a2a]==2.0.0" "crewai[tools]==2.0.0"'
def test_preserves_surrounding_text(self) -> None:
cmd = 'echo hello && uv pip install "crewai[a2a]==1.14.0" && echo done'
result = _repin_crewai_install(cmd, "2.0.0")
assert (
result == 'echo hello && uv pip install "crewai[a2a]==2.0.0" && echo done'
)
def test_no_version_specifier_unchanged(self) -> None:
cmd = 'pip install "crewai[tools]>=1.0"'
assert _repin_crewai_install(cmd, "2.0.0") == cmd