diff --git a/lib/devtools/src/crewai_devtools/cli.py b/lib/devtools/src/crewai_devtools/cli.py index c72070de1..682b89058 100644 --- a/lib/devtools/src/crewai_devtools/cli.py +++ b/lib/devtools/src/crewai_devtools/cli.py @@ -156,6 +156,33 @@ def update_version_in_file(file_path: Path, new_version: str) -> bool: return False +def update_pyproject_version(file_path: Path, new_version: str) -> bool: + """Update the [project] version field in a pyproject.toml file. + + Args: + file_path: Path to pyproject.toml file. + new_version: New version string. + + Returns: + True if version was updated, False otherwise. + """ + 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 + + _DEFAULT_WORKSPACE_PACKAGES: Final[list[str]] = [ "crewai", "crewai-tools", @@ -1141,6 +1168,11 @@ def _release_enterprise(version: str, is_prerelease: bool, dry_run: bool) -> Non pyproject = pkg_dir / "pyproject.toml" if pyproject.exists(): + if update_pyproject_version(pyproject, version): + console.print( + f"[green]✓[/green] Updated version in: " + f"{pyproject.relative_to(repo_dir)}" + ) if update_pyproject_dependencies( pyproject, version, extra_packages=list(_ENTERPRISE_EXTRA_PACKAGES) ): @@ -1159,19 +1191,35 @@ def _release_enterprise(version: str, is_prerelease: bool, dry_run: bool) -> Non _wait_for_pypi("crewai", version) console.print("\nSyncing workspace...") - run_command( - [ - "uv", - "sync", - "--refresh-package", - "crewai", - "--refresh-package", - "crewai-tools", - "--refresh-package", - "crewai-files", - ], - cwd=repo_dir, - ) + sync_cmd = [ + "uv", + "sync", + "--refresh-package", + "crewai", + "--refresh-package", + "crewai-tools", + "--refresh-package", + "crewai-files", + ] + if is_prerelease: + sync_cmd.append("--prerelease=allow") + + max_retries = 10 + for attempt in range(1, max_retries + 1): + try: + run_command(sync_cmd, cwd=repo_dir) + break + except subprocess.CalledProcessError: + if attempt == max_retries: + console.print( + f"[red]Error:[/red] uv sync failed after {max_retries} attempts" + ) + raise + console.print( + f"[yellow]uv sync failed (attempt {attempt}/{max_retries})," + f" retrying in {_PYPI_POLL_INTERVAL}s...[/yellow]" + ) + time.sleep(_PYPI_POLL_INTERVAL) console.print("[green]✓[/green] Workspace synced") # --- branch, commit, push, PR --- @@ -1187,7 +1235,7 @@ def _release_enterprise(version: str, is_prerelease: bool, dry_run: bool) -> Non run_command(["git", "push", "-u", "origin", branch_name], cwd=repo_dir) console.print("[green]✓[/green] Branch pushed") - run_command( + pr_url = run_command( [ "gh", "pr", @@ -1204,6 +1252,7 @@ def _release_enterprise(version: str, is_prerelease: bool, dry_run: bool) -> Non cwd=repo_dir, ) console.print("[green]✓[/green] Enterprise bump PR created") + console.print(f"[cyan]PR URL:[/cyan] {pr_url}") _poll_pr_until_merged(branch_name, "enterprise bump PR", repo=enterprise_repo) @@ -1570,7 +1619,18 @@ def tag(dry_run: bool, no_edit: bool) -> None: is_flag=True, help="Skip the enterprise release phase", ) -def release(version: str, dry_run: bool, no_edit: bool, skip_enterprise: bool) -> None: +@click.option( + "--skip-to-enterprise", + is_flag=True, + help="Skip phases 1 & 2, run only the enterprise release phase", +) +def release( + version: str, + dry_run: bool, + no_edit: bool, + skip_enterprise: bool, + skip_to_enterprise: bool, +) -> None: """Full release: bump versions, tag, and publish a GitHub release. Combines bump and tag into a single workflow. Creates a version bump PR, @@ -1583,11 +1643,19 @@ def release(version: str, dry_run: bool, no_edit: bool, skip_enterprise: bool) - dry_run: Show what would be done without making changes. no_edit: Skip editing release notes. skip_enterprise: Skip the enterprise release phase. + skip_to_enterprise: Skip phases 1 & 2, run only the enterprise release phase. """ try: check_gh_installed() - if not skip_enterprise: + if skip_enterprise and skip_to_enterprise: + console.print( + "[red]Error:[/red] Cannot use both --skip-enterprise " + "and --skip-to-enterprise" + ) + sys.exit(1) + + if not skip_enterprise or skip_to_enterprise: missing: list[str] = [] if not _ENTERPRISE_REPO: missing.append("ENTERPRISE_REPO") @@ -1606,6 +1674,15 @@ def release(version: str, dry_run: bool, no_edit: bool, skip_enterprise: bool) - cwd = Path.cwd() lib_dir = cwd / "lib" + is_prerelease = _is_prerelease(version) + + if skip_to_enterprise: + _release_enterprise(version, is_prerelease, dry_run) + console.print( + f"\n[green]✓[/green] Enterprise release [bold]{version}[/bold] complete!" + ) + return + if not dry_run: console.print("Checking git status...") check_git_clean()