mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-05-18 07:28:11 +00:00
Compare commits
7 Commits
1.12.0
...
docs/file-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8ba5b823a | ||
|
|
5a850a708b | ||
|
|
bd03f6cf64 | ||
|
|
a91cd1a7d7 | ||
|
|
66dee3195f | ||
|
|
034f576dc0 | ||
|
|
918654318b |
50
.github/security.md
vendored
50
.github/security.md
vendored
@@ -1,50 +1,12 @@
|
||||
## CrewAI Security Policy
|
||||
|
||||
We are committed to protecting the confidentiality, integrity, and availability of the CrewAI ecosystem. This policy explains how to report potential vulnerabilities and what you can expect from us when you do.
|
||||
|
||||
### Scope
|
||||
|
||||
We welcome reports for vulnerabilities that could impact:
|
||||
|
||||
- CrewAI-maintained source code and repositories
|
||||
- CrewAI-operated infrastructure and services
|
||||
- Official CrewAI releases, packages, and distributions
|
||||
|
||||
Issues affecting clearly unaffiliated third-party services or user-generated content are out of scope, unless you can demonstrate a direct impact on CrewAI systems or customers.
|
||||
We are committed to protecting the confidentiality, integrity, and availability of the
|
||||
CrewAI ecosystem.
|
||||
|
||||
### How to Report
|
||||
|
||||
- **Please do not** disclose vulnerabilities via public GitHub issues, pull requests, or social media.
|
||||
- Email detailed reports to **security@crewai.com** with the subject line `Security Report`.
|
||||
- If you need to share large files or sensitive artifacts, mention it in your email and we will coordinate a secure transfer method.
|
||||
Please submit reports to **crewai-vdp-ess@submit.bugcrowd.com**
|
||||
|
||||
### What to Include
|
||||
|
||||
Providing comprehensive information enables us to validate the issue quickly:
|
||||
|
||||
- **Vulnerability overview** — a concise description and classification (e.g., RCE, privilege escalation)
|
||||
- **Affected components** — repository, branch, tag, or deployed service along with relevant file paths or endpoints
|
||||
- **Reproduction steps** — detailed, step-by-step instructions; include logs, screenshots, or screen recordings when helpful
|
||||
- **Proof-of-concept** — exploit details or code that demonstrates the impact (if available)
|
||||
- **Impact analysis** — severity assessment, potential exploitation scenarios, and any prerequisites or special configurations
|
||||
|
||||
### Our Commitment
|
||||
|
||||
- **Acknowledgement:** We aim to acknowledge your report within two business days.
|
||||
- **Communication:** We will keep you informed about triage results, remediation progress, and planned release timelines.
|
||||
- **Resolution:** Confirmed vulnerabilities will be prioritized based on severity and fixed as quickly as possible.
|
||||
- **Recognition:** We currently do not run a bug bounty program; any rewards or recognition are issued at CrewAI's discretion.
|
||||
|
||||
### Coordinated Disclosure
|
||||
|
||||
We ask that you allow us a reasonable window to investigate and remediate confirmed issues before any public disclosure. We will coordinate publication timelines with you whenever possible.
|
||||
|
||||
### Safe Harbor
|
||||
|
||||
We will not pursue or support legal action against individuals who, in good faith:
|
||||
|
||||
- Follow this policy and refrain from violating any applicable laws
|
||||
- Avoid privacy violations, data destruction, or service disruption
|
||||
- Limit testing to systems in scope and respect rate limits and terms of service
|
||||
|
||||
If you are unsure whether your testing is covered, please contact us at **security@crewai.com** before proceeding.
|
||||
- **Please do not** disclose vulnerabilities via public GitHub issues, pull requests,
|
||||
or social media
|
||||
- Reports submitted via channels other than this Bugcrowd submission email will not be reviewed and will be dismissed
|
||||
|
||||
@@ -4,6 +4,46 @@ description: "تحديثات المنتج والتحسينات وإصلاحات
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="25 مارس 2026">
|
||||
## v1.12.1
|
||||
|
||||
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.12.1)
|
||||
|
||||
## ما الذي تغير
|
||||
|
||||
### الميزات
|
||||
- إضافة request_id إلى HumanFeedbackRequestedEvent
|
||||
- إضافة Qdrant Edge كخلفية تخزين لنظام الذاكرة
|
||||
- إضافة أمر docs-check لتحليل التغييرات وتوليد الوثائق مع الترجمات
|
||||
- إضافة دعم اللغة العربية إلى سجل التغييرات وأدوات الإصدار
|
||||
- إضافة ترجمة باللغة العربية الفصحى لجميع الوثائق
|
||||
- إضافة أمر تسجيل الخروج في واجهة سطر الأوامر
|
||||
- إضافة مهارات الوكيل
|
||||
- تنفيذ root_scope تلقائيًا لعزل الذاكرة الهيكلية
|
||||
- تنفيذ مزودين متوافقين مع OpenAI (OpenRouter، DeepSeek، Ollama، vLLM، Cerebras، Dashscope)
|
||||
|
||||
### إصلاحات الأخطاء
|
||||
- إصلاح بيانات اعتماد غير صحيحة لدفع دفعات التتبع (404)
|
||||
- حل العديد من الأخطاء في نظام تدفق HITL
|
||||
- إصلاح حفظ ذاكرة الوكيل
|
||||
- حل جميع أخطاء mypy الصارمة عبر حزمة crewai
|
||||
- إصلاح استخدام __router_paths__ لطرق المستمع + الموجه في FlowMeta
|
||||
- إصلاح خطأ القيمة عند عدم دعم الملفات
|
||||
- تصحيح صياغة الحجر الصحي لـ litellm في الوثائق
|
||||
- إصلاح جميع أخطاء mypy في crewai-files وإضافة جميع الحزم إلى فحوصات النوع في CI
|
||||
- تثبيت الحد الأعلى لـ litellm على آخر إصدار تم اختباره (1.82.6)
|
||||
|
||||
### الوثائق
|
||||
- تحديث سجل التغييرات والإصدار لـ v1.12.0
|
||||
- إضافة CONTRIBUTING.md
|
||||
- إضافة دليل لاستخدام CrewAI بدون LiteLLM
|
||||
|
||||
## المساهمون
|
||||
|
||||
@akaKuruma، @alex-clawd، @greysonlalonde، @iris-clawd، @joaomdmoura، @lorenzejay، @lucasgomide، @nicoferdi96
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="25 مارس 2026">
|
||||
## v1.12.0
|
||||
|
||||
|
||||
1863
docs/docs.json
1863
docs/docs.json
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,46 @@ description: "Product updates, improvements, and bug fixes for CrewAI"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="Mar 25, 2026">
|
||||
## v1.12.1
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.12.1)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Features
|
||||
- Add request_id to HumanFeedbackRequestedEvent
|
||||
- Add Qdrant Edge storage backend for memory system
|
||||
- Add docs-check command to analyze changes and generate docs with translations
|
||||
- Add Arabic language support to changelog and release tooling
|
||||
- Add modern standard Arabic translation of all documentation
|
||||
- Add logout command in CLI
|
||||
- Add agent skills
|
||||
- Implement automatic root_scope for hierarchical memory isolation
|
||||
- Implement native OpenAI-compatible providers (OpenRouter, DeepSeek, Ollama, vLLM, Cerebras, Dashscope)
|
||||
|
||||
### Bug Fixes
|
||||
- Fix bad credentials for traces batch push (404)
|
||||
- Resolve multiple bugs in HITL flow system
|
||||
- Fix agent memory saving
|
||||
- Resolve all strict mypy errors across crewai package
|
||||
- Fix use of __router_paths__ for listener+router methods in FlowMeta
|
||||
- Fix value error on no file support
|
||||
- Correct litellm quarantine wording in docs
|
||||
- Fix all mypy errors in crewai-files and add all packages to CI type checks
|
||||
- Pin litellm upper bound to last tested version (1.82.6)
|
||||
|
||||
### Documentation
|
||||
- Update changelog and version for v1.12.0
|
||||
- Add CONTRIBUTING.md
|
||||
- Add guide for using CrewAI without LiteLLM
|
||||
|
||||
## Contributors
|
||||
|
||||
@akaKuruma, @alex-clawd, @greysonlalonde, @iris-clawd, @joaomdmoura, @lorenzejay, @lucasgomide, @nicoferdi96
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Mar 25, 2026">
|
||||
## v1.12.0
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -4,6 +4,46 @@ description: "CrewAI의 제품 업데이트, 개선 사항 및 버그 수정"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="2026년 3월 25일">
|
||||
## v1.12.1
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.12.1)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- HumanFeedbackRequestedEvent에 request_id 추가
|
||||
- 메모리 시스템을 위한 Qdrant Edge 저장소 백엔드 추가
|
||||
- 변경 사항을 분석하고 번역된 문서와 함께 문서를 생성하는 docs-check 명령어 추가
|
||||
- 변경 로그 및 릴리스 도구에 아랍어 지원 추가
|
||||
- 모든 문서에 대한 현대 표준 아랍어 번역 추가
|
||||
- CLI에 로그아웃 명령어 추가
|
||||
- 에이전트 기술 추가
|
||||
- 계층적 메모리 격리를 위한 자동 root_scope 구현
|
||||
- OpenAI 호환 네이티브 제공자 구현 (OpenRouter, DeepSeek, Ollama, vLLM, Cerebras, Dashscope)
|
||||
|
||||
### 버그 수정
|
||||
- 트레이스 배치 푸시에 대한 잘못된 자격 증명 수정 (404)
|
||||
- HITL 흐름 시스템의 여러 버그 해결
|
||||
- 에이전트 메모리 저장 수정
|
||||
- crewai 패키지 전반에 걸쳐 모든 엄격한 mypy 오류 해결
|
||||
- FlowMeta의 listener+router 메서드에 대한 __router_paths__ 사용 수정
|
||||
- 파일 지원이 없는 경우 값 오류 수정
|
||||
- 문서에서 litellm 격리 단어 수정
|
||||
- crewai-files의 모든 mypy 오류 수정 및 모든 패키지를 CI 유형 검사에 추가
|
||||
- litellm의 상한을 마지막 테스트된 버전 (1.82.6)으로 고정
|
||||
|
||||
### 문서
|
||||
- v1.12.0에 대한 변경 로그 및 버전 업데이트
|
||||
- CONTRIBUTING.md 추가
|
||||
- LiteLLM 없이 CrewAI를 사용하는 가이드 추가
|
||||
|
||||
## 기여자
|
||||
|
||||
@akaKuruma, @alex-clawd, @greysonlalonde, @iris-clawd, @joaomdmoura, @lorenzejay, @lucasgomide, @nicoferdi96
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026년 3월 25일">
|
||||
## v1.12.0
|
||||
|
||||
|
||||
@@ -4,6 +4,46 @@ description: "Atualizações de produto, melhorias e correções do CrewAI"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="25 mar 2026">
|
||||
## v1.12.1
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.12.1)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Recursos
|
||||
- Adicionar request_id ao HumanFeedbackRequestedEvent
|
||||
- Adicionar backend de armazenamento Qdrant Edge para sistema de memória
|
||||
- Adicionar comando docs-check para analisar mudanças e gerar documentação com traduções
|
||||
- Adicionar suporte ao idioma árabe para changelog e ferramentas de lançamento
|
||||
- Adicionar tradução em árabe padrão moderno de toda a documentação
|
||||
- Adicionar comando de logout na CLI
|
||||
- Adicionar habilidades de agente
|
||||
- Implementar root_scope automático para isolamento hierárquico de memória
|
||||
- Implementar provedores nativos compatíveis com OpenAI (OpenRouter, DeepSeek, Ollama, vLLM, Cerebras, Dashscope)
|
||||
|
||||
### Correções de Bugs
|
||||
- Corrigir credenciais incorretas para envio em lote de traces (404)
|
||||
- Resolver múltiplos bugs no sistema de fluxo HITL
|
||||
- Corrigir salvamento de memória do agente
|
||||
- Resolver todos os erros estritos do mypy no pacote crewai
|
||||
- Corrigir uso de __router_paths__ para métodos listener+router em FlowMeta
|
||||
- Corrigir erro de valor em caso de suporte a nenhum arquivo
|
||||
- Corrigir redação da quarentena do litellm na documentação
|
||||
- Corrigir todos os erros do mypy em crewai-files e adicionar todos os pacotes às verificações de tipo do CI
|
||||
- Fixar limite superior do litellm na última versão testada (1.82.6)
|
||||
|
||||
### Documentação
|
||||
- Atualizar changelog e versão para v1.12.0
|
||||
- Adicionar CONTRIBUTING.md
|
||||
- Adicionar guia para usar CrewAI sem LiteLLM
|
||||
|
||||
## Contribuidores
|
||||
|
||||
@akaKuruma, @alex-clawd, @greysonlalonde, @iris-clawd, @joaomdmoura, @lorenzejay, @lucasgomide, @nicoferdi96
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="25 mar 2026">
|
||||
## v1.12.0
|
||||
|
||||
|
||||
@@ -152,4 +152,4 @@ __all__ = [
|
||||
"wrap_file_source",
|
||||
]
|
||||
|
||||
__version__ = "1.12.0"
|
||||
__version__ = "1.12.1"
|
||||
|
||||
@@ -11,7 +11,7 @@ dependencies = [
|
||||
"pytube~=15.0.0",
|
||||
"requests~=2.32.5",
|
||||
"docker~=7.1.0",
|
||||
"crewai==1.12.0a3",
|
||||
"crewai==1.12.1",
|
||||
"tiktoken~=0.8.0",
|
||||
"beautifulsoup4~=4.13.4",
|
||||
"python-docx~=1.2.0",
|
||||
|
||||
@@ -309,4 +309,4 @@ __all__ = [
|
||||
"ZapierActionTools",
|
||||
]
|
||||
|
||||
__version__ = "1.12.0"
|
||||
__version__ = "1.12.1"
|
||||
|
||||
@@ -54,7 +54,7 @@ Repository = "https://github.com/crewAIInc/crewAI"
|
||||
|
||||
[project.optional-dependencies]
|
||||
tools = [
|
||||
"crewai-tools==1.12.0a3",
|
||||
"crewai-tools==1.12.1",
|
||||
]
|
||||
embeddings = [
|
||||
"tiktoken~=0.8.0"
|
||||
|
||||
@@ -42,7 +42,7 @@ def _suppress_pydantic_deprecation_warnings() -> None:
|
||||
|
||||
_suppress_pydantic_deprecation_warnings()
|
||||
|
||||
__version__ = "1.12.0"
|
||||
__version__ = "1.12.1"
|
||||
_telemetry_submitted = False
|
||||
|
||||
|
||||
|
||||
@@ -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.12.0a3"
|
||||
"crewai[tools]==1.12.1"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -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.12.0a3"
|
||||
"crewai[tools]==1.12.1"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -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.12.0a3"
|
||||
"crewai[tools]==1.12.1"
|
||||
]
|
||||
|
||||
[tool.crewai]
|
||||
|
||||
@@ -178,12 +178,15 @@ class HumanFeedbackRequestedEvent(FlowEvent):
|
||||
output: The method output shown to the human for review.
|
||||
message: The message displayed when requesting feedback.
|
||||
emit: Optional list of possible outcomes for routing.
|
||||
request_id: Platform-assigned identifier for this feedback request,
|
||||
used for correlating the request across system boundaries.
|
||||
"""
|
||||
|
||||
method_name: str
|
||||
output: Any
|
||||
message: str
|
||||
emit: list[str] | None = None
|
||||
request_id: str | None = None
|
||||
type: str = "human_feedback_requested"
|
||||
|
||||
|
||||
@@ -198,9 +201,12 @@ class HumanFeedbackReceivedEvent(FlowEvent):
|
||||
method_name: Name of the method that received feedback.
|
||||
feedback: The raw text feedback provided by the human.
|
||||
outcome: The collapsed outcome string (if emit was specified).
|
||||
request_id: Platform-assigned identifier for this feedback request,
|
||||
used for correlating the response back to its originating request.
|
||||
"""
|
||||
|
||||
method_name: str
|
||||
feedback: str
|
||||
outcome: str | None = None
|
||||
request_id: str | None = None
|
||||
type: str = "human_feedback_received"
|
||||
|
||||
@@ -883,6 +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
|
||||
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())
|
||||
@@ -2290,6 +2291,15 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
result = await result
|
||||
|
||||
self._method_outputs.append(result)
|
||||
|
||||
# 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.
|
||||
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
|
||||
)
|
||||
|
||||
@@ -591,6 +591,12 @@ def human_feedback(
|
||||
):
|
||||
_distill_and_store_lessons(self, method_output, raw_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)
|
||||
if emit:
|
||||
self._human_feedback_method_output = method_output
|
||||
|
||||
return result
|
||||
|
||||
wrapper: Any = async_wrapper
|
||||
@@ -615,6 +621,12 @@ def human_feedback(
|
||||
):
|
||||
_distill_and_store_lessons(self, method_output, raw_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)
|
||||
if emit:
|
||||
self._human_feedback_method_output = method_output
|
||||
|
||||
return result
|
||||
|
||||
wrapper = sync_wrapper
|
||||
|
||||
@@ -246,7 +246,7 @@ class TestHumanFeedbackExecution:
|
||||
@patch("builtins.input", return_value="")
|
||||
@patch("builtins.print")
|
||||
def test_empty_feedback_with_default_outcome(self, mock_print, mock_input):
|
||||
"""Test empty feedback uses default_outcome."""
|
||||
"""Test empty feedback uses default_outcome for routing, but flow returns method output."""
|
||||
|
||||
class TestFlow(Flow):
|
||||
@start()
|
||||
@@ -264,14 +264,16 @@ class TestHumanFeedbackExecution:
|
||||
with patch.object(flow, "_request_human_feedback", return_value=""):
|
||||
result = flow.kickoff()
|
||||
|
||||
assert result == "needs_work"
|
||||
# Flow result is the method's return value, NOT the collapsed outcome
|
||||
assert result == "Content"
|
||||
assert flow.last_human_feedback is not None
|
||||
# But the outcome is still correctly set for routing purposes
|
||||
assert flow.last_human_feedback.outcome == "needs_work"
|
||||
|
||||
@patch("builtins.input", return_value="Approved!")
|
||||
@patch("builtins.print")
|
||||
def test_feedback_collapsing(self, mock_print, mock_input):
|
||||
"""Test that feedback is collapsed to an outcome."""
|
||||
"""Test that feedback is collapsed to an outcome for routing, but flow returns method output."""
|
||||
|
||||
class TestFlow(Flow):
|
||||
@start()
|
||||
@@ -291,8 +293,10 @@ class TestHumanFeedbackExecution:
|
||||
):
|
||||
result = flow.kickoff()
|
||||
|
||||
assert result == "approved"
|
||||
# Flow result is the method's return value, NOT the collapsed outcome
|
||||
assert result == "Content"
|
||||
assert flow.last_human_feedback is not None
|
||||
# But the outcome is still correctly set for routing purposes
|
||||
assert flow.last_human_feedback.outcome == "approved"
|
||||
|
||||
|
||||
@@ -591,3 +595,134 @@ class TestHumanFeedbackLearn:
|
||||
assert config.learn is True
|
||||
# llm defaults to "gpt-4o-mini" at the function level
|
||||
assert config.llm == "gpt-4o-mini"
|
||||
|
||||
|
||||
class TestHumanFeedbackFinalOutputPreservation:
|
||||
"""Tests for preserving method return value as flow's final output when @human_feedback with emit is terminal.
|
||||
|
||||
This addresses the bug where the flow's final output was the collapsed outcome string (e.g., 'approved')
|
||||
instead of the method's actual return value when a @human_feedback method with emit is the final method.
|
||||
"""
|
||||
|
||||
@patch("builtins.input", return_value="Looks good!")
|
||||
@patch("builtins.print")
|
||||
def test_final_output_is_method_return_not_collapsed_outcome(
|
||||
self, mock_print, mock_input
|
||||
):
|
||||
"""When @human_feedback with emit is the final method, flow output is the method's return value."""
|
||||
|
||||
class FinalHumanFeedbackFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(
|
||||
message="Review this content:",
|
||||
emit=["approved", "rejected"],
|
||||
llm="gpt-4o-mini",
|
||||
)
|
||||
def generate_and_review(self):
|
||||
# This dict should be the final output, NOT the string 'approved'
|
||||
return {"title": "My Article", "content": "Article content here", "status": "ready"}
|
||||
|
||||
flow = FinalHumanFeedbackFlow()
|
||||
|
||||
with (
|
||||
patch.object(flow, "_request_human_feedback", return_value="Looks great, approved!"),
|
||||
patch.object(flow, "_collapse_to_outcome", return_value="approved"),
|
||||
):
|
||||
result = flow.kickoff()
|
||||
|
||||
# The final output should be the actual method return value, not the collapsed outcome
|
||||
assert isinstance(result, dict), f"Expected dict, got {type(result).__name__}: {result}"
|
||||
assert result == {"title": "My Article", "content": "Article content here", "status": "ready"}
|
||||
# But the outcome should still be tracked in last_human_feedback
|
||||
assert flow.last_human_feedback is not None
|
||||
assert flow.last_human_feedback.outcome == "approved"
|
||||
|
||||
@patch("builtins.input", return_value="approved")
|
||||
@patch("builtins.print")
|
||||
def test_routing_still_works_with_downstream_listener(self, mock_print, mock_input):
|
||||
"""When @human_feedback has a downstream listener, routing still triggers the listener."""
|
||||
publish_called = []
|
||||
|
||||
class RoutingFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(
|
||||
message="Review:",
|
||||
emit=["approved", "rejected"],
|
||||
llm="gpt-4o-mini",
|
||||
)
|
||||
def review(self):
|
||||
return {"content": "original content"}
|
||||
|
||||
@listen("approved")
|
||||
def publish(self):
|
||||
publish_called.append(True)
|
||||
return {"published": True, "timestamp": "2024-01-01"}
|
||||
|
||||
flow = RoutingFlow()
|
||||
|
||||
with (
|
||||
patch.object(flow, "_request_human_feedback", return_value="LGTM"),
|
||||
patch.object(flow, "_collapse_to_outcome", return_value="approved"),
|
||||
):
|
||||
result = flow.kickoff()
|
||||
|
||||
# The downstream listener should have been triggered
|
||||
assert len(publish_called) == 1, "publish() should have been called"
|
||||
# The final output should be from the listener, not the human_feedback method
|
||||
assert result == {"published": True, "timestamp": "2024-01-01"}
|
||||
|
||||
@patch("builtins.input", return_value="")
|
||||
@patch("builtins.print")
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_human_feedback_final_output_preserved(self, mock_print, mock_input):
|
||||
"""Async @human_feedback methods also preserve the real return value."""
|
||||
|
||||
class AsyncFinalFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(
|
||||
message="Review async content:",
|
||||
emit=["approved", "rejected"],
|
||||
llm="gpt-4o-mini",
|
||||
default_outcome="approved",
|
||||
)
|
||||
async def async_generate(self):
|
||||
return {"async_data": "value", "computed": 42}
|
||||
|
||||
flow = AsyncFinalFlow()
|
||||
|
||||
with (
|
||||
patch.object(flow, "_request_human_feedback", return_value=""),
|
||||
):
|
||||
result = await flow.kickoff_async()
|
||||
|
||||
# The final output should be the dict, not "approved"
|
||||
assert isinstance(result, dict), f"Expected dict, got {type(result).__name__}: {result}"
|
||||
assert result == {"async_data": "value", "computed": 42}
|
||||
assert flow.last_human_feedback.outcome == "approved"
|
||||
|
||||
@patch("builtins.input", return_value="feedback")
|
||||
@patch("builtins.print")
|
||||
def test_method_outputs_contains_real_output(self, mock_print, mock_input):
|
||||
"""The _method_outputs list should contain the real method output, not the collapsed outcome."""
|
||||
|
||||
class OutputTrackingFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(
|
||||
message="Review:",
|
||||
emit=["approved", "rejected"],
|
||||
llm="gpt-4o-mini",
|
||||
)
|
||||
def generate(self):
|
||||
return {"data": "real output"}
|
||||
|
||||
flow = OutputTrackingFlow()
|
||||
|
||||
with (
|
||||
patch.object(flow, "_request_human_feedback", return_value="approved"),
|
||||
patch.object(flow, "_collapse_to_outcome", return_value="approved"),
|
||||
):
|
||||
flow.kickoff()
|
||||
|
||||
# _method_outputs should contain the real output
|
||||
assert len(flow._method_outputs) == 1
|
||||
assert flow._method_outputs[0] == {"data": "real output"}
|
||||
|
||||
@@ -708,7 +708,7 @@ class TestEdgeCases:
|
||||
@patch("builtins.input", return_value="")
|
||||
@patch("builtins.print")
|
||||
def test_empty_feedback_first_outcome_fallback(self, mock_print, mock_input):
|
||||
"""Test that empty feedback without default uses first outcome."""
|
||||
"""Test that empty feedback without default uses first outcome for routing, but returns method output."""
|
||||
|
||||
class FallbackFlow(Flow):
|
||||
@start()
|
||||
@@ -726,12 +726,15 @@ class TestEdgeCases:
|
||||
with patch.object(flow, "_request_human_feedback", return_value=""):
|
||||
result = flow.kickoff()
|
||||
|
||||
assert result == "first" # Falls back to first outcome
|
||||
# Flow result is the method's return value, NOT the collapsed outcome
|
||||
assert result == "content"
|
||||
# But outcome is still set to first for routing purposes
|
||||
assert flow.last_human_feedback.outcome == "first"
|
||||
|
||||
@patch("builtins.input", return_value="whitespace only ")
|
||||
@patch("builtins.print")
|
||||
def test_whitespace_only_feedback_treated_as_empty(self, mock_print, mock_input):
|
||||
"""Test that whitespace-only feedback is treated as empty."""
|
||||
"""Test that whitespace-only feedback is treated as empty for routing, but returns method output."""
|
||||
|
||||
class WhitespaceFlow(Flow):
|
||||
@start()
|
||||
@@ -749,7 +752,10 @@ class TestEdgeCases:
|
||||
with patch.object(flow, "_request_human_feedback", return_value=" "):
|
||||
result = flow.kickoff()
|
||||
|
||||
assert result == "reject" # Uses default because feedback is empty after strip
|
||||
# Flow result is the method's return value, NOT the collapsed outcome
|
||||
assert result == "content"
|
||||
# But outcome is set to default because feedback is empty after strip
|
||||
assert flow.last_human_feedback.outcome == "reject"
|
||||
|
||||
@patch("builtins.input", return_value="feedback")
|
||||
@patch("builtins.print")
|
||||
|
||||
@@ -8,18 +8,22 @@ Installed automatically via the workspace (`uv sync`). Requires:
|
||||
|
||||
- [GitHub CLI](https://cli.github.com/) (`gh`) — authenticated
|
||||
- `OPENAI_API_KEY` env var — for release note generation and translation
|
||||
- `ENTERPRISE_REPO` env var — GitHub repo for enterprise releases
|
||||
- `ENTERPRISE_VERSION_DIRS` env var — comma-separated directories to bump in the enterprise repo
|
||||
- `ENTERPRISE_CREWAI_DEP_PATH` env var — path to the pyproject.toml with the `crewai[tools]` pin in the enterprise repo
|
||||
|
||||
## Commands
|
||||
|
||||
### `devtools release <version>`
|
||||
|
||||
Full end-to-end release. Bumps versions, creates PRs, tags, and publishes a GitHub release.
|
||||
Full end-to-end release. Bumps versions, creates PRs, tags, publishes a GitHub release, and releases the enterprise repo.
|
||||
|
||||
```
|
||||
devtools release 1.10.3
|
||||
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.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
|
||||
```
|
||||
|
||||
**Flow:**
|
||||
@@ -31,6 +35,10 @@ devtools release 1.10.3 --dry-run # preview without changes
|
||||
5. Updates changelogs (en, pt-BR, ko) and docs version switcher
|
||||
6. Creates docs PR against main, polls until merged
|
||||
7. Tags main and creates GitHub release
|
||||
8. Triggers PyPI publish workflow
|
||||
9. Clones enterprise repo, bumps versions and `crewai[tools]` dep, runs `uv sync`
|
||||
10. Creates enterprise bump PR, polls until merged
|
||||
11. Tags and creates GitHub release on enterprise repo
|
||||
|
||||
### `devtools bump <version>`
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""CrewAI development tools."""
|
||||
|
||||
__version__ = "1.12.0"
|
||||
__version__ = "1.12.1"
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from typing import Final, Literal
|
||||
from urllib.request import urlopen
|
||||
|
||||
import click
|
||||
from dotenv import load_dotenv
|
||||
@@ -153,12 +156,24 @@ def update_version_in_file(file_path: Path, new_version: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def update_pyproject_dependencies(file_path: Path, new_version: str) -> bool:
|
||||
_DEFAULT_WORKSPACE_PACKAGES: Final[list[str]] = [
|
||||
"crewai",
|
||||
"crewai-tools",
|
||||
"crewai-devtools",
|
||||
]
|
||||
|
||||
|
||||
def update_pyproject_dependencies(
|
||||
file_path: Path,
|
||||
new_version: str,
|
||||
extra_packages: list[str] | None = None,
|
||||
) -> bool:
|
||||
"""Update workspace dependency versions in pyproject.toml.
|
||||
|
||||
Args:
|
||||
file_path: Path to pyproject.toml file.
|
||||
new_version: New version string.
|
||||
extra_packages: Additional package names to update beyond the defaults.
|
||||
|
||||
Returns:
|
||||
True if any dependencies were updated, False otherwise.
|
||||
@@ -170,7 +185,7 @@ def update_pyproject_dependencies(file_path: Path, new_version: str) -> bool:
|
||||
lines = content.splitlines()
|
||||
updated = False
|
||||
|
||||
workspace_packages = ["crewai", "crewai-tools", "crewai-devtools"]
|
||||
workspace_packages = _DEFAULT_WORKSPACE_PACKAGES + (extra_packages or [])
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
for pkg in workspace_packages:
|
||||
@@ -431,12 +446,29 @@ def update_changelog(
|
||||
return True
|
||||
|
||||
|
||||
def update_template_dependencies(templates_dir: Path, new_version: str) -> list[Path]:
|
||||
"""Update crewai dependency versions in CLI template pyproject.toml files.
|
||||
def _pin_crewai_deps(content: str, version: str) -> str:
|
||||
"""Replace crewai dependency version pins in a pyproject.toml string.
|
||||
|
||||
Handles both pinned (==) and minimum (>=) version specifiers,
|
||||
as well as extras like [tools].
|
||||
|
||||
Args:
|
||||
content: File content to transform.
|
||||
version: New version string.
|
||||
|
||||
Returns:
|
||||
Transformed content.
|
||||
"""
|
||||
return re.sub(
|
||||
r'"crewai(\[tools\])?(==|>=)[^"]*"',
|
||||
lambda m: f'"crewai{(m.group(1) or "")!s}=={version}"',
|
||||
content,
|
||||
)
|
||||
|
||||
|
||||
def update_template_dependencies(templates_dir: Path, new_version: str) -> list[Path]:
|
||||
"""Update crewai dependency versions in CLI template pyproject.toml files.
|
||||
|
||||
Args:
|
||||
templates_dir: Path to the CLI templates directory.
|
||||
new_version: New version string.
|
||||
@@ -444,16 +476,10 @@ def update_template_dependencies(templates_dir: Path, new_version: str) -> list[
|
||||
Returns:
|
||||
List of paths that were updated.
|
||||
"""
|
||||
import re
|
||||
|
||||
updated = []
|
||||
for pyproject in templates_dir.rglob("pyproject.toml"):
|
||||
content = pyproject.read_text()
|
||||
new_content = re.sub(
|
||||
r'"crewai(\[tools\])?(==|>=)[^"]*"',
|
||||
lambda m: f'"crewai{(m.group(1) or "")!s}=={new_version}"',
|
||||
content,
|
||||
)
|
||||
new_content = _pin_crewai_deps(content, new_version)
|
||||
if new_content != content:
|
||||
pyproject.write_text(new_content)
|
||||
updated.append(pyproject)
|
||||
@@ -607,24 +633,26 @@ def get_github_contributors(commit_range: str) -> list[str]:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _poll_pr_until_merged(branch_name: str, label: str) -> None:
|
||||
"""Poll a GitHub PR until it is merged. Exit if closed without merging."""
|
||||
def _poll_pr_until_merged(
|
||||
branch_name: str, label: str, repo: str | None = None
|
||||
) -> None:
|
||||
"""Poll a GitHub PR until it is merged. Exit if closed without merging.
|
||||
|
||||
Args:
|
||||
branch_name: Branch name to look up the PR.
|
||||
label: Human-readable label for status messages.
|
||||
repo: Optional GitHub repo (owner/name) for cross-repo PRs.
|
||||
"""
|
||||
console.print(f"[cyan]Waiting for {label} to be merged...[/cyan]")
|
||||
cmd = ["gh", "pr", "view", branch_name]
|
||||
if repo:
|
||||
cmd.extend(["--repo", repo])
|
||||
cmd.extend(["--json", "state", "--jq", ".state"])
|
||||
|
||||
while True:
|
||||
time.sleep(10)
|
||||
try:
|
||||
state = run_command(
|
||||
[
|
||||
"gh",
|
||||
"pr",
|
||||
"view",
|
||||
branch_name,
|
||||
"--json",
|
||||
"state",
|
||||
"--jq",
|
||||
".state",
|
||||
]
|
||||
)
|
||||
state = run_command(cmd)
|
||||
except subprocess.CalledProcessError:
|
||||
state = ""
|
||||
|
||||
@@ -984,8 +1012,252 @@ def _create_tag_and_release(
|
||||
console.print(f"[green]✓[/green] Created GitHub {release_type} for {tag_name}")
|
||||
|
||||
|
||||
def _trigger_pypi_publish(tag_name: str) -> None:
|
||||
"""Trigger the PyPI publish GitHub Actions workflow."""
|
||||
_ENTERPRISE_REPO: Final[str | None] = os.getenv("ENTERPRISE_REPO")
|
||||
_ENTERPRISE_VERSION_DIRS: Final[tuple[str, ...]] = tuple(
|
||||
d.strip() for d in os.getenv("ENTERPRISE_VERSION_DIRS", "").split(",") if d.strip()
|
||||
)
|
||||
_ENTERPRISE_CREWAI_DEP_PATH: Final[str | None] = os.getenv("ENTERPRISE_CREWAI_DEP_PATH")
|
||||
_ENTERPRISE_EXTRA_PACKAGES: Final[tuple[str, ...]] = tuple(
|
||||
p.strip()
|
||||
for p in os.getenv("ENTERPRISE_EXTRA_PACKAGES", "").split(",")
|
||||
if p.strip()
|
||||
)
|
||||
|
||||
|
||||
def _update_enterprise_crewai_dep(pyproject_path: Path, version: str) -> bool:
|
||||
"""Update the crewai[tools] pin in an enterprise pyproject.toml.
|
||||
|
||||
Args:
|
||||
pyproject_path: Path to the pyproject.toml file.
|
||||
version: New crewai version string.
|
||||
|
||||
Returns:
|
||||
True if the file was modified.
|
||||
"""
|
||||
if not pyproject_path.exists():
|
||||
return False
|
||||
|
||||
content = pyproject_path.read_text()
|
||||
new_content = _pin_crewai_deps(content, version)
|
||||
if new_content != content:
|
||||
pyproject_path.write_text(new_content)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
_PYPI_POLL_INTERVAL: Final[int] = 15
|
||||
_PYPI_POLL_TIMEOUT: Final[int] = 600
|
||||
|
||||
|
||||
def _wait_for_pypi(package: str, version: str) -> None:
|
||||
"""Poll PyPI until a specific package version is available.
|
||||
|
||||
Args:
|
||||
package: PyPI package name.
|
||||
version: Version string to wait for.
|
||||
"""
|
||||
url = f"https://pypi.org/pypi/{package}/{version}/json"
|
||||
deadline = time.monotonic() + _PYPI_POLL_TIMEOUT
|
||||
|
||||
console.print(f"[cyan]Waiting for {package}=={version} to appear on PyPI...[/cyan]")
|
||||
while time.monotonic() < deadline:
|
||||
try:
|
||||
with urlopen(url) as resp: # noqa: S310
|
||||
if resp.status == 200:
|
||||
console.print(
|
||||
f"[green]✓[/green] {package}=={version} is available on PyPI"
|
||||
)
|
||||
return
|
||||
except Exception: # noqa: S110
|
||||
pass
|
||||
time.sleep(_PYPI_POLL_INTERVAL)
|
||||
|
||||
console.print(
|
||||
f"[red]Error:[/red] Timed out waiting for {package}=={version} on PyPI"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _release_enterprise(version: str, is_prerelease: bool, dry_run: bool) -> None:
|
||||
"""Clone the enterprise repo, bump versions, and create a release PR.
|
||||
|
||||
Expects ENTERPRISE_REPO, ENTERPRISE_VERSION_DIRS, and
|
||||
ENTERPRISE_CREWAI_DEP_PATH to be validated before calling.
|
||||
|
||||
Args:
|
||||
version: New version string.
|
||||
is_prerelease: Whether this is a pre-release version.
|
||||
dry_run: Show what would be done without making changes.
|
||||
"""
|
||||
if (
|
||||
not _ENTERPRISE_REPO
|
||||
or not _ENTERPRISE_VERSION_DIRS
|
||||
or not _ENTERPRISE_CREWAI_DEP_PATH
|
||||
):
|
||||
console.print("[red]Error:[/red] Enterprise env vars not configured")
|
||||
sys.exit(1)
|
||||
|
||||
enterprise_repo: str = _ENTERPRISE_REPO
|
||||
enterprise_dep_path: str = _ENTERPRISE_CREWAI_DEP_PATH
|
||||
|
||||
console.print(
|
||||
f"\n[bold cyan]Phase 3: Releasing {enterprise_repo} {version}[/bold cyan]"
|
||||
)
|
||||
|
||||
if dry_run:
|
||||
console.print(f"[dim][DRY RUN][/dim] Would clone {enterprise_repo}")
|
||||
for d in _ENTERPRISE_VERSION_DIRS:
|
||||
console.print(f"[dim][DRY RUN][/dim] Would update versions in {d}")
|
||||
console.print(
|
||||
f"[dim][DRY RUN][/dim] Would update crewai[tools] dep in "
|
||||
f"{enterprise_dep_path}"
|
||||
)
|
||||
console.print(
|
||||
"[dim][DRY RUN][/dim] Would create bump PR, wait for merge, "
|
||||
"then tag and release"
|
||||
)
|
||||
return
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
repo_dir = Path(tmp) / enterprise_repo.split("/")[-1]
|
||||
console.print(f"Cloning {enterprise_repo}...")
|
||||
run_command(["gh", "repo", "clone", enterprise_repo, str(repo_dir)])
|
||||
console.print(f"[green]✓[/green] Cloned {enterprise_repo}")
|
||||
|
||||
# --- bump versions ---
|
||||
for rel_dir in _ENTERPRISE_VERSION_DIRS:
|
||||
pkg_dir = repo_dir / rel_dir
|
||||
if not pkg_dir.exists():
|
||||
console.print(
|
||||
f"[yellow]Warning:[/yellow] {rel_dir} not found, skipping"
|
||||
)
|
||||
continue
|
||||
|
||||
for vfile in find_version_files(pkg_dir):
|
||||
if update_version_in_file(vfile, version):
|
||||
console.print(
|
||||
f"[green]✓[/green] Updated: {vfile.relative_to(repo_dir)}"
|
||||
)
|
||||
|
||||
pyproject = pkg_dir / "pyproject.toml"
|
||||
if pyproject.exists():
|
||||
if update_pyproject_dependencies(
|
||||
pyproject, version, extra_packages=list(_ENTERPRISE_EXTRA_PACKAGES)
|
||||
):
|
||||
console.print(
|
||||
f"[green]✓[/green] Updated deps in: "
|
||||
f"{pyproject.relative_to(repo_dir)}"
|
||||
)
|
||||
|
||||
# --- update crewai[tools] pin ---
|
||||
enterprise_pyproject = repo_dir / enterprise_dep_path
|
||||
if _update_enterprise_crewai_dep(enterprise_pyproject, version):
|
||||
console.print(
|
||||
f"[green]✓[/green] Updated crewai[tools] dep in {enterprise_dep_path}"
|
||||
)
|
||||
|
||||
_wait_for_pypi("crewai", version)
|
||||
|
||||
console.print("\nSyncing workspace...")
|
||||
run_command(["uv", "sync"], cwd=repo_dir)
|
||||
console.print("[green]✓[/green] Workspace synced")
|
||||
|
||||
# --- branch, commit, push, PR ---
|
||||
branch_name = f"feat/bump-version-{version}"
|
||||
run_command(["git", "checkout", "-b", branch_name], cwd=repo_dir)
|
||||
run_command(["git", "add", "."], cwd=repo_dir)
|
||||
run_command(
|
||||
["git", "commit", "-m", f"feat: bump versions to {version}"],
|
||||
cwd=repo_dir,
|
||||
)
|
||||
console.print("[green]✓[/green] Changes committed")
|
||||
|
||||
run_command(["git", "push", "-u", "origin", branch_name], cwd=repo_dir)
|
||||
console.print("[green]✓[/green] Branch pushed")
|
||||
|
||||
run_command(
|
||||
[
|
||||
"gh",
|
||||
"pr",
|
||||
"create",
|
||||
"--repo",
|
||||
enterprise_repo,
|
||||
"--base",
|
||||
"main",
|
||||
"--title",
|
||||
f"feat: bump versions to {version}",
|
||||
"--body",
|
||||
"",
|
||||
],
|
||||
cwd=repo_dir,
|
||||
)
|
||||
console.print("[green]✓[/green] Enterprise bump PR created")
|
||||
|
||||
_poll_pr_until_merged(branch_name, "enterprise bump PR", repo=enterprise_repo)
|
||||
|
||||
# --- tag and release ---
|
||||
run_command(["git", "checkout", "main"], cwd=repo_dir)
|
||||
run_command(["git", "pull"], cwd=repo_dir)
|
||||
|
||||
tag_name = version
|
||||
run_command(
|
||||
["git", "tag", "-a", tag_name, "-m", f"Release {version}"],
|
||||
cwd=repo_dir,
|
||||
)
|
||||
run_command(["git", "push", "origin", tag_name], cwd=repo_dir)
|
||||
console.print(f"[green]✓[/green] Pushed tag {tag_name}")
|
||||
|
||||
gh_cmd = [
|
||||
"gh",
|
||||
"release",
|
||||
"create",
|
||||
tag_name,
|
||||
"--repo",
|
||||
enterprise_repo,
|
||||
"--title",
|
||||
tag_name,
|
||||
"--notes",
|
||||
f"Release {version}",
|
||||
]
|
||||
if is_prerelease:
|
||||
gh_cmd.append("--prerelease")
|
||||
|
||||
run_command(gh_cmd)
|
||||
release_type = "prerelease" if is_prerelease else "release"
|
||||
console.print(
|
||||
f"[green]✓[/green] Created GitHub {release_type} for "
|
||||
f"{enterprise_repo} {tag_name}"
|
||||
)
|
||||
|
||||
|
||||
def _trigger_pypi_publish(tag_name: str, wait: bool = False) -> None:
|
||||
"""Trigger the PyPI publish GitHub Actions workflow.
|
||||
|
||||
Args:
|
||||
tag_name: The release tag to publish.
|
||||
wait: Block until the workflow run completes.
|
||||
"""
|
||||
# Capture the latest run ID before triggering so we can detect the new one
|
||||
prev_run_id = ""
|
||||
if wait:
|
||||
try:
|
||||
prev_run_id = run_command(
|
||||
[
|
||||
"gh",
|
||||
"run",
|
||||
"list",
|
||||
"--workflow=publish.yml",
|
||||
"--limit=1",
|
||||
"--json=databaseId",
|
||||
"--jq=.[0].databaseId",
|
||||
]
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
console.print(
|
||||
"[yellow]Note:[/yellow] Could not determine previous workflow run; "
|
||||
"continuing without previous run ID"
|
||||
)
|
||||
|
||||
with console.status("[cyan]Triggering PyPI publish workflow..."):
|
||||
try:
|
||||
run_command(
|
||||
@@ -1003,6 +1275,42 @@ def _trigger_pypi_publish(tag_name: str) -> None:
|
||||
sys.exit(1)
|
||||
console.print("[green]✓[/green] Triggered PyPI publish workflow")
|
||||
|
||||
if wait:
|
||||
console.print("[cyan]Waiting for PyPI publish workflow to complete...[/cyan]")
|
||||
run_id = ""
|
||||
deadline = time.monotonic() + 120
|
||||
while time.monotonic() < deadline:
|
||||
time.sleep(5)
|
||||
try:
|
||||
run_id = run_command(
|
||||
[
|
||||
"gh",
|
||||
"run",
|
||||
"list",
|
||||
"--workflow=publish.yml",
|
||||
"--limit=1",
|
||||
"--json=databaseId",
|
||||
"--jq=.[0].databaseId",
|
||||
]
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
continue
|
||||
if run_id and run_id != prev_run_id:
|
||||
break
|
||||
|
||||
if not run_id or run_id == prev_run_id:
|
||||
console.print(
|
||||
"[red]Error:[/red] Could not find the PyPI publish workflow run"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
run_command(["gh", "run", "watch", run_id, "--exit-status"])
|
||||
except subprocess.CalledProcessError as e:
|
||||
console.print(f"[red]✗[/red] PyPI publish workflow failed: {e}")
|
||||
sys.exit(1)
|
||||
console.print("[green]✓[/green] PyPI publish workflow completed")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI commands
|
||||
@@ -1032,6 +1340,15 @@ def bump(version: str, dry_run: bool, no_push: bool, no_commit: bool) -> None:
|
||||
no_push: Don't push changes to remote.
|
||||
no_commit: Don't commit changes (just update files).
|
||||
"""
|
||||
console.print(
|
||||
f"\n[yellow]Note:[/yellow] [bold]devtools bump[/bold] only bumps versions "
|
||||
f"in this repo. It will not tag, publish to PyPI, or release enterprise.\n"
|
||||
f"If you want a full end-to-end release, run "
|
||||
f"[bold]devtools release {version}[/bold] instead."
|
||||
)
|
||||
if not Confirm.ask("Continue with bump only?", default=True):
|
||||
sys.exit(0)
|
||||
|
||||
try:
|
||||
check_gh_installed()
|
||||
|
||||
@@ -1136,6 +1453,16 @@ def tag(dry_run: bool, no_edit: bool) -> None:
|
||||
dry_run: Show what would be done without making changes.
|
||||
no_edit: Skip editing release notes.
|
||||
"""
|
||||
console.print(
|
||||
"\n[yellow]Note:[/yellow] [bold]devtools tag[/bold] only tags and creates "
|
||||
"a GitHub release for this repo. It will not bump versions, publish to "
|
||||
"PyPI, or release enterprise.\n"
|
||||
"If you want a full end-to-end release, run "
|
||||
"[bold]devtools release <version>[/bold] instead."
|
||||
)
|
||||
if not Confirm.ask("Continue with tag only?", default=True):
|
||||
sys.exit(0)
|
||||
|
||||
try:
|
||||
cwd = Path.cwd()
|
||||
lib_dir = cwd / "lib"
|
||||
@@ -1226,21 +1553,44 @@ def tag(dry_run: bool, no_edit: bool) -> None:
|
||||
"--dry-run", is_flag=True, help="Show what would be done without making changes"
|
||||
)
|
||||
@click.option("--no-edit", is_flag=True, help="Skip editing release notes")
|
||||
def release(version: str, dry_run: bool, no_edit: bool) -> None:
|
||||
@click.option(
|
||||
"--skip-enterprise",
|
||||
is_flag=True,
|
||||
help="Skip the enterprise release phase",
|
||||
)
|
||||
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,
|
||||
waits for it to be merged, then generates release notes, updates docs,
|
||||
creates the tag, and publishes a GitHub release.
|
||||
creates the tag, and publishes a GitHub release. Then bumps versions and
|
||||
releases the enterprise repo.
|
||||
|
||||
Args:
|
||||
version: New version to set (e.g., 1.0.0, 1.0.0a1).
|
||||
dry_run: Show what would be done without making changes.
|
||||
no_edit: Skip editing release notes.
|
||||
skip_enterprise: Skip the enterprise release phase.
|
||||
"""
|
||||
try:
|
||||
check_gh_installed()
|
||||
|
||||
if not skip_enterprise:
|
||||
missing: list[str] = []
|
||||
if not _ENTERPRISE_REPO:
|
||||
missing.append("ENTERPRISE_REPO")
|
||||
if not _ENTERPRISE_VERSION_DIRS:
|
||||
missing.append("ENTERPRISE_VERSION_DIRS")
|
||||
if not _ENTERPRISE_CREWAI_DEP_PATH:
|
||||
missing.append("ENTERPRISE_CREWAI_DEP_PATH")
|
||||
if missing:
|
||||
console.print(
|
||||
f"[red]Error:[/red] Missing required environment variable(s): "
|
||||
f"{', '.join(missing)}\n"
|
||||
f"Set them or pass --skip-enterprise to skip the enterprise release."
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
cwd = Path.cwd()
|
||||
lib_dir = cwd / "lib"
|
||||
|
||||
@@ -1337,7 +1687,10 @@ def release(version: str, dry_run: bool, no_edit: bool) -> None:
|
||||
|
||||
if not dry_run:
|
||||
_create_tag_and_release(tag_name, release_notes, is_prerelease)
|
||||
_trigger_pypi_publish(tag_name)
|
||||
_trigger_pypi_publish(tag_name, wait=not skip_enterprise)
|
||||
|
||||
if not skip_enterprise:
|
||||
_release_enterprise(version, is_prerelease, dry_run)
|
||||
|
||||
console.print(f"\n[green]✓[/green] Release [bold]{version}[/bold] complete!")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user