From 74705c1f67466658b6151dd719ecb9a05ac7c884 Mon Sep 17 00:00:00 2001 From: UncleCode Date: Fri, 4 Jul 2025 15:02:25 +0800 Subject: [PATCH] Move release scripts to private .scripts folder - Remove release-agent.py, build-nightly.py from public repo - Add .scripts/ to .gitignore for private tools - Maintain clean public repository while keeping internal tools --- .gitignore | 3 + build-nightly.py | 137 -------- release-agent.py | 792 ----------------------------------------------- 3 files changed, 3 insertions(+), 929 deletions(-) delete mode 100755 build-nightly.py delete mode 100755 release-agent.py diff --git a/.gitignore b/.gitignore index 1e16241b..6277b5cf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Scripts folder (private tools) +.scripts/ + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/build-nightly.py b/build-nightly.py deleted file mode 100755 index 76cb9c7e..00000000 --- a/build-nightly.py +++ /dev/null @@ -1,137 +0,0 @@ -#!/usr/bin/env python3 -""" -Build script for creating nightly versions of Crawl4AI. -This script temporarily modifies pyproject.toml to build the nightly package. -""" - -import shutil -import sys -import os -import tempfile -from pathlib import Path - -def modify_files_for_nightly(): - """Modify pyproject.toml and __version__.py for nightly package.""" - - from datetime import datetime - - # Generate date-based version: YY.M.D.HHMMSS - now = datetime.utcnow() - nightly_version = f"{now.year % 100}.{now.month}.{now.day}.{now.strftime('%H%M%S')}" - - # 1. Modify pyproject.toml - pyproject_path = Path("pyproject.toml") - if not pyproject_path.exists(): - print("Error: pyproject.toml not found!") - sys.exit(1) - - with open(pyproject_path, 'r') as f: - content = f.read() - - # Create backup - pyproject_backup = pyproject_path.with_suffix('.toml.backup') - shutil.copy2(pyproject_path, pyproject_backup) - print(f"Created backup: {pyproject_backup}") - - # Modify content for nightly build - modified_content = content.replace( - 'name = "Crawl4AI"', - 'name = "crawl4ai-nightly"' - ) - - # Also update the description - modified_content = modified_content.replace( - 'description = "🚀🤖 Crawl4AI: Open-source LLM Friendly Web Crawler & scraper"', - 'description = "🚀🤖 Crawl4AI: Open-source LLM Friendly Web Crawler & scraper (Nightly Build)"' - ) - - # Update the version attribute to use __nightly_version__ - modified_content = modified_content.replace( - 'version = {attr = "crawl4ai.__version__.__version__"}', - 'version = {attr = "crawl4ai.__version__.__nightly_version__"}' - ) - - # Write modified content - with open(pyproject_path, 'w') as f: - f.write(modified_content) - print("Modified pyproject.toml for nightly build") - - # 2. Update __nightly_version__ in __version__.py - version_path = Path("crawl4ai/__version__.py") - if not version_path.exists(): - print("Error: crawl4ai/__version__.py not found!") - sys.exit(1) - - with open(version_path, 'r') as f: - version_content = f.read() - - # Create backup - version_backup = version_path.with_suffix('.py.backup') - shutil.copy2(version_path, version_backup) - print(f"Created backup: {version_backup}") - - # Update __nightly_version__ - modified_version_content = version_content.replace( - '__nightly_version__ = None', - f'__nightly_version__ = "{nightly_version}"' - ) - - # Write modified content - with open(version_path, 'w') as f: - f.write(modified_version_content) - print(f"Set nightly version: {nightly_version}") - - return pyproject_backup, version_backup - -def restore_files(pyproject_backup, version_backup): - """Restore original files from backups.""" - # Restore pyproject.toml - pyproject_path = Path("pyproject.toml") - shutil.move(pyproject_backup, pyproject_path) - print("Restored original pyproject.toml") - - # Restore __version__.py - version_path = Path("crawl4ai/__version__.py") - shutil.move(version_backup, version_path) - print("Restored original __version__.py") - -def main(): - """Main function to handle build process.""" - # Set environment variable for nightly versioning - os.environ['CRAWL4AI_NIGHTLY'] = '1' - - try: - # Modify files for nightly - pyproject_backup, version_backup = modify_files_for_nightly() - - print("\nReady for nightly build!") - print("Run your build command now (e.g., 'python -m build')") - print(f"\nTo restore original files, run:") - print(f" python build-nightly.py --restore") - - except Exception as e: - print(f"Error: {e}") - sys.exit(1) - -def restore_mode(): - """Restore original files from backups.""" - pyproject_backup = Path("pyproject.toml.backup") - version_backup = Path("crawl4ai/__version__.py.backup") - - if pyproject_backup.exists() and version_backup.exists(): - restore_files(pyproject_backup, version_backup) - else: - if pyproject_backup.exists(): - shutil.move(pyproject_backup, Path("pyproject.toml")) - print("Restored pyproject.toml") - if version_backup.exists(): - shutil.move(version_backup, Path("crawl4ai/__version__.py")) - print("Restored __version__.py") - if not pyproject_backup.exists() and not version_backup.exists(): - print("No backups found. Nothing to restore.") - -if __name__ == "__main__": - if len(sys.argv) > 1 and sys.argv[1] == "--restore": - restore_mode() - else: - main() \ No newline at end of file diff --git a/release-agent.py b/release-agent.py deleted file mode 100755 index 4d8e7680..00000000 --- a/release-agent.py +++ /dev/null @@ -1,792 +0,0 @@ -#!/usr/bin/env python3 -""" -Crawl4AI Release Agent - Automated release management with LLM assistance -""" - -import os -import sys -import re -import json -import subprocess -from dataclasses import dataclass, field -from typing import List, Dict, Optional, Literal, Tuple -from datetime import datetime -from pathlib import Path -import click -from rich.console import Console -from rich.prompt import Prompt, Confirm -from rich.table import Table -from rich.progress import Progress, SpinnerColumn, TextColumn -import litellm - -console = Console() - -# State machine states -States = Literal[ - "init", - "commit_selection", - "version_bump", - "test_generation", - "test_execution", - "release_notes", - "demo_generation", - "docs_update", - "branch_creation", - "build_publish", - "complete" -] - -@dataclass -class SharedContext: - """Shared context that grows throughout the release process""" - selected_commits: List[Dict] = field(default_factory=list) - version: str = "" - old_version: str = "" - test_script: str = "" - test_results: Dict = field(default_factory=dict) - release_notes: str = "" - demo_script: str = "" - branch_name: str = "" - - # Growing context - decisions: List[Dict] = field(default_factory=list) - files_changed: List[str] = field(default_factory=list) - api_changes: List[str] = field(default_factory=list) - - def add_decision(self, step: str, decision: str, reason: str = ""): - """Track decisions made during the process""" - self.decisions.append({ - "step": step, - "decision": decision, - "reason": reason, - "timestamp": datetime.now().isoformat() - }) - -@dataclass -class JudgeResult: - """Result from the judge LLM""" - status: Literal["good", "retry", "human"] - feedback: str - specific_issues: List[str] = field(default_factory=list) - -class LLMManager: - """Manages stateless LLM calls with context engineering""" - - def __init__(self, main_model: str = "claude-sonnet-4-20250514", judge_model: str = "claude-sonnet-4-20250514"): - self.main_model = os.getenv("MAIN_MODEL", main_model) - self.judge_model = os.getenv("JUDGE_MODEL", judge_model) - - def call(self, - task: str, - context: Dict, - model: Optional[str] = None, - temperature: float = 0.7) -> str: - """ - Make a stateless LLM call with engineered context - """ - model = model or self.main_model - - # Build system message with context engineering - system_message = self._build_system_message(context) - - # Single user message with the task - messages = [ - {"role": "system", "content": system_message}, - {"role": "user", "content": task} - ] - - try: - response = litellm.completion( - model=model, - messages=messages, - temperature=temperature, - max_tokens=16000, - ) - return response.choices[0].message.content - except Exception as e: - console.print(f"[red]LLM Error: {e}[/red]") - raise - - def _extract_json(self, response: str) -> Dict: - """Extract JSON from tags""" - import re - json_match = re.search(r'(.*?)', response, re.DOTALL) - if json_match: - json_str = json_match.group(1).strip() - return json.loads(json_str) - raise ValueError("No JSON found in response") - - def get_relevant_files(self, query: str, num_files: int = 10) -> List[Dict[str, str]]: - """Use LLM to select relevant files from codebase for context""" - - # Get directory structure - crawl4ai_files = [] - docs_files = [] - examples_files = [] - - # Scan crawl4ai directory - for root, dirs, files in os.walk("crawl4ai"): - # Skip __pycache__ and other unwanted directories - dirs[:] = [d for d in dirs if not d.startswith('__') and d != '.git'] - for file in files: - if file.endswith('.py') and not file.startswith('__'): - rel_path = os.path.relpath(os.path.join(root, file)) - crawl4ai_files.append(rel_path) - - # Scan docs directory - if os.path.exists("docs"): - for root, dirs, files in os.walk("docs"): - dirs[:] = [d for d in dirs if not d.startswith('.')] - for file in files: - if file.endswith(('.md', '.rst')): - rel_path = os.path.relpath(os.path.join(root, file)) - docs_files.append(rel_path) - - # Scan examples directory - if os.path.exists("examples"): - for root, dirs, files in os.walk("examples"): - for file in files: - if file.endswith('.py'): - rel_path = os.path.relpath(os.path.join(root, file)) - examples_files.append(rel_path) - - # Build file selection prompt - file_selection_prompt = f"""Select the most relevant files to understand Crawl4AI for the following task: - - -{query} - - - -## Core Library Files: -{chr(10).join(crawl4ai_files[:50])} # Limit to prevent context overflow - -## Documentation Files: -{chr(10).join(docs_files[:30])} - -## Example Files: -{chr(10).join(examples_files[:20])} - - -Select exactly {num_files} files that would be most helpful for understanding Crawl4AI in the context of the given task. -Prioritize: -1. Core API classes and interfaces -2. Relevant examples -3. Documentation explaining key concepts -4. Files related to the specific task - -IMPORTANT: Return ONLY a JSON response wrapped in tags. - -{{ - "selected_files": [ - "crawl4ai/core_api.py", - "docs/getting_started.md", - "examples/basic_usage.py" - ], - "reasoning": "Brief explanation of why these files were selected" -}} -""" - - try: - response = self.call(file_selection_prompt, {}, temperature=0.3) - result = self._extract_json(response) - selected_files = result.get("selected_files", []) - - # Read the selected files - file_contents = [] - for file_path in selected_files: - if os.path.exists(file_path): - try: - with open(file_path, 'r', encoding='utf-8') as f: - content = f.read() - # Limit file size to prevent context overflow - if len(content) > 10000: - content = content[:10000] + "\n... (truncated)" - file_contents.append({ - "path": file_path, - "content": content - }) - except Exception as e: - console.print(f"[yellow]Warning: Could not read {file_path}: {e}[/yellow]") - - return file_contents - - except Exception as e: - console.print(f"[yellow]Warning: Could not select relevant files: {e}[/yellow]") - # Fallback: return some default important files - default_files = [ - "crawl4ai/__init__.py", - "crawl4ai/async_crawler.py", - "README.md" - ] - file_contents = [] - for file_path in default_files: - if os.path.exists(file_path): - try: - with open(file_path, 'r', encoding='utf-8') as f: - content = f.read()[:10000] - file_contents.append({ - "path": file_path, - "content": content - }) - except: - pass - return file_contents - - def _build_system_message(self, context: Dict) -> str: - """Build engineered system message with all context""" - sections = [] - - # Role and objective - sections.append("""You are a Release Engineering Assistant for Crawl4AI. -Your role is to help create high-quality releases with proper testing, documentation, and validation. -You work step-by-step, focusing on the current task while aware of the overall release context.""") - - # Add context sections with unique delimiters - if "codebase_info" in context: - sections.append(f""" -<> -{context['codebase_info']} -<>""") - - if "commit_diffs" in context: - sections.append(f""" -<> -{context['commit_diffs']} -<>""") - - if "previous_decisions" in context: - sections.append(f""" -<> -{context['previous_decisions']} -<>""") - - if "existing_patterns" in context: - sections.append(f""" -<> -{context['existing_patterns']} -<>""") - - if "constraints" in context: - sections.append(f""" -<> -{context['constraints']} -<>""") - - if "judge_feedback" in context: - sections.append(f""" -<> -{context['judge_feedback']} -<>""") - - return "\n".join(sections) - - def judge(self, - step_output: str, - expected_criteria: List[str], - context: Dict) -> JudgeResult: - """Judge the quality of a step's output""" - - judge_task = f"""Evaluate the following output against the criteria: - - -{step_output} - - - -{chr(10).join(f"- {c}" for c in expected_criteria)} - - -## Evaluation Required -1. Does the output meet ALL criteria? -2. Are there any issues or improvements needed? -3. Is human intervention required? - -IMPORTANT: Return ONLY a JSON response wrapped in tags. -Do NOT include any markdown code blocks, backticks, or explanatory text. -The JSON will be directly parsed, so any extra formatting will cause errors. - -Return your evaluation as: - -{{ - "status": "good" | "retry" | "human", - "feedback": "Clear explanation of the evaluation", - "specific_issues": ["specific issue 1", "specific issue 2"] -}} -""" - - response = self.call(judge_task, context, model=self.judge_model, temperature=0.3) - - try: - # Extract JSON between tags - import re - json_match = re.search(r'(.*?)', response, re.DOTALL) - if json_match: - json_str = json_match.group(1).strip() - result = json.loads(json_str) - return JudgeResult(**result) - else: - raise ValueError("No JSON found in response") - except Exception as e: - # Fallback if JSON parsing fails - console.print(f"[yellow]Judge parsing error: {e}[/yellow]") - return JudgeResult( - status="retry", - feedback="Failed to parse judge response", - specific_issues=["Invalid judge response format"] - ) - -class GitOperations: - """Handle git operations""" - - @staticmethod - def get_commits_between_branches(base: str = "main", head: str = "next") -> List[Dict]: - """Get commits in head that aren't in base""" - cmd = ["git", "log", f"{base}..{head}", "--pretty=format:%H|%an|%ae|%at|%s", "--reverse"] - result = subprocess.run(cmd, capture_output=True, text=True) - - commits = [] - for line in result.stdout.strip().split('\n'): - if line: - hash, author, email, timestamp, subject = line.split('|', 4) - commits.append({ - "hash": hash, - "author": author, - "email": email, - "date": datetime.fromtimestamp(int(timestamp)).isoformat(), - "subject": subject, - "selected": False - }) - return commits - - @staticmethod - def get_commit_diff(commit_hash: str) -> str: - """Get the diff for a specific commit""" - cmd = ["git", "show", commit_hash, "--pretty=format:", "--unified=3"] - result = subprocess.run(cmd, capture_output=True, text=True) - return result.stdout - - @staticmethod - def cherry_pick_commits(commits: List[str], branch: str) -> bool: - """Cherry pick commits to a branch""" - # Create and checkout branch - subprocess.run(["git", "checkout", "-b", branch], check=True) - - # Cherry pick each commit - for commit in commits: - result = subprocess.run(["git", "cherry-pick", commit]) - if result.returncode != 0: - console.print(f"[red]Failed to cherry-pick {commit}[/red]") - return False - return True - -class ReleaseAgent: - """Main release agent orchestrating the entire process""" - - def __init__(self, auto_mode: bool = False, select_all: bool = False, test_mode: bool = False): - self.state: States = "init" - self.context = SharedContext() - self.llm = LLMManager() - self.auto_mode = auto_mode - self.select_all = select_all - self.test_mode = test_mode - - # Load current version - self._load_current_version() - - def _load_current_version(self): - """Load current version from __version__.py""" - version_file = Path("crawl4ai/__version__.py") - if version_file.exists(): - content = version_file.read_text() - for line in content.split('\n'): - if '__version__' in line and '=' in line: - self.context.old_version = line.split('=')[1].strip().strip('"') - break - - def run(self): - """Run the release process""" - console.print("[bold cyan]🚀 Crawl4AI Release Agent[/bold cyan]\n") - - # State machine - while self.state != "complete": - try: - if self.state == "init": - self.state = "commit_selection" - elif self.state == "commit_selection": - self._select_commits() - self.state = "version_bump" - elif self.state == "version_bump": - self._bump_version() - self.state = "test_generation" - elif self.state == "test_generation": - self._generate_tests() - self.state = "test_execution" - elif self.state == "test_execution": - if self._run_tests(): - self.state = "branch_creation" - else: - console.print("[red]Tests failed! Fix issues and try again.[/red]") - break - elif self.state == "branch_creation": - self._create_version_branch() - self.state = "release_notes" - elif self.state == "release_notes": - self._generate_release_notes() - self.state = "demo_generation" - elif self.state == "demo_generation": - self._generate_demo() - self.state = "docs_update" - elif self.state == "docs_update": - self._update_docs() - self.state = "build_publish" - elif self.state == "build_publish": - if self._build_and_publish(): - self.state = "complete" - else: - break - - except KeyboardInterrupt: - console.print("\n[yellow]Release process interrupted by user[/yellow]") - break - except Exception as e: - console.print(f"[red]Error in state {self.state}: {e}[/red]") - break - - if self.state == "complete": - console.print("\n[green]✅ Release completed successfully![/green]") - - def _select_commits(self): - """Select commits to include in release""" - console.print("[bold]Step 1: Select Commits[/bold]") - - commits = GitOperations.get_commits_between_branches() - - if self.select_all: - # Auto-select all commits - for commit in commits: - commit["selected"] = True - self.context.selected_commits = commits - console.print(f"[green]Auto-selected all {len(commits)} commits[/green]") - else: - # Interactive selection - table = Table(title="Commits in 'next' not in 'main'") - table.add_column("", style="cyan", width=3) - table.add_column("Hash", style="yellow") - table.add_column("Author", style="green") - table.add_column("Date", style="blue") - table.add_column("Subject", style="white") - - for i, commit in enumerate(commits): - table.add_row( - str(i), - commit["hash"][:8], - commit["author"], - commit["date"][:10], - commit["subject"] - ) - - console.print(table) - - # Get selections - selections = Prompt.ask( - "Select commits (e.g., 0,2,3-5 or 'all')", - default="all" - ) - - if selections.lower() == "all": - for commit in commits: - commit["selected"] = True - else: - # Parse selection - for part in selections.split(','): - if '-' in part: - start, end = map(int, part.split('-')) - for i in range(start, end + 1): - if 0 <= i < len(commits): - commits[i]["selected"] = True - else: - i = int(part.strip()) - if 0 <= i < len(commits): - commits[i]["selected"] = True - - self.context.selected_commits = [c for c in commits if c["selected"]] - - # Collect diffs for selected commits - for commit in self.context.selected_commits: - diff = GitOperations.get_commit_diff(commit["hash"]) - # Store simplified diff info - self.context.files_changed.extend(self._extract_changed_files(diff)) - - console.print(f"[green]Selected {len(self.context.selected_commits)} commits[/green]") - - def _bump_version(self): - """Determine and confirm version bump""" - console.print("\n[bold]Step 2: Version Bump[/bold]") - - # Analyze commits to suggest version bump - commit_types = {"feat": 0, "fix": 0, "breaking": 0} - for commit in self.context.selected_commits: - subject = commit["subject"].lower() - if "breaking" in subject or "!" in subject: - commit_types["breaking"] += 1 - elif subject.startswith("feat"): - commit_types["feat"] += 1 - elif subject.startswith("fix"): - commit_types["fix"] += 1 - - # Suggest version - current_parts = self.context.old_version.split('.') - major, minor, patch = map(int, current_parts) - - if commit_types["breaking"] > 0: - suggested = f"{major + 1}.0.0" - elif commit_types["feat"] > 0: - suggested = f"{major}.{minor + 1}.0" - else: - suggested = f"{major}.{minor}.{patch + 1}" - - if self.auto_mode: - self.context.version = suggested - else: - self.context.version = Prompt.ask( - f"New version (current: {self.context.old_version})", - default=suggested - ) - - console.print(f"[green]Version: {self.context.old_version} → {self.context.version}[/green]") - self.context.branch_name = f"v{self.context.version}" - - def _generate_tests(self): - """Generate test script using LLM""" - console.print("\n[bold]Step 3: Generate Tests[/bold]") - - with Progress( - SpinnerColumn(), - TextColumn("[progress.description]{task.description}"), - console=console - ) as progress: - task = progress.add_task("Generating test script...", total=None) - - # Get relevant files for understanding Crawl4AI - query = f"Generate tests for Crawl4AI with these changes: {self._format_commits_for_llm()}" - relevant_files = self.llm.get_relevant_files(query, num_files=8) - - # Format file contents for context - codebase_context = [] - for file_info in relevant_files: - codebase_context.append(f"\n{file_info['content']}\n") - - # Build context for test generation - context = { - "commit_diffs": self._get_selected_diffs_summary(), - "existing_patterns": self._load_test_patterns(), - "constraints": "Generate comprehensive tests for all changed functionality", - "codebase_info": "\n\n".join(codebase_context) - } - - task_prompt = f"""Generate a Python test script for the following changes in Crawl4AI v{self.context.version}: - - -{self._format_commits_for_llm()} - - - -0. No mock data or uni test style, simple use them like a user will use. -1. Test all new features and changes -2. Be runnable with pytest -3. Return exit code 0 on success, non-zero on failure -4. Dont make it too ling, these are all already tested, this is final test after cherry-pick - - -IMPORTANT: Return ONLY a JSON response wrapped in tags. -Do NOT include any markdown code blocks, backticks, or explanatory text. -The JSON will be directly parsed, so any extra formatting will cause errors. - -Return the test script as: - -{{ - "test_script": "# Complete Python test script here\\nfrom crawl4ai..." -}} -""" - - response = self.llm.call(task_prompt, context) - - # Extract test script from JSON response - start_index = response.find("") - end_index = response.find("", start_index) - if start_index != -1 and end_index != -1: - json_str = response[start_index + len(""):end_index].strip() - result = json.loads(json_str) - self.context.test_script = result.get("test_script", "") - else: - console.print("[red]Failed to extract test script from response[/red]") - return - - # Judge the generated tests - judge_result = self.llm.judge( - self.context.test_script, - [ - "Tests cover all selected commits", - "Tests are comprehensive and meaningful", - "Test script is valid Python code", - "Tests check both success and failure cases" - ], - context - ) - - if judge_result.status == "retry": - console.print(f"[yellow]Regenerating tests: {judge_result.feedback}[/yellow]") - # Add feedback to context and retry - context["judge_feedback"] = judge_result.feedback - response = self.llm.call(task_prompt, context) - # Extract again - json_match = re.search(r'(.*?)', response, re.DOTALL) - if json_match: - json_str = json_match.group(1).strip() - result = json.loads(json_str) - self.context.test_script = result.get("test_script", "") - elif judge_result.status == "human": - console.print(f"[yellow]Human intervention needed: {judge_result.feedback}[/yellow]") - # TODO: Implement human feedback loop - - progress.update(task, completed=True) - - # Save test script - test_file = Path(f"test_release_{self.context.version}.py") - test_file.write_text(self.context.test_script) - console.print(f"[green]Test script saved to {test_file}[/green]") - - def _run_tests(self) -> bool: - """Run the generated tests""" - console.print("\n[bold]Step 4: Run Tests[/bold]") - - test_file = f"test_release_{self.context.version}.py" - - with Progress( - SpinnerColumn(), - TextColumn("[progress.description]{task.description}"), - console=console - ) as progress: - task = progress.add_task("Running tests...", total=None) - - result = subprocess.run( - ["python", test_file], - capture_output=True, - text=True - ) - - progress.update(task, completed=True) - - if result.returncode == 0: - console.print("[green]✅ All tests passed![/green]") - self.context.test_results = {"status": "passed", "output": result.stdout} - return True - else: - console.print("[red]❌ Tests failed![/red]") - console.print(result.stdout) - console.print(result.stderr) - self.context.test_results = { - "status": "failed", - "output": result.stdout, - "error": result.stderr - } - return False - - def _create_version_branch(self): - """Create version branch and cherry-pick commits""" - console.print(f"\n[bold]Step 5: Create Branch {self.context.branch_name}[/bold]") - - # Checkout main first - subprocess.run(["git", "checkout", "main"], check=True) - - # Create version branch - commit_hashes = [c["hash"] for c in self.context.selected_commits] - - if GitOperations.cherry_pick_commits(commit_hashes, self.context.branch_name): - console.print(f"[green]Created branch {self.context.branch_name} with {len(commit_hashes)} commits[/green]") - else: - raise Exception("Failed to create version branch") - - def _generate_release_notes(self): - """Generate release notes""" - console.print("\n[bold]Step 6: Generate Release Notes[/bold]") - - # Implementation continues... - # (Keeping it minimal as requested) - pass - - def _generate_demo(self): - """Generate demo script""" - console.print("\n[bold]Step 7: Generate Demo[/bold]") - pass - - def _update_docs(self): - """Update documentation""" - console.print("\n[bold]Step 8: Update Documentation[/bold]") - pass - - def _build_and_publish(self): - """Build and publish to PyPI""" - console.print("\n[bold]Step 9: Build and Publish[/bold]") - - if not self.auto_mode: - if not Confirm.ask("Ready to publish to PyPI?"): - return False - - # Run publish.sh - result = subprocess.run(["./publish.sh"], capture_output=True) - - if result.returncode == 0: - console.print(f"[green]✅ Published v{self.context.version} to PyPI![/green]") - - # Merge to main - subprocess.run(["git", "checkout", "main"], check=True) - subprocess.run(["git", "merge", "--squash", self.context.branch_name], check=True) - subprocess.run(["git", "commit", "-m", f"Release v{self.context.version}"], check=True) - - return True - else: - console.print("[red]Publishing failed![/red]") - return False - - # Helper methods - def _extract_changed_files(self, diff: str) -> List[str]: - """Extract changed file paths from diff""" - files = [] - for line in diff.split('\n'): - if line.startswith('+++') or line.startswith('---'): - file = line[4:].split('\t')[0] - if file != '/dev/null' and file not in files: - files.append(file) - return files - - def _get_selected_diffs_summary(self) -> str: - """Get summary of diffs for selected commits""" - # Simplified for brevity - return f"{len(self.context.selected_commits)} commits selected" - - def _load_test_patterns(self) -> str: - """Load existing test patterns""" - # Would load from existing test files - return "Follow pytest patterns" - - def _format_commits_for_llm(self) -> str: - """Format commits for LLM consumption""" - lines = [] - for commit in self.context.selected_commits: - lines.append(f"- {commit['hash'][:8]}: {commit['subject']}") - return '\n'.join(lines) - -@click.command() -@click.option('--all', is_flag=True, help='Select all commits automatically') -@click.option('-y', '--yes', is_flag=True, help='Auto-confirm version bump') -@click.option('--dry-run', is_flag=True, help='Run without publishing') -@click.option('--test', is_flag=True, help='Test mode - no git operations, no publishing') -def main(all, yes, dry_run, test): - """Crawl4AI Release Agent - Automated release management""" - agent = ReleaseAgent(auto_mode=yes, select_all=all, test_mode=test) - agent.run() - -if __name__ == "__main__": - main() \ No newline at end of file