diff --git a/lib/crewai/tests/llms/anthropic/test_anthropic.py b/lib/crewai/tests/llms/anthropic/test_anthropic.py index de2ee0320a..e191c443f8 100644 --- a/lib/crewai/tests/llms/anthropic/test_anthropic.py +++ b/lib/crewai/tests/llms/anthropic/test_anthropic.py @@ -47,14 +47,19 @@ def test_anthropic_completion_is_used_when_claude_provider(): -def test_anthropic_completion_module_is_imported(): +def test_anthropic_completion_module_is_imported(monkeypatch): """ Test that the completion module is properly imported when using Anthropic provider """ module_name = "crewai.llms.providers.anthropic.completion" - if module_name in sys.modules: - del sys.modules[module_name] + import crewai.llms.providers.anthropic as anthropic_package + + if hasattr(anthropic_package, "completion"): + monkeypatch.setattr( + anthropic_package, "completion", anthropic_package.completion + ) + monkeypatch.delitem(sys.modules, module_name, raising=False) LLM(model="anthropic/claude-3-5-sonnet-20241022") diff --git a/lib/devtools/README.md b/lib/devtools/README.md index 861af3298a..a63eb91e9c 100644 --- a/lib/devtools/README.md +++ b/lib/devtools/README.md @@ -26,26 +26,28 @@ devtools release 1.10.3a1 # pre-release devtools release 1.10.3 --no-edit # skip editing release notes devtools release 1.10.3 --dry-run # preview without changes devtools release 1.10.3 --skip-enterprise # skip enterprise release phase +devtools release 1.10.3 --skip-litellm-catalog-update # skip the catalog PR ``` **Flow:** -1. Bumps `__version__` and dependency pins across all `lib/` packages -2. Runs `uv sync` -3. Creates version bump PR against main, polls until merged -4. Generates release notes (OpenAI) from commits since last release -5. Updates Edge changelogs (`docs/edge/{en,pt-BR,ko,ar}/changelog.mdx`) -6. Downloads the LiteLLM model catalog into `lib/crewai/src/crewai/llms/litellm_catalog/model_prices_and_context_window.json`, freezes `docs/edge/` into `docs/v/`, registers the version in `docs.json`, and points the canonical `//:slug*` redirects at the new default -7. Opens a `[docs-freeze]` PR against main, polls until merged -8. Tags main and creates GitHub release -9. Triggers PyPI publish workflow -10. Updates `crewAIInc/crew_deployment_test` to the exact CrewAI version, +1. Downloads the LiteLLM model catalog into `lib/crewai/src/crewai/llms/litellm_catalog/model_prices_and_context_window.json` and opens a `chore: update litellm model catalog` PR against main, polling until merged (skipped when the catalog is unchanged) +2. Bumps `__version__` and dependency pins across all `lib/` packages +3. Runs `uv sync` +4. Creates version bump PR against main, polls until merged +5. Generates release notes (OpenAI) from commits since last release +6. Updates Edge changelogs (`docs/edge/{en,pt-BR,ko,ar}/changelog.mdx`) +7. Freezes `docs/edge/` into `docs/v/`, registers the version in `docs.json`, and points the canonical `//:slug*` redirects at the new default +8. Opens a `[docs-freeze]` PR against main, polls until merged +9. Tags main and creates GitHub release +10. Triggers PyPI publish workflow +11. Updates `crewAIInc/crew_deployment_test` to the exact CrewAI version, creates a bump PR, and waits for it to merge -11. Updates `crewAIInc/flow_deployment_test` to the exact CrewAI version, +12. Updates `crewAIInc/flow_deployment_test` to the exact CrewAI version, creates a bump PR, and waits for it to merge -12. Clones enterprise repo, bumps versions and `crewai[tools]` dep, runs `uv sync` -13. Creates enterprise bump PR, polls until merged -14. Tags and creates GitHub release on enterprise repo +13. Clones enterprise repo, bumps versions and `crewai[tools]` dep, runs `uv sync` +14. Creates enterprise bump PR, polls until merged +15. Tags and creates GitHub release on enterprise repo > The `docs-snapshots` CI guard rejects writes under `docs/v*/` and deletions/renames in `docs/images/` unless the PR title starts with `[docs-freeze]`. The release CLI sets that prefix automatically; manual edits to a frozen snapshot need the same prefix to land. > diff --git a/lib/devtools/src/crewai_devtools/cli.py b/lib/devtools/src/crewai_devtools/cli.py index ee943f28a0..cef60ab00f 100644 --- a/lib/devtools/src/crewai_devtools/cli.py +++ b/lib/devtools/src/crewai_devtools/cli.py @@ -1088,6 +1088,59 @@ def _generate_release_notes( return release_notes, openai_client, is_prerelease +def _update_catalog_and_create_pr(cwd: Path, version: str, dry_run: bool) -> str | None: + """Refresh the LiteLLM catalog and open a standalone PR for it. + + Runs before the version bump so the catalog lands on main ahead of the + release commits. The catalog is downloaded before any branch is created, so + when it matches the checked-in file no branch or PR is created. + + Returns: + The catalog branch name if a PR was created, None otherwise. + """ + if dry_run: + console.print( + f"[dim][DRY RUN][/dim] Would download the LiteLLM catalog into {CATALOG_RELATIVE}" + ) + console.print( + f"[dim][DRY RUN][/dim] Would open PR titled " + f"'chore: update litellm model catalog for v{version}' if it changed, " + "and wait for merge" + ) + return None + + run_command(["git", "checkout", "main"]) + run_command(["git", "pull"]) + + catalog_path = update_catalog(cwd) + if not run_command(["git", "status", "--porcelain", "--", str(catalog_path)]): + console.print("[green]✓[/green] LiteLLM catalog is already up to date") + return None + console.print( + f"[green]✓[/green] Updated LiteLLM catalog {catalog_path.relative_to(cwd)}" + ) + + catalog_branch = f"chore/update-litellm-catalog-v{version}" + try: + create_or_reset_branch(catalog_branch) + except BaseException: + run_command(["git", "checkout", "--", str(catalog_path)]) + raise + + pr_title = f"chore: update litellm model catalog for v{version}" + run_command(["git", "add", str(catalog_path)]) + run_command(["git", "commit", "-m", pr_title]) + run_command(["git", "push", "-u", "origin", catalog_branch]) + console.print(f"[green]✓[/green] Pushed branch {catalog_branch}") + + pr_url = run_command( + ["gh", "pr", "create", "--base", "main", "--title", pr_title, "--body", ""] + ) + console.print("[green]✓[/green] Created catalog PR") + console.print(f"[cyan]PR URL:[/cyan] {pr_url}") + return catalog_branch + + def _update_docs_and_create_pr( cwd: Path, version: str, @@ -1098,18 +1151,17 @@ def _update_docs_and_create_pr( ) -> str | None: """Update Edge changelogs, freeze a snapshot, and open the docs PR. - Runs after the version-bump PR has merged. Refreshes the checked-in - LiteLLM catalog, then for a stable release freezes ``docs/edge/`` into - ``docs/v/`` (after the Edge changelogs have been updated so the - snapshot contains the new entry), updates ``docs/docs.json`` to register - the new version and canonical-URL redirects, and opens a ``[docs-freeze]`` - PR. The ``docs-snapshots`` CI guard recognises that title prefix and - allows the snapshot directory to land. The catalog file is included in - that same PR. + Runs after the version-bump PR has merged. For a stable release, freezes + ``docs/edge/`` into ``docs/v/`` (after the Edge changelogs have + been updated so the snapshot contains the new entry), updates + ``docs/docs.json`` to register the new version and canonical-URL + redirects, and opens a ``[docs-freeze]`` PR. The ``docs-snapshots`` CI + guard recognises that title prefix and allows the snapshot directory to + land. - For a pre-release, the catalog is still refreshed and the Edge changelogs - are updated. Pre-releases don't get a frozen snapshot — they ride Edge — - and the PR title omits the ``[docs-freeze]`` prefix. + For a pre-release, only the Edge changelogs are updated. Pre-releases + don't get a frozen snapshot — they ride Edge — and the PR title omits the + ``[docs-freeze]`` prefix. Returns: The docs branch name if a PR was created, None otherwise. @@ -1120,9 +1172,6 @@ def _update_docs_and_create_pr( changelog_langs: list[ChangelogLang] = ["en", "pt-BR", "ko", "ar"] if dry_run: - console.print( - f"[dim][DRY RUN][/dim] Would download the LiteLLM catalog into {CATALOG_RELATIVE}" - ) for lang in changelog_langs: cl_path = edge_root / lang / "changelog.mdx" translated = " (translated)" if lang != "en" else "" @@ -1149,14 +1198,6 @@ def _update_docs_and_create_pr( docs_paths_staged: list[str] = [] - # The bump PR has already merged. Refresh the catalog before the docs - # snapshot so this PR carries the model data that ships with the release. - catalog_path = update_catalog(cwd) - console.print( - f"[green]✓[/green] Updated LiteLLM catalog {catalog_path.relative_to(cwd)}" - ) - docs_paths_staged.append(str(catalog_path)) - # Step 1: update Edge changelogs first so the snapshot we freeze afterwards # contains the new release's entry. for lang in changelog_langs: @@ -2313,16 +2354,23 @@ def tag(dry_run: bool, no_edit: bool) -> None: is_flag=True, help="Skip phases 1 & 2, run only the enterprise release phase", ) +@click.option( + "--skip-litellm-catalog-update", + is_flag=True, + help="Skip the LiteLLM catalog update phase", +) def release( version: str, dry_run: bool, no_edit: bool, skip_enterprise: bool, skip_to_enterprise: bool, + skip_litellm_catalog_update: 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, + Combines bump and tag into a single workflow. Opens a PR refreshing the + LiteLLM model catalog (skipped when unchanged), then creates a version bump PR, waits for it to be merged, then generates release notes, updates docs, creates the tag, and publishes a GitHub release. Then bumps versions and releases the enterprise repo. @@ -2333,12 +2381,15 @@ def release( 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. + skip_litellm_catalog_update: Skip the LiteLLM catalog update phase. """ flags: list[str] = [] if no_edit: flags.append("--no-edit") if skip_enterprise: flags.append("--skip-enterprise") + if skip_litellm_catalog_update: + flags.append("--skip-litellm-catalog-update") flag_suffix = (" " + " ".join(flags)) if flags else "" enterprise_hint = ( "" @@ -2405,6 +2456,29 @@ def release( for pkg in packages: console.print(f" - {pkg.name}") + if skip_litellm_catalog_update: + console.print( + "\n[dim]Skipping Phase 0: LiteLLM catalog update " + "(--skip-litellm-catalog-update)[/dim]" + ) + else: + console.print("\n[bold cyan]Phase 0: Updating the LiteLLM catalog[/bold cyan]") + + try: + catalog_branch = _update_catalog_and_create_pr(cwd, version, dry_run) + if catalog_branch: + _poll_pr_until_merged(catalog_branch, "catalog PR") + run_command(["git", "checkout", "main"]) + run_command(["git", "pull"]) + console.print("[green]✓[/green] main branch updated with the catalog") + except BaseException as e: + _print_release_error(e) + _resume_hint( + f"Phase 0 failed. Fix the issue, then re-run:\n\n" + f" devtools release {version}{flag_suffix}" + ) + sys.exit(1) + console.print(f"\n[bold cyan]Phase 1: Bumping versions to {version}[/bold cyan]") try: diff --git a/lib/devtools/src/crewai_devtools/litellm_catalog_update.py b/lib/devtools/src/crewai_devtools/litellm_catalog_update.py index b2a7c89beb..70acaa5d63 100644 --- a/lib/devtools/src/crewai_devtools/litellm_catalog_update.py +++ b/lib/devtools/src/crewai_devtools/litellm_catalog_update.py @@ -1,8 +1,8 @@ # ruff: noqa: T201 """Refresh the checked-in LiteLLM model catalog. -``devtools release`` calls :func:`update_catalog` after the version-bump PR -merges, while it is preparing the docs PR. The standalone script +``devtools release`` calls :func:`update_catalog` first, in its own PR, before +the version-bump PR. The standalone script ``scripts/update_litellm_catalog.py`` is the manual wrapper. """ diff --git a/lib/devtools/tests/test_release_catalog_phase.py b/lib/devtools/tests/test_release_catalog_phase.py new file mode 100644 index 0000000000..04bc15641b --- /dev/null +++ b/lib/devtools/tests/test_release_catalog_phase.py @@ -0,0 +1,72 @@ +"""Phase 0 of ``devtools release`` opens a catalog PR only when the catalog changed.""" + +from pathlib import Path + +from crewai_devtools import cli +import pytest + + +@pytest.fixture +def calls(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> list[list[str]]: + """Record git/gh commands; ``git status --porcelain`` output is set per test.""" + recorded: list[list[str]] = [] + monkeypatch.setattr(cli, "update_catalog", lambda root: root / cli.CATALOG_RELATIVE) + monkeypatch.setattr( + cli, + "create_or_reset_branch", + lambda branch: recorded.append(["branch", branch]), + ) + return recorded + + +def _fake_run(recorded: list[list[str]], status: str): + def run(cmd: list[str], cwd: Path | None = None) -> str: + recorded.append(cmd) + if cmd[:3] == ["git", "status", "--porcelain"]: + return status + if cmd[:3] == ["gh", "pr", "create"]: + return "https://github.com/o/r/pull/1" + return "" + + return run + + +def test_unchanged_catalog_creates_no_branch_or_pr( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, calls: list[list[str]] +) -> None: + monkeypatch.setattr(cli, "run_command", _fake_run(calls, status="")) + + assert cli._update_catalog_and_create_pr(tmp_path, "1.2.3", dry_run=False) is None + + assert not any(c[0] == "branch" for c in calls) + assert not any(c[:2] == ["gh", "pr"] for c in calls) + + +def test_changed_catalog_opens_a_pr_from_its_own_branch( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, calls: list[list[str]] +) -> None: + monkeypatch.setattr(cli, "run_command", _fake_run(calls, status=" M catalog.json")) + + branch = cli._update_catalog_and_create_pr(tmp_path, "1.2.3", dry_run=False) + + assert branch == "chore/update-litellm-catalog-v1.2.3" + assert ["branch", branch] in calls + pr = next(c for c in calls if c[:3] == ["gh", "pr", "create"]) + assert "chore: update litellm model catalog for v1.2.3" in pr + + +def test_declining_the_branch_reset_restores_the_catalog( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, calls: list[list[str]] +) -> None: + monkeypatch.setattr(cli, "run_command", _fake_run(calls, status=" M catalog.json")) + + def decline(branch: str) -> None: + raise SystemExit(1) + + monkeypatch.setattr(cli, "create_or_reset_branch", decline) + + with pytest.raises(SystemExit): + cli._update_catalog_and_create_pr(tmp_path, "1.2.3", dry_run=False) + + assert any(c[:3] == ["git", "checkout", "--"] for c in calls) + assert not any(c[:2] == ["gh", "pr"] for c in calls) diff --git a/scripts/update_litellm_catalog.py b/scripts/update_litellm_catalog.py index c3487a5d3c..7fe72fc16c 100644 --- a/scripts/update_litellm_catalog.py +++ b/scripts/update_litellm_catalog.py @@ -3,7 +3,8 @@ """Refresh the checked-in LiteLLM model catalog. Manual wrapper around :func:`crewai_devtools.litellm_catalog_update.update_catalog`. -``devtools release`` calls that function after the version-bump PR merges. +``devtools release`` calls that function first, in its own PR, before the +version-bump PR. Usage::