mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-04-30 14:52:36 +00:00
feat: add resume hints to devtools release on failure
This commit is contained in:
@@ -29,6 +29,33 @@ load_dotenv()
|
||||
console = Console()
|
||||
|
||||
|
||||
def _resume_hint(message: str) -> None:
|
||||
"""Print a boxed resume hint after a failure."""
|
||||
console.print()
|
||||
console.print(
|
||||
Panel(
|
||||
message,
|
||||
title="[bold yellow]How to resume[/bold yellow]",
|
||||
border_style="yellow",
|
||||
padding=(1, 2),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _print_release_error(e: BaseException) -> None:
|
||||
"""Print a release error with stderr if available."""
|
||||
if isinstance(e, KeyboardInterrupt):
|
||||
raise
|
||||
if isinstance(e, SystemExit):
|
||||
return
|
||||
if isinstance(e, subprocess.CalledProcessError):
|
||||
console.print(f"[red]Error running command:[/red] {e}")
|
||||
if e.stderr:
|
||||
console.print(e.stderr)
|
||||
else:
|
||||
console.print(f"[red]Error:[/red] {e}")
|
||||
|
||||
|
||||
def run_command(cmd: list[str], cwd: Path | None = None) -> str:
|
||||
"""Run a shell command and return output.
|
||||
|
||||
@@ -264,11 +291,9 @@ def add_docs_version(docs_json_path: Path, version: str) -> bool:
|
||||
if not versions:
|
||||
continue
|
||||
|
||||
# Skip if this version already exists for this language
|
||||
if any(v.get("version") == version_label for v in versions):
|
||||
continue
|
||||
|
||||
# Find the current default and copy its tabs
|
||||
default_version = next(
|
||||
(v for v in versions if v.get("default")),
|
||||
versions[0],
|
||||
@@ -280,10 +305,7 @@ def add_docs_version(docs_json_path: Path, version: str) -> bool:
|
||||
"tabs": default_version.get("tabs", []),
|
||||
}
|
||||
|
||||
# Remove default flag from old default
|
||||
default_version.pop("default", None)
|
||||
|
||||
# Insert new version at the beginning
|
||||
versions.insert(0, new_version)
|
||||
updated = True
|
||||
|
||||
@@ -477,7 +499,7 @@ def _is_crewai_dep(spec: str) -> bool:
|
||||
"""Return True if *spec* is a ``crewai`` or ``crewai[...]`` dependency."""
|
||||
if not spec.startswith("crewai"):
|
||||
return False
|
||||
rest = spec[6:] # after "crewai"
|
||||
rest = spec[6:]
|
||||
return len(rest) > 0 and rest[0] in ("[", "=", ">", "<", "~", "!")
|
||||
|
||||
|
||||
@@ -499,7 +521,6 @@ def _pin_crewai_deps(content: str, version: str) -> str:
|
||||
deps = doc.get("project", {}).get(key)
|
||||
if deps is None:
|
||||
continue
|
||||
# optional-dependencies is a table of lists; dependencies is a list
|
||||
dep_lists = deps.values() if isinstance(deps, Mapping) else [deps]
|
||||
for dep_list in dep_lists:
|
||||
for i, dep in enumerate(dep_list):
|
||||
@@ -638,7 +659,6 @@ def get_github_contributors(commit_range: str) -> list[str]:
|
||||
List of GitHub usernames sorted alphabetically.
|
||||
"""
|
||||
try:
|
||||
# Get GitHub token from gh CLI
|
||||
try:
|
||||
gh_token = run_command(["gh", "auth", "token"])
|
||||
except subprocess.CalledProcessError:
|
||||
@@ -680,11 +700,6 @@ def get_github_contributors(commit_range: str) -> list[str]:
|
||||
return []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Shared workflow helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _poll_pr_until_merged(
|
||||
branch_name: str, label: str, repo: str | None = None
|
||||
) -> None:
|
||||
@@ -764,7 +779,6 @@ def _update_all_versions(
|
||||
"[yellow]Warning:[/yellow] No __version__ attributes found to update"
|
||||
)
|
||||
|
||||
# Update CLI template pyproject.toml files
|
||||
templates_dir = lib_dir / "crewai" / "src" / "crewai" / "cli" / "templates"
|
||||
if templates_dir.exists():
|
||||
if dry_run:
|
||||
@@ -1163,13 +1177,11 @@ def _repin_crewai_install(run_value: str, version: str) -> str:
|
||||
while marker in remainder:
|
||||
before, _, after = remainder.partition(marker)
|
||||
result.append(before)
|
||||
# after looks like: a2a]==1.14.0" ...
|
||||
bracket_end = after.index("]")
|
||||
extras = after[:bracket_end]
|
||||
rest = after[bracket_end + 1 :]
|
||||
if rest.startswith("=="):
|
||||
# Find end of version — next quote or whitespace
|
||||
ver_start = 2 # len("==")
|
||||
ver_start = 2
|
||||
ver_end = ver_start
|
||||
while ver_end < len(rest) and rest[ver_end] not in ('"', "'", " ", "\n"):
|
||||
ver_end += 1
|
||||
@@ -1331,7 +1343,6 @@ def _release_enterprise(version: str, is_prerelease: bool, dry_run: bool) -> Non
|
||||
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():
|
||||
@@ -1361,14 +1372,12 @@ def _release_enterprise(version: str, is_prerelease: bool, dry_run: bool) -> Non
|
||||
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}"
|
||||
)
|
||||
|
||||
# --- update crewai pins in CI workflows ---
|
||||
for wf in _update_enterprise_workflows(repo_dir, version):
|
||||
console.print(
|
||||
f"[green]✓[/green] Updated crewai pin in {wf.relative_to(repo_dir)}"
|
||||
@@ -1408,7 +1417,6 @@ def _release_enterprise(version: str, is_prerelease: bool, dry_run: bool) -> Non
|
||||
time.sleep(_PYPI_POLL_INTERVAL)
|
||||
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)
|
||||
@@ -1442,7 +1450,6 @@ def _release_enterprise(version: str, is_prerelease: bool, dry_run: bool) -> Non
|
||||
|
||||
_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)
|
||||
|
||||
@@ -1484,7 +1491,6 @@ def _trigger_pypi_publish(tag_name: str, wait: bool = False) -> None:
|
||||
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:
|
||||
@@ -1559,11 +1565,6 @@ def _trigger_pypi_publish(tag_name: str, wait: bool = False) -> None:
|
||||
console.print("[green]✓[/green] PyPI publish workflow completed")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI commands
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@click.group()
|
||||
def cli() -> None:
|
||||
"""Development tools for version bumping and git automation."""
|
||||
@@ -1831,62 +1832,80 @@ def release(
|
||||
skip_enterprise: Skip the enterprise release phase.
|
||||
skip_to_enterprise: Skip phases 1 & 2, run only the enterprise release phase.
|
||||
"""
|
||||
try:
|
||||
check_gh_installed()
|
||||
flags: list[str] = []
|
||||
if no_edit:
|
||||
flags.append("--no-edit")
|
||||
if skip_enterprise:
|
||||
flags.append("--skip-enterprise")
|
||||
flag_suffix = (" " + " ".join(flags)) if flags else ""
|
||||
enterprise_hint = (
|
||||
""
|
||||
if skip_enterprise
|
||||
else f"\n\nThen release enterprise:\n\n"
|
||||
f" devtools release {version} --skip-to-enterprise"
|
||||
)
|
||||
|
||||
if skip_enterprise and skip_to_enterprise:
|
||||
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:
|
||||
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(
|
||||
"[red]Error:[/red] Cannot use both --skip-enterprise "
|
||||
"and --skip-to-enterprise"
|
||||
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)
|
||||
|
||||
if not skip_enterprise or skip_to_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"
|
||||
|
||||
cwd = Path.cwd()
|
||||
lib_dir = cwd / "lib"
|
||||
is_prerelease = _is_prerelease(version)
|
||||
|
||||
is_prerelease = _is_prerelease(version)
|
||||
|
||||
if skip_to_enterprise:
|
||||
if skip_to_enterprise:
|
||||
try:
|
||||
_release_enterprise(version, is_prerelease, dry_run)
|
||||
console.print(
|
||||
f"\n[green]✓[/green] Enterprise release [bold]{version}[/bold] complete!"
|
||||
except BaseException as e:
|
||||
_print_release_error(e)
|
||||
_resume_hint(
|
||||
f"Fix the issue, then re-run:\n\n"
|
||||
f" devtools release {version} --skip-to-enterprise"
|
||||
)
|
||||
return
|
||||
|
||||
if not dry_run:
|
||||
console.print("Checking git status...")
|
||||
check_git_clean()
|
||||
console.print("[green]✓[/green] Working directory is clean")
|
||||
else:
|
||||
console.print("[dim][DRY RUN][/dim] Would check git status")
|
||||
|
||||
packages = get_packages(lib_dir)
|
||||
|
||||
console.print(f"\nFound {len(packages)} package(s) to update:")
|
||||
for pkg in packages:
|
||||
console.print(f" - {pkg.name}")
|
||||
|
||||
# --- Phase 1: Bump versions ---
|
||||
sys.exit(1)
|
||||
console.print(
|
||||
f"\n[bold cyan]Phase 1: Bumping versions to {version}[/bold cyan]"
|
||||
f"\n[green]✓[/green] Enterprise release [bold]{version}[/bold] complete!"
|
||||
)
|
||||
return
|
||||
|
||||
if not dry_run:
|
||||
console.print("Checking git status...")
|
||||
check_git_clean()
|
||||
console.print("[green]✓[/green] Working directory is clean")
|
||||
else:
|
||||
console.print("[dim][DRY RUN][/dim] Would check git status")
|
||||
|
||||
packages = get_packages(lib_dir)
|
||||
|
||||
console.print(f"\nFound {len(packages)} package(s) to update:")
|
||||
for pkg in packages:
|
||||
console.print(f" - {pkg.name}")
|
||||
|
||||
console.print(f"\n[bold cyan]Phase 1: Bumping versions to {version}[/bold cyan]")
|
||||
|
||||
try:
|
||||
_update_all_versions(cwd, lib_dir, version, packages, dry_run)
|
||||
|
||||
branch_name = f"feat/bump-version-{version}"
|
||||
@@ -1930,12 +1949,17 @@ def release(
|
||||
console.print(
|
||||
"[dim][DRY RUN][/dim] Would push branch, create PR, and wait for merge"
|
||||
)
|
||||
|
||||
# --- Phase 2: Tag and release ---
|
||||
console.print(
|
||||
f"\n[bold cyan]Phase 2: Tagging and releasing {version}[/bold cyan]"
|
||||
except BaseException as e:
|
||||
_print_release_error(e)
|
||||
_resume_hint(
|
||||
f"Phase 1 failed. Fix the issue, then re-run:\n\n"
|
||||
f" devtools release {version}{flag_suffix}"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
console.print(f"\n[bold cyan]Phase 2: Tagging and releasing {version}[/bold cyan]")
|
||||
|
||||
try:
|
||||
tag_name = version
|
||||
|
||||
if not dry_run:
|
||||
@@ -1962,22 +1986,57 @@ def release(
|
||||
|
||||
if not dry_run:
|
||||
_create_tag_and_release(tag_name, release_notes, is_prerelease)
|
||||
except BaseException as e:
|
||||
_print_release_error(e)
|
||||
_resume_hint(
|
||||
"Phase 2 failed before PyPI publish. The bump PR is already merged.\n"
|
||||
"Fix the issue, then resume with:\n\n"
|
||||
" devtools tag"
|
||||
f"\n\nAfter tagging, publish to PyPI and update deployment test:\n\n"
|
||||
f" gh workflow run publish.yml -f release_tag={version}"
|
||||
f"{enterprise_hint}"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
if not dry_run:
|
||||
_trigger_pypi_publish(tag_name, wait=True)
|
||||
except BaseException as e:
|
||||
_print_release_error(e)
|
||||
_resume_hint(
|
||||
f"Phase 2 failed at PyPI publish. Tag and GitHub release already exist.\n"
|
||||
f"Retry PyPI publish manually:\n\n"
|
||||
f" gh workflow run publish.yml -f release_tag={version}"
|
||||
f"{enterprise_hint}"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
if not dry_run:
|
||||
_update_deployment_test_repo(version, is_prerelease)
|
||||
except BaseException as e:
|
||||
_print_release_error(e)
|
||||
_resume_hint(
|
||||
f"Phase 2 failed updating deployment test repo. "
|
||||
f"Tag, release, and PyPI are done.\n"
|
||||
f"Fix the issue and update {_DEPLOYMENT_TEST_REPO} manually."
|
||||
f"{enterprise_hint}"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
if not skip_enterprise:
|
||||
if not skip_enterprise:
|
||||
try:
|
||||
_release_enterprise(version, is_prerelease, dry_run)
|
||||
except BaseException as e:
|
||||
_print_release_error(e)
|
||||
_resume_hint(
|
||||
f"Phase 3 (enterprise) failed. Phases 1 & 2 completed successfully.\n"
|
||||
f"Fix the issue, then resume:\n\n"
|
||||
f" devtools release {version} --skip-to-enterprise"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
console.print(f"\n[green]✓[/green] Release [bold]{version}[/bold] complete!")
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
console.print(f"[red]Error running command:[/red] {e}")
|
||||
if e.stderr:
|
||||
console.print(e.stderr)
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
console.print(f"[red]Error:[/red] {e}")
|
||||
sys.exit(1)
|
||||
console.print(f"\n[green]✓[/green] Release [bold]{version}[/bold] complete!")
|
||||
|
||||
|
||||
cli.add_command(bump)
|
||||
|
||||
Reference in New Issue
Block a user