Compare commits

..

2 Commits

Author SHA1 Message Date
Alex
d8ba5b823a docs: add file upload support documentation for flows
- Add 'File Inputs' section to flows.mdx documenting:
  - Using crewai-files types (ImageFile, PDFFile, etc.) in flow state
  - CrewAI Platform (AMP) automatic file upload dropzone rendering
  - API usage with URL string coercion via Pydantic
- Update files.mdx with:
  - Example of file types in flow state
  - Note about CrewAI Platform integration for flows

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-27 00:45:31 -07:00
Joao Moura
5a850a708b fix: preserve method return value as flow output for @human_feedback with emit
When a @human_feedback decorated method with emit= is the final method in a
flow (no downstream listeners triggered), the flow's final output was
incorrectly set to the collapsed outcome string (e.g., 'approved') instead
of the method's actual return value (e.g., a state dict).

Root cause: _process_feedback() returns the collapsed_outcome string when
emit is set, and this string was being stored as the method's result in
_method_outputs.

The fix:
1. In human_feedback.py: After _process_feedback, stash the real method_output
   on the flow instance as _human_feedback_method_output when emit is set.

2. In flow.py: After appending a method result to _method_outputs, check if
   _human_feedback_method_output is set. If so, replace the last entry with
   the stashed real output and clear the stash.

This ensures:
- Routing still works correctly (collapsed outcome used for @listen matching)
- The flow's final result is the actual method return value
- If downstream listeners execute, their results become the final output

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-25 22:51:44 -07:00
27 changed files with 138 additions and 3041 deletions

View File

@@ -23,7 +23,7 @@ jobs:
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: "22"
node-version: "latest"
- name: Install Mintlify CLI
run: npm i -g mintlify

View File

@@ -4,86 +4,6 @@ description: "تحديثات المنتج والتحسينات وإصلاحات
icon: "clock"
mode: "wide"
---
<Update label="27 مارس 2026">
## v1.13.0rc1
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0rc1)
## ما الذي تغير
### الوثائق
- تحديث سجل التغييرات والإصدار لـ v1.13.0a2
## المساهمون
@greysonlalonde
</Update>
<Update label="27 مارس 2026">
## v1.13.0a2
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a2)
## ما الذي تغير
### الميزات
- تحديث تلقائي لمستودع اختبار النشر أثناء الإصدار
- تحسين مرونة إصدار المؤسسات وتجربة المستخدم
### الوثائق
- تحديث سجل التغييرات والإصدار للإصدار v1.13.0a1
## المساهمون
@greysonlalonde
</Update>
<Update label="27 مارس 2026">
## v1.13.0a1
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a1)
## ما الذي تغير
### إصلاحات الأخطاء
- إصلاح الروابط المعطلة في سير العمل الوثائقي عن طريق تثبيت Node على LTS 22
- مسح ذاكرة التخزين المؤقت لـ uv للحزم المنشورة حديثًا في الإصدار المؤسسي
### الوثائق
- إضافة مصفوفة شاملة لأذونات RBAC ودليل النشر
- تحديث سجل التغييرات والإصدار للإصدار v1.12.2
## المساهمون
@greysonlalonde, @iris-clawd, @joaomdmoura
</Update>
<Update label="25 مارس 2026">
## v1.12.2
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.12.2)
## ما الذي تغير
### الميزات
- إضافة مرحلة إصدار المؤسسات إلى إصدار أدوات المطورين
### إصلاحات الأخطاء
- الحفاظ على قيمة إرجاع الطريقة كإخراج تدفق لـ @human_feedback مع emit
### الوثائق
- تحديث سجل التغييرات والإصدار لـ v1.12.1
- مراجعة سياسة الأمان وتعليمات الإبلاغ
## المساهمون
@alex-clawd, @greysonlalonde, @joaomdmoura, @theCyberTech
</Update>
<Update label="25 مارس 2026">
## v1.12.1

File diff suppressed because it is too large Load Diff

View File

@@ -4,86 +4,6 @@ description: "Product updates, improvements, and bug fixes for CrewAI"
icon: "clock"
mode: "wide"
---
<Update label="Mar 27, 2026">
## v1.13.0rc1
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0rc1)
## What's Changed
### Documentation
- Update changelog and version for v1.13.0a2
## Contributors
@greysonlalonde
</Update>
<Update label="Mar 27, 2026">
## v1.13.0a2
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a2)
## What's Changed
### Features
- Auto-update deployment test repo during release
- Improve enterprise release resilience and UX
### Documentation
- Update changelog and version for v1.13.0a1
## Contributors
@greysonlalonde
</Update>
<Update label="Mar 27, 2026">
## v1.13.0a1
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a1)
## What's Changed
### Bug Fixes
- Fix broken links in documentation workflow by pinning Node to LTS 22
- Bust the uv cache for freshly published packages in enterprise release
### Documentation
- Add comprehensive RBAC permissions matrix and deployment guide
- Update changelog and version for v1.12.2
## Contributors
@greysonlalonde, @iris-clawd, @joaomdmoura
</Update>
<Update label="Mar 25, 2026">
## v1.12.2
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.12.2)
## What's Changed
### Features
- Add enterprise release phase to devtools release
### Bug Fixes
- Preserve method return value as flow output for @human_feedback with emit
### Documentation
- Update changelog and version for v1.12.1
- Revise security policy and reporting instructions
## Contributors
@alex-clawd, @greysonlalonde, @joaomdmoura, @theCyberTech
</Update>
<Update label="Mar 25, 2026">
## v1.12.1

View File

@@ -134,6 +134,29 @@ result = flow.kickoff(
)
```
You can also define file types directly in your flow state for structured file handling:
```python
from pydantic import BaseModel
from crewai.flow.flow import Flow, start
from crewai_files import ImageFile, PDFFile
class DocumentState(BaseModel):
document: PDFFile
cover_image: ImageFile
title: str = ""
class DocumentFlow(Flow[DocumentState]):
@start()
def process(self):
content = self.state.document.read()
return {"processed": True}
```
<Note type="info" title="CrewAI Platform Integration">
When deploying flows to the CrewAI Platform (AMP), file fields in your state automatically render as file upload dropzones in the UI. For API usage, you can pass URL strings directly and Pydantic coerces them to file objects automatically. See [Flows - File Inputs](/en/concepts/flows#file-inputs) for details.
</Note>
### With Standalone Agents
Pass files directly to agent kickoff:

View File

@@ -341,6 +341,69 @@ flow.kickoff()
By providing both unstructured and structured state management options, CrewAI Flows empowers developers to build AI workflows that are both flexible and robust, catering to a wide range of application requirements.
## File Inputs
Flows support file inputs through the `crewai-files` package, enabling you to build workflows that process images, PDFs, and other file types. When you use file types like `ImageFile` or `PDFFile` in your flow state, they integrate seamlessly with both local development and the CrewAI Platform.
<Note type="info" title="Optional Dependency">
File support requires the optional `crewai-files` package. Install it with:
```bash
uv add 'crewai[file-processing]'
```
</Note>
### Using File Types in Flow State
You can include file types directly in your structured flow state:
```python
from pydantic import BaseModel
from crewai.flow.flow import Flow, start
from crewai_files import ImageFile, PDFFile
class DocumentProcessingState(BaseModel):
document: PDFFile # Renders as file upload in CrewAI Platform
cover_image: ImageFile # Renders as image upload
title: str = "" # Renders as text input
class DocumentFlow(Flow[DocumentProcessingState]):
@start()
def process_document(self):
# Access the file - works with URLs, paths, or uploaded files
content = self.state.document.read()
# Or pass to an agent with VisionTool, etc.
return {"processed": True}
```
### CrewAI Platform Integration
When you deploy a flow to the CrewAI Platform (AMP), file fields in your state automatically render as file upload dropzones in the UI. This makes it easy to build user-facing applications that accept file uploads without any additional frontend work.
| State Field Type | Platform UI Rendering |
|:-----------------|:----------------------|
| `ImageFile` | Image upload dropzone |
| `PDFFile` | PDF upload dropzone |
| `AudioFile` | Audio upload dropzone |
| `VideoFile` | Video upload dropzone |
| `TextFile` | Text file upload dropzone |
| `str`, `int`, etc. | Standard form inputs |
### API Usage
When calling your flow via API, you can pass URL strings directly for file fields. Pydantic automatically coerces URLs into the appropriate file type:
```python
# API request body - URLs are automatically converted to file objects
{
"document": "https://example.com/report.pdf",
"cover_image": "https://example.com/cover.png",
"title": "Q4 Report"
}
```
For more details on file types, sources, and provider support, see the [Files documentation](/en/concepts/files).
## Flow Persistence
The @persist decorator enables automatic state persistence in CrewAI Flows, allowing you to maintain flow state across restarts or different workflow executions. This decorator can be applied at either the class level or method level, providing flexibility in how you manage state persistence.

View File

@@ -7,13 +7,11 @@ mode: "wide"
## Overview
RBAC in CrewAI AMP enables secure, scalable access management through two layers:
1. **Feature permissions** — control what each role can do across the platform (manage, read, or no access)
2. **Entity-level permissions** — fine-grained access on individual automations, environment variables, LLM connections, and Git repositories
RBAC in CrewAI AMP enables secure, scalable access management through a combination of organizationlevel roles and automationlevel visibility controls.
<Frame>
<img src="/images/enterprise/users_and_roles.png" alt="RBAC overview in CrewAI AMP" />
</Frame>
## Users and Roles
@@ -41,13 +39,6 @@ You can configure users and roles in Settings → Roles.
</Step>
</Steps>
### Predefined Roles
| Role | Description |
| :--------- | :-------------------------------------------------------------------------- |
| **Owner** | Full access to all features and settings. Cannot be restricted. |
| **Member** | Read access to most features, manage access to Studio projects. Cannot modify organization or default settings. |
### Configuration summary
| Area | Where to configure | Options |
@@ -55,80 +46,23 @@ You can configure users and roles in Settings → Roles.
| Users & Roles | Settings → Roles | Predefined: Owner, Member; Custom roles |
| Automation visibility | Automation → Settings → Visibility | Private; Whitelist users/roles |
---
## Automationlevel Access Control
## Feature Permissions Matrix
In addition to organizationwide roles, CrewAI Automations support finegrained visibility settings that let you restrict access to specific automations by user or role.
Every role has a permission level for each feature area. The three levels are:
- **Manage** — full read/write access (create, edit, delete)
- **Read** — view-only access
- **No access** — feature is hidden/inaccessible
| Feature | Owner | Member (default) | Description |
| :------------------------ | :------ | :--------------- | :-------------------------------------------------------------- |
| `usage_dashboards` | Manage | Read | View usage metrics and analytics |
| `crews_dashboards` | Manage | Read | View deployment dashboards, access automation details |
| `invitations` | Manage | Read | Invite new members to the organization |
| `training_ui` | Manage | Read | Access training/fine-tuning interfaces |
| `tools` | Manage | Read | Create and manage tools |
| `agents` | Manage | Read | Create and manage agents |
| `environment_variables` | Manage | Read | Create and manage environment variables |
| `llm_connections` | Manage | Read | Configure LLM provider connections |
| `default_settings` | Manage | No access | Modify organization-wide default settings |
| `organization_settings` | Manage | No access | Manage billing, plans, and organization configuration |
| `studio_projects` | Manage | Manage | Create and edit projects in Studio |
<Tip>
When creating a custom role, you can set each feature independently to **Manage**, **Read**, or **No access** to match your team's needs.
</Tip>
---
## Deploying from GitHub or Zip
One of the most common RBAC questions is: _"What permissions does a team member need to deploy?"_
### Deploy from GitHub
To deploy an automation from a GitHub repository, a user needs:
1. **`crews_dashboards`**: at least `Read` — required to access the automations dashboard where deployments are created
2. **Git repository access** (if entity-level RBAC for Git repositories is enabled): the user's role must be granted access to the specific Git repository via entity-level permissions
3. **`studio_projects`: `Manage`** — if building the crew in Studio before deploying
### Deploy from Zip
To deploy an automation from a Zip file upload, a user needs:
1. **`crews_dashboards`**: at least `Read` — required to access the automations dashboard
2. **Zip deployments enabled**: the organization must not have disabled zip deployments in organization settings
### Quick Reference: Minimum Permissions for Deployment
| Action | Required feature permissions | Additional requirements |
| :------------------- | :------------------------------------ | :----------------------------------------------- |
| Deploy from GitHub | `crews_dashboards: Read` | Git repo entity access (if Git RBAC is enabled) |
| Deploy from Zip | `crews_dashboards: Read` | Zip deployments must be enabled at the org level |
| Build in Studio | `studio_projects: Manage` | — |
| Configure LLM keys | `llm_connections: Manage` | — |
| Set environment vars | `environment_variables: Manage` | Entity-level access (if entity RBAC is enabled) |
---
## Automationlevel Access Control (Entity Permissions)
In addition to organizationwide roles, CrewAI supports finegrained entity-level permissions that restrict access to individual resources.
### Automation Visibility
Automations support visibility settings that restrict access by user or role. This is useful for:
This is useful for:
- Keeping sensitive or experimental automations private
- Managing visibility across large teams or external collaborators
- Testing automations in isolated contexts
Deployments can be configured as private, meaning only whitelisted users and roles will be able to interact with them.
Deployments can be configured as private, meaning only whitelisted users and roles will be able to:
- View the deployment
- Run it or interact with its API
- Access its logs, metrics, and settings
The organization owner always has access, regardless of visibility settings.
You can configure automationlevel access control in Automation → Settings → Visibility tab.
@@ -165,92 +99,9 @@ You can configure automationlevel access control in Automation → Settings
<Frame>
<img src="/images/enterprise/visibility.png" alt="Automation Visibility settings in CrewAI AMP" />
</Frame>
### Deployment Permission Types
When granting entity-level access to a specific automation, you can assign these permission types:
| Permission | What it allows |
| :------------------- | :-------------------------------------------------- |
| `run` | Execute the automation and use its API |
| `traces` | View execution traces and logs |
| `manage_settings` | Edit, redeploy, rollback, or delete the automation |
| `human_in_the_loop` | Respond to human-in-the-loop (HITL) requests |
| `full_access` | All of the above |
### Entity-level RBAC for Other Resources
When entity-level RBAC is enabled, access to these resources can also be controlled per user or role:
| Resource | Controlled by | Description |
| :--------------------- | :------------------------------- | :---------------------------------------------------- |
| Environment variables | Entity RBAC feature flag | Restrict which roles/users can view or manage specific env vars |
| LLM connections | Entity RBAC feature flag | Restrict access to specific LLM provider configurations |
| Git repositories | Git repositories RBAC org setting | Restrict which roles/users can access specific connected repos |
---
## Common Role Patterns
While CrewAI ships with Owner and Member roles, most teams benefit from creating custom roles. Here are common patterns:
### Developer Role
A role for team members who build and deploy automations but don't manage organization settings.
| Feature | Permission |
| :------------------------ | :--------- |
| `usage_dashboards` | Read |
| `crews_dashboards` | Manage |
| `invitations` | Read |
| `training_ui` | Read |
| `tools` | Manage |
| `agents` | Manage |
| `environment_variables` | Manage |
| `llm_connections` | Read |
| `default_settings` | No access |
| `organization_settings` | No access |
| `studio_projects` | Manage |
### Viewer / Stakeholder Role
A role for non-technical stakeholders who need to monitor automations and view results.
| Feature | Permission |
| :------------------------ | :--------- |
| `usage_dashboards` | Read |
| `crews_dashboards` | Read |
| `invitations` | No access |
| `training_ui` | Read |
| `tools` | Read |
| `agents` | Read |
| `environment_variables` | No access |
| `llm_connections` | No access |
| `default_settings` | No access |
| `organization_settings` | No access |
| `studio_projects` | Read |
### Ops / Platform Admin Role
A role for platform operators who manage infrastructure settings but may not build agents.
| Feature | Permission |
| :------------------------ | :--------- |
| `usage_dashboards` | Manage |
| `crews_dashboards` | Manage |
| `invitations` | Manage |
| `training_ui` | Read |
| `tools` | Read |
| `agents` | Read |
| `environment_variables` | Manage |
| `llm_connections` | Manage |
| `default_settings` | Manage |
| `organization_settings` | Read |
| `studio_projects` | Read |
---
<Card title="Need Help?" icon="headset" href="mailto:support@crewai.com">
Contact our support team for assistance with RBAC questions.
</Card>

View File

@@ -4,86 +4,6 @@ description: "CrewAI의 제품 업데이트, 개선 사항 및 버그 수정"
icon: "clock"
mode: "wide"
---
<Update label="2026년 3월 27일">
## v1.13.0rc1
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0rc1)
## 변경 사항
### 문서
- v1.13.0a2의 변경 로그 및 버전 업데이트
## 기여자
@greysonlalonde
</Update>
<Update label="2026년 3월 27일">
## v1.13.0a2
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a2)
## 변경 사항
### 기능
- 릴리스 중 자동 업데이트 배포 테스트 리포지토리
- 기업 릴리스의 복원력 및 사용자 경험 개선
### 문서
- v1.13.0a1에 대한 변경 로그 및 버전 업데이트
## 기여자
@greysonlalonde
</Update>
<Update label="2026년 3월 27일">
## v1.13.0a1
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a1)
## 변경 사항
### 버그 수정
- Node를 LTS 22로 고정하여 문서 작업 흐름의 끊어진 링크 수정
- 기업 릴리스에서 새로 게시된 패키지의 uv 캐시 초기화
### 문서
- 포괄적인 RBAC 권한 매트릭스 및 배포 가이드 추가
- v1.12.2에 대한 변경 로그 및 버전 업데이트
## 기여자
@greysonlalonde, @iris-clawd, @joaomdmoura
</Update>
<Update label="2026년 3월 25일">
## v1.12.2
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.12.2)
## 변경 사항
### 기능
- devtools 릴리스에 기업 릴리스 단계 추가
### 버그 수정
- @human_feedback과 함께 emit을 사용할 때 메서드 반환 값을 흐름 출력으로 유지
### 문서
- v1.12.1에 대한 변경 로그 및 버전 업데이트
- 보안 정책 및 보고 지침 수정
## 기여자
@alex-clawd, @greysonlalonde, @joaomdmoura, @theCyberTech
</Update>
<Update label="2026년 3월 25일">
## v1.12.1

View File

@@ -4,86 +4,6 @@ description: "Atualizações de produto, melhorias e correções do CrewAI"
icon: "clock"
mode: "wide"
---
<Update label="27 mar 2026">
## v1.13.0rc1
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0rc1)
## O que Mudou
### Documentação
- Atualizar changelog e versão para v1.13.0a2
## Contribuidores
@greysonlalonde
</Update>
<Update label="27 mar 2026">
## v1.13.0a2
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a2)
## O que Mudou
### Recursos
- Repositório de teste de implantação de autoatualização durante o lançamento
- Melhorar a resiliência e a experiência do usuário na versão empresarial
### Documentação
- Atualizar changelog e versão para v1.13.0a1
## Contribuidores
@greysonlalonde
</Update>
<Update label="27 mar 2026">
## v1.13.0a1
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a1)
## O que Mudou
### Correções de Bugs
- Corrigir links quebrados no fluxo de documentação fixando o Node na LTS 22
- Limpar o cache uv para pacotes recém-publicados na versão empresarial
### Documentação
- Adicionar uma matriz abrangente de permissões RBAC e guia de implantação
- Atualizar o changelog e a versão para v1.12.2
## Contributors
@greysonlalonde, @iris-clawd, @joaomdmoura
</Update>
<Update label="25 mar 2026">
## v1.12.2
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.12.2)
## O que Mudou
### Recursos
- Adicionar fase de lançamento empresarial ao lançamento do devtools
### Correções de Bugs
- Preservar o valor de retorno do método como saída de fluxo para @human_feedback com emit
### Documentação
- Atualizar changelog e versão para v1.12.1
- Revisar política de segurança e instruções de relatório
## Contributors
@alex-clawd, @greysonlalonde, @joaomdmoura, @theCyberTech
</Update>
<Update label="25 mar 2026">
## v1.12.1

View File

@@ -152,4 +152,4 @@ __all__ = [
"wrap_file_source",
]
__version__ = "1.13.0rc1"
__version__ = "1.12.1"

View File

@@ -11,7 +11,7 @@ dependencies = [
"pytube~=15.0.0",
"requests~=2.32.5",
"docker~=7.1.0",
"crewai==1.13.0rc1",
"crewai==1.12.1",
"tiktoken~=0.8.0",
"beautifulsoup4~=4.13.4",
"python-docx~=1.2.0",

View File

@@ -309,4 +309,4 @@ __all__ = [
"ZapierActionTools",
]
__version__ = "1.13.0rc1"
__version__ = "1.12.1"

View File

@@ -54,7 +54,7 @@ Repository = "https://github.com/crewAIInc/crewAI"
[project.optional-dependencies]
tools = [
"crewai-tools==1.13.0rc1",
"crewai-tools==1.12.1",
]
embeddings = [
"tiktoken~=0.8.0"

View File

@@ -42,7 +42,7 @@ def _suppress_pydantic_deprecation_warnings() -> None:
_suppress_pydantic_deprecation_warnings()
__version__ = "1.13.0rc1"
__version__ = "1.12.1"
_telemetry_submitted = False

View File

@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
authors = [{ name = "Your Name", email = "you@example.com" }]
requires-python = ">=3.10,<3.14"
dependencies = [
"crewai[tools]==1.13.0rc1"
"crewai[tools]==1.12.1"
]
[project.scripts]

View File

@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
authors = [{ name = "Your Name", email = "you@example.com" }]
requires-python = ">=3.10,<3.14"
dependencies = [
"crewai[tools]==1.13.0rc1"
"crewai[tools]==1.12.1"
]
[project.scripts]

View File

@@ -5,7 +5,7 @@ description = "Power up your crews with {{folder_name}}"
readme = "README.md"
requires-python = ">=3.10,<3.14"
dependencies = [
"crewai[tools]==1.13.0rc1"
"crewai[tools]==1.12.1"
]
[tool.crewai]

View File

@@ -1338,18 +1338,7 @@ class Crew(FlowTrackable, BaseModel):
api = getattr(agent.llm, "api", None)
supported_types = get_supported_content_types(provider, api)
# Text files are always auto-injected (inlined as text), even
# when the model does not support multimodal input.
text_prefixes = (
"text/",
"application/json",
"application/xml",
"application/x-yaml",
)
def is_auto_injected(content_type: str) -> bool:
if any(content_type.startswith(t) for t in text_prefixes):
return True
return any(content_type.startswith(t) for t in supported_types)
# Only add read_file tool if there are files that need it

View File

@@ -883,9 +883,7 @@ class Flow(Generic[T], metaclass=FlowMeta):
self.human_feedback_history: list[HumanFeedbackResult] = []
self.last_human_feedback: HumanFeedbackResult | None = None
self._pending_feedback_context: PendingFeedbackContext | None = None
# Per-method stash for real @human_feedback output (keyed by method name)
# Used to decouple routing outcome from method return value when emit is set
self._human_feedback_method_outputs: dict[str, Any] = {}
self._human_feedback_method_output: Any = None # Stashed real output from @human_feedback with emit
self.suppress_flow_events: bool = suppress_flow_events
# User input history (for self.ask())
@@ -2297,12 +2295,10 @@ class Flow(Generic[T], metaclass=FlowMeta):
# For @human_feedback methods with emit, the result is the collapsed outcome
# (e.g., "approved") used for routing. But we want the actual method output
# to be the stored result (for final flow output). Replace the last entry
# if a stashed output exists. Dict-based stash is concurrency-safe and
# handles None return values (presence in dict = stashed, not value).
if method_name in self._human_feedback_method_outputs:
self._method_outputs[-1] = self._human_feedback_method_outputs.pop(
method_name
)
# if a stashed output exists.
if self._human_feedback_method_output is not None:
self._method_outputs[-1] = self._human_feedback_method_output
self._human_feedback_method_output = None
self._method_execution_counts[method_name] = (
self._method_execution_counts.get(method_name, 0) + 1

View File

@@ -594,9 +594,8 @@ def human_feedback(
# Stash the real method output for final flow result when emit is set
# (result is the collapsed outcome string for routing, but we want to
# preserve the actual method output as the flow's final result)
# Uses per-method dict for concurrency safety and to handle None returns
if emit:
self._human_feedback_method_outputs[func.__name__] = method_output
self._human_feedback_method_output = method_output
return result
@@ -625,9 +624,8 @@ def human_feedback(
# Stash the real method output for final flow result when emit is set
# (result is the collapsed outcome string for routing, but we want to
# preserve the actual method output as the flow's final result)
# Uses per-method dict for concurrency safety and to handle None returns
if emit:
self._human_feedback_method_outputs[func.__name__] = method_output
self._human_feedback_method_output = method_output
return result

View File

@@ -54,10 +54,7 @@ from crewai.utilities.string_utils import sanitize_tool_name
try:
from crewai_files import (
aformat_multimodal_content,
format_multimodal_content,
)
from crewai_files import aformat_multimodal_content, format_multimodal_content
HAS_CREWAI_FILES = True
except ImportError:
@@ -2042,10 +2039,6 @@ class LLM(BaseLLM):
For each message with a `files` field, formats the files into
provider-specific content blocks and updates the message content.
Text files (TextFile instances or files with text/* / application/json /
application/xml / application/x-yaml content types) are always inlined
as text content, even when the model does not support multimodal input.
Args:
messages: List of messages that may contain file attachments.
@@ -2056,55 +2049,11 @@ class LLM(BaseLLM):
return messages
if not self.supports_multimodal():
# Inline text files as text; reject non-text files
has_non_text = False
for msg in messages:
files = msg.get("files")
if not files:
continue
text_parts: list[str] = []
non_text_files: dict[str, Any] = {}
for name, file_input in files.items():
if self._is_text_file(file_input):
try:
content = file_input.read_text()
text_parts.append(
f"--- Content of file '{name}' ---\n{content}"
)
except Exception:
non_text_files[name] = file_input
else:
non_text_files[name] = file_input
if non_text_files:
has_non_text = True
if text_parts:
existing_content = msg.get("content", "")
inlined = "\n\n".join(text_parts)
if isinstance(existing_content, str):
msg["content"] = (
f"{existing_content}\n\n{inlined}"
if existing_content
else inlined
)
elif isinstance(existing_content, list):
msg["content"] = [
*existing_content,
self.format_text_content(inlined),
]
if non_text_files:
msg["files"] = non_text_files
else:
msg.pop("files", None)
if has_non_text:
if any(msg.get("files") for msg in messages):
raise ValueError(
f"Model '{self.model}' does not support multimodal input, "
"but non-text files were provided via 'input_files'. "
"Use a vision-capable model or remove the non-text file inputs."
"but files were provided via 'input_files'. "
"Use a vision-capable model or remove the file inputs."
)
return messages
@@ -2141,10 +2090,6 @@ class LLM(BaseLLM):
For each message with a `files` field, formats the files into
provider-specific content blocks and updates the message content.
Text files (TextFile instances or files with text/* / application/json /
application/xml / application/x-yaml content types) are always inlined
as text content, even when the model does not support multimodal input.
Args:
messages: List of messages that may contain file attachments.
@@ -2155,55 +2100,11 @@ class LLM(BaseLLM):
return messages
if not self.supports_multimodal():
# Inline text files as text; reject non-text files
has_non_text = False
for msg in messages:
files = msg.get("files")
if not files:
continue
text_parts: list[str] = []
non_text_files: dict[str, Any] = {}
for name, file_input in files.items():
if self._is_text_file(file_input):
try:
content = file_input.read_text()
text_parts.append(
f"--- Content of file '{name}' ---\n{content}"
)
except Exception:
non_text_files[name] = file_input
else:
non_text_files[name] = file_input
if non_text_files:
has_non_text = True
if text_parts:
existing_content = msg.get("content", "")
inlined = "\n\n".join(text_parts)
if isinstance(existing_content, str):
msg["content"] = (
f"{existing_content}\n\n{inlined}"
if existing_content
else inlined
)
elif isinstance(existing_content, list):
msg["content"] = [
*existing_content,
self.format_text_content(inlined),
]
if non_text_files:
msg["files"] = non_text_files
else:
msg.pop("files", None)
if has_non_text:
if any(msg.get("files") for msg in messages):
raise ValueError(
f"Model '{self.model}' does not support multimodal input, "
"but non-text files were provided via 'input_files'. "
"Use a vision-capable model or remove the non-text file inputs."
"but files were provided via 'input_files'. "
"Use a vision-capable model or remove the file inputs."
)
return messages

View File

@@ -37,7 +37,7 @@ from crewai.types.usage_metrics import UsageMetrics
try:
from crewai_files import TextFile, format_multimodal_content
from crewai_files import format_multimodal_content
HAS_CREWAI_FILES = True
except ImportError:
@@ -635,10 +635,6 @@ class BaseLLM(ABC):
For each message with a `files` field, formats the files into
provider-specific content blocks and updates the message content.
Text files (TextFile instances or files with text/* / application/json /
application/xml / application/x-yaml content types) are always inlined
as text content, even when the model does not support multimodal input.
Args:
messages: List of messages that may contain file attachments.
@@ -648,61 +644,12 @@ class BaseLLM(ABC):
if not HAS_CREWAI_FILES:
return messages
is_multimodal = self.supports_multimodal()
if not is_multimodal:
# Inline text files as text; reject non-text files
has_non_text = False
for msg in messages:
files = msg.get("files")
if not files:
continue
text_parts: list[str] = []
non_text_files: dict[str, Any] = {}
for name, file_input in files.items():
if self._is_text_file(file_input):
try:
content = file_input.read_text()
text_parts.append(
f"--- Content of file '{name}' ---\n{content}"
)
except Exception:
# If reading fails, fall back to tool-based access
non_text_files[name] = file_input
else:
non_text_files[name] = file_input
if non_text_files:
has_non_text = True
# Append inlined text content to the message
if text_parts:
existing_content = msg.get("content", "")
inlined = "\n\n".join(text_parts)
if isinstance(existing_content, str):
msg["content"] = (
f"{existing_content}\n\n{inlined}"
if existing_content
else inlined
)
elif isinstance(existing_content, list):
msg["content"] = [
*existing_content,
self.format_text_content(inlined),
]
# Keep only non-text files (for tool-based access)
if non_text_files:
msg["files"] = non_text_files
else:
msg.pop("files", None)
if has_non_text:
if not self.supports_multimodal():
if any(msg.get("files") for msg in messages):
raise ValueError(
f"Model '{self.model}' does not support multimodal input, "
"but non-text files were provided via 'input_files'. "
"Use a vision-capable model or remove the non-text file inputs."
"but files were provided via 'input_files'. "
"Use a vision-capable model or remove the file inputs."
)
return messages
@@ -733,25 +680,6 @@ class BaseLLM(ABC):
return messages
@staticmethod
def _is_text_file(file_input: Any) -> bool:
"""Check whether a file input is a text file.
Returns True for TextFile instances or files whose content_type
starts with ``text/`` or matches common text-based MIME types
(application/json, application/xml, application/x-yaml).
"""
if HAS_CREWAI_FILES and isinstance(file_input, TextFile):
return True
content_type = getattr(file_input, "content_type", "")
if content_type.startswith("text/"):
return True
return content_type in (
"application/json",
"application/xml",
"application/x-yaml",
)
@staticmethod
def _validate_structured_output(
response: str,

View File

@@ -824,18 +824,7 @@ class Task(BaseModel):
api: str | None = getattr(self.agent.llm, "api", None)
supported_types = get_supported_content_types(provider, api)
# Text files are always auto-injected (inlined as text), even
# when the model does not support multimodal input.
text_prefixes = (
"text/",
"application/json",
"application/xml",
"application/x-yaml",
)
def is_auto_injected(content_type: str) -> bool:
if any(content_type.startswith(t) for t in text_prefixes):
return True
return any(content_type.startswith(t) for t in supported_types)
auto_injected_files = {

View File

@@ -338,280 +338,6 @@ class TestBaseLLMMultimodal:
assert result == {"type": "text", "text": "Hello"}
class TestTextFileInliningNonMultimodal:
"""Tests for text file inlining on non-multimodal models (issue #5137).
When a model does not support multimodal input, text files should be
inlined as plain text in the message content rather than raising a
ValueError.
"""
# --- BaseLLM (native provider path) ---
def test_base_text_file_inlined_on_non_multimodal(self) -> None:
"""TextFile content is inlined when model is not multimodal (BaseLLM)."""
from crewai.llms.base_llm import BaseLLM
class NonMultimodalLLM(BaseLLM):
def call(self, messages, tools=None, callbacks=None):
return "test"
llm = NonMultimodalLLM(model="test-model")
assert llm.supports_multimodal() is False
text_content = b"Hello from a text file!"
messages = [
{
"role": "user",
"content": "Analyse this file",
"files": {"readme": TextFile(source=text_content)},
}
]
result = llm._process_message_files(messages)
assert "files" not in result[0]
assert "Hello from a text file!" in result[0]["content"]
assert "readme" in result[0]["content"]
def test_base_multiple_text_files_inlined(self) -> None:
"""Multiple text files are all inlined on non-multimodal model."""
from crewai.llms.base_llm import BaseLLM
class NonMultimodalLLM(BaseLLM):
def call(self, messages, tools=None, callbacks=None):
return "test"
llm = NonMultimodalLLM(model="test-model")
messages = [
{
"role": "user",
"content": "Analyse these files",
"files": {
"file1": TextFile(source=b"Content of file 1"),
"file2": TextFile(source=b"Content of file 2"),
},
}
]
result = llm._process_message_files(messages)
assert "files" not in result[0]
assert "Content of file 1" in result[0]["content"]
assert "Content of file 2" in result[0]["content"]
def test_base_image_file_still_rejected_on_non_multimodal(self) -> None:
"""ImageFile still raises ValueError on non-multimodal model."""
from crewai.llms.base_llm import BaseLLM
class NonMultimodalLLM(BaseLLM):
def call(self, messages, tools=None, callbacks=None):
return "test"
llm = NonMultimodalLLM(model="test-model")
messages = [
{
"role": "user",
"content": "Describe this image",
"files": {"photo": ImageFile(source=MINIMAL_PNG)},
}
]
with pytest.raises(ValueError, match="non-text files"):
llm._process_message_files(messages)
def test_base_mixed_text_and_image_rejects_but_inlines_text(self) -> None:
"""Mixed text+image: text is inlined, but error is raised for image."""
from crewai.llms.base_llm import BaseLLM
class NonMultimodalLLM(BaseLLM):
def call(self, messages, tools=None, callbacks=None):
return "test"
llm = NonMultimodalLLM(model="test-model")
messages = [
{
"role": "user",
"content": "Process these",
"files": {
"readme": TextFile(source=b"Some text content"),
"photo": ImageFile(source=MINIMAL_PNG),
},
}
]
with pytest.raises(ValueError, match="non-text files"):
llm._process_message_files(messages)
# Text file should have been inlined before the error
assert "Some text content" in messages[0]["content"]
def test_base_no_files_no_error(self) -> None:
"""Messages without files pass through unchanged."""
from crewai.llms.base_llm import BaseLLM
class NonMultimodalLLM(BaseLLM):
def call(self, messages, tools=None, callbacks=None):
return "test"
llm = NonMultimodalLLM(model="test-model")
messages = [
{"role": "user", "content": "No files here"},
]
result = llm._process_message_files(messages)
assert result[0]["content"] == "No files here"
def test_base_text_file_with_empty_existing_content(self) -> None:
"""TextFile inlined when existing content is empty string."""
from crewai.llms.base_llm import BaseLLM
class NonMultimodalLLM(BaseLLM):
def call(self, messages, tools=None, callbacks=None):
return "test"
llm = NonMultimodalLLM(model="test-model")
messages = [
{
"role": "user",
"content": "",
"files": {"doc": TextFile(source=b"File content here")},
}
]
result = llm._process_message_files(messages)
assert "files" not in result[0]
assert "File content here" in result[0]["content"]
# Should not start with newlines when existing content is empty
assert not result[0]["content"].startswith("\n")
# --- LiteLLM LLM class ---
def test_litellm_text_file_inlined_on_non_multimodal(self) -> None:
"""TextFile content is inlined when litellm model is not multimodal."""
llm = LLM(model="gpt-3.5-turbo", is_litellm=True)
assert llm.supports_multimodal() is False
messages = [
{
"role": "user",
"content": "Analyse this file",
"files": {"readme": TextFile(source=b"Hello from litellm test")},
}
]
result = llm._process_message_files(messages)
assert "files" not in result[0]
assert "Hello from litellm test" in result[0]["content"]
def test_litellm_image_file_rejected_on_non_multimodal(self) -> None:
"""ImageFile raises ValueError on non-multimodal litellm model."""
llm = LLM(model="gpt-3.5-turbo", is_litellm=True)
assert llm.supports_multimodal() is False
messages = [
{
"role": "user",
"content": "Describe this",
"files": {"photo": ImageFile(source=MINIMAL_PNG)},
}
]
with pytest.raises(ValueError, match="non-text files"):
llm._process_message_files(messages)
def test_litellm_json_file_inlined_on_non_multimodal(self) -> None:
"""JSON file (application/json) is treated as text and inlined."""
llm = LLM(model="gpt-3.5-turbo", is_litellm=True)
assert llm.supports_multimodal() is False
json_content = b'{"key": "value"}'
messages = [
{
"role": "user",
"content": "Parse this JSON",
"files": {"data": TextFile(source=json_content)},
}
]
result = llm._process_message_files(messages)
assert "files" not in result[0]
assert '{"key": "value"}' in result[0]["content"]
# --- _is_text_file helper ---
def test_is_text_file_with_text_file_instance(self) -> None:
"""_is_text_file returns True for TextFile instances."""
from crewai.llms.base_llm import BaseLLM
assert BaseLLM._is_text_file(TextFile(source=b"hello")) is True
def test_is_text_file_with_image_file_instance(self) -> None:
"""_is_text_file returns False for ImageFile instances."""
from crewai.llms.base_llm import BaseLLM
assert BaseLLM._is_text_file(ImageFile(source=MINIMAL_PNG)) is False
def test_is_text_file_with_pdf_file_instance(self) -> None:
"""_is_text_file returns False for PDFFile instances."""
from crewai.llms.base_llm import BaseLLM
assert BaseLLM._is_text_file(PDFFile(source=MINIMAL_PDF)) is False
def test_is_text_file_with_text_content_type(self) -> None:
"""_is_text_file returns True for objects with text/* content_type."""
from crewai.llms.base_llm import BaseLLM
class MockFile:
content_type = "text/plain"
assert BaseLLM._is_text_file(MockFile()) is True
def test_is_text_file_with_json_content_type(self) -> None:
"""_is_text_file returns True for application/json content_type."""
from crewai.llms.base_llm import BaseLLM
class MockFile:
content_type = "application/json"
assert BaseLLM._is_text_file(MockFile()) is True
def test_is_text_file_with_xml_content_type(self) -> None:
"""_is_text_file returns True for application/xml content_type."""
from crewai.llms.base_llm import BaseLLM
class MockFile:
content_type = "application/xml"
assert BaseLLM._is_text_file(MockFile()) is True
def test_is_text_file_with_yaml_content_type(self) -> None:
"""_is_text_file returns True for application/x-yaml content_type."""
from crewai.llms.base_llm import BaseLLM
class MockFile:
content_type = "application/x-yaml"
assert BaseLLM._is_text_file(MockFile()) is True
def test_is_text_file_with_image_content_type(self) -> None:
"""_is_text_file returns False for image/* content_type."""
from crewai.llms.base_llm import BaseLLM
class MockFile:
content_type = "image/png"
assert BaseLLM._is_text_file(MockFile()) is False
class TestMultipleFilesFormatting:
"""Tests for formatting multiple files at once."""
@@ -646,4 +372,4 @@ class TestMultipleFilesFormatting:
result = format_multimodal_content({}, llm.model)
assert result == []
assert result == []

View File

@@ -726,31 +726,3 @@ class TestHumanFeedbackFinalOutputPreservation:
# _method_outputs should contain the real output
assert len(flow._method_outputs) == 1
assert flow._method_outputs[0] == {"data": "real output"}
@patch("builtins.input", return_value="looks good")
@patch("builtins.print")
def test_none_return_value_is_preserved(self, mock_print, mock_input):
"""A method returning None should preserve None as flow output, not the outcome string."""
class NoneReturnFlow(Flow):
@start()
@human_feedback(
message="Review:",
emit=["approved", "rejected"],
llm="gpt-4o-mini",
)
def process(self):
# Method does work but returns None (implicit)
pass
flow = NoneReturnFlow()
with (
patch.object(flow, "_request_human_feedback", return_value=""),
patch.object(flow, "_collapse_to_outcome", return_value="approved"),
):
result = flow.kickoff()
# Final output should be None (the method's real return), not "approved"
assert result is None, f"Expected None, got {result!r}"
assert flow.last_human_feedback.outcome == "approved"

View File

@@ -1,3 +1,3 @@
"""CrewAI development tools."""
__version__ = "1.13.0rc1"
__version__ = "1.12.1"

View File

@@ -156,33 +156,6 @@ 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",
@@ -1072,84 +1045,10 @@ def _update_enterprise_crewai_dep(pyproject_path: Path, version: str) -> bool:
return False
_DEPLOYMENT_TEST_REPO: Final[str] = "crewAIInc/crew_deployment_test"
_PYPI_POLL_INTERVAL: Final[int] = 15
_PYPI_POLL_TIMEOUT: Final[int] = 600
def _update_deployment_test_repo(version: str, is_prerelease: bool) -> None:
"""Update the deployment test repo to pin the new crewai version.
Clones the repo, updates the crewai[tools] pin in pyproject.toml,
regenerates the lockfile, commits, and pushes directly to main.
Args:
version: New crewai version string.
is_prerelease: Whether this is a pre-release version.
"""
console.print(
f"\n[bold cyan]Updating {_DEPLOYMENT_TEST_REPO} to {version}[/bold cyan]"
)
with tempfile.TemporaryDirectory() as tmp:
repo_dir = Path(tmp) / "crew_deployment_test"
run_command(["gh", "repo", "clone", _DEPLOYMENT_TEST_REPO, str(repo_dir)])
console.print(f"[green]✓[/green] Cloned {_DEPLOYMENT_TEST_REPO}")
pyproject = repo_dir / "pyproject.toml"
content = pyproject.read_text()
new_content = re.sub(
r'"crewai\[tools\]==[^"]+"',
f'"crewai[tools]=={version}"',
content,
)
if new_content == content:
console.print(
"[yellow]Warning:[/yellow] No crewai[tools] pin found to update"
)
return
pyproject.write_text(new_content)
console.print(f"[green]✓[/green] Updated crewai[tools] pin to {version}")
lock_cmd = [
"uv",
"lock",
"--refresh-package",
"crewai",
"--refresh-package",
"crewai-tools",
]
if is_prerelease:
lock_cmd.append("--prerelease=allow")
max_retries = 10
for attempt in range(1, max_retries + 1):
try:
run_command(lock_cmd, cwd=repo_dir)
break
except subprocess.CalledProcessError:
if attempt == max_retries:
console.print(
f"[red]Error:[/red] uv lock failed after {max_retries} attempts"
)
raise
console.print(
f"[yellow]uv lock failed (attempt {attempt}/{max_retries}),"
f" retrying in {_PYPI_POLL_INTERVAL}s...[/yellow]"
)
time.sleep(_PYPI_POLL_INTERVAL)
console.print("[green]✓[/green] Lockfile updated")
run_command(["git", "add", "pyproject.toml", "uv.lock"], cwd=repo_dir)
run_command(
["git", "commit", "-m", f"chore: bump crewai to {version}"],
cwd=repo_dir,
)
run_command(["git", "push"], cwd=repo_dir)
console.print(f"[green]✓[/green] Pushed to {_DEPLOYMENT_TEST_REPO}")
def _wait_for_pypi(package: str, version: str) -> None:
"""Poll PyPI until a specific package version is available.
@@ -1242,11 +1141,6 @@ 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)
):
@@ -1265,35 +1159,7 @@ def _release_enterprise(version: str, is_prerelease: bool, dry_run: bool) -> Non
_wait_for_pypi("crewai", version)
console.print("\nSyncing workspace...")
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)
run_command(["uv", "sync"], cwd=repo_dir)
console.print("[green]✓[/green] Workspace synced")
# --- branch, commit, push, PR ---
@@ -1309,7 +1175,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")
pr_url = run_command(
run_command(
[
"gh",
"pr",
@@ -1326,7 +1192,6 @@ 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)
@@ -1693,18 +1558,7 @@ def tag(dry_run: bool, no_edit: bool) -> None:
is_flag=True,
help="Skip the enterprise release phase",
)
@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:
def release(version: str, dry_run: bool, no_edit: bool, skip_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,
@@ -1717,19 +1571,11 @@ def release(
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 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:
if not skip_enterprise:
missing: list[str] = []
if not _ENTERPRISE_REPO:
missing.append("ENTERPRISE_REPO")
@@ -1748,15 +1594,6 @@ def release(
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()
@@ -1850,8 +1687,7 @@ def release(
if not dry_run:
_create_tag_and_release(tag_name, release_notes, is_prerelease)
_trigger_pypi_publish(tag_name, wait=True)
_update_deployment_test_repo(version, is_prerelease)
_trigger_pypi_publish(tag_name, wait=not skip_enterprise)
if not skip_enterprise:
_release_enterprise(version, is_prerelease, dry_run)