Add Git validations for publishing tools (#1381)

This commit prevents tools from being published if the underlying Git
repository is unsynced with origin.
This commit is contained in:
Vini Brasil
2024-10-02 11:46:18 -03:00
committed by GitHub
parent 01329a01ab
commit dd0b622826
9 changed files with 235 additions and 53 deletions

View File

@@ -2,12 +2,9 @@ from typing import Any, Dict, List, Optional
from rich.console import Console
from crewai.cli import git
from crewai.cli.command import BaseCommand, PlusAPIMixin
from crewai.cli.utils import (
fetch_and_json_env_file,
get_git_remote_url,
get_project_name,
)
from crewai.cli.utils import fetch_and_json_env_file, get_project_name
console = Console()
@@ -91,7 +88,11 @@ class DeployCommand(BaseCommand, PlusAPIMixin):
)
console.print("Creating deployment...", style="bold blue")
env_vars = fetch_and_json_env_file()
remote_repo_url = get_git_remote_url()
try:
remote_repo_url = git.Repository().origin_url()
except ValueError:
remote_repo_url = None
if remote_repo_url is None:
console.print("No remote repository URL found.", style="bold red")

80
src/crewai/cli/git.py Normal file
View File

@@ -0,0 +1,80 @@
import subprocess
class Repository:
def __init__(self, path="."):
self.path = path
if not self.is_git_installed():
raise ValueError("Git is not installed or not found in your PATH.")
if not self.is_git_repo():
raise ValueError(f"{self.path} is not a Git repository.")
self.fetch()
def is_git_installed(self) -> bool:
"""Check if Git is installed and available in the system."""
try:
subprocess.run(
["git", "--version"], capture_output=True, check=True, text=True
)
return True
except (subprocess.CalledProcessError, FileNotFoundError):
return False
def fetch(self) -> None:
"""Fetch latest updates from the remote."""
subprocess.run(["git", "fetch"], cwd=self.path, check=True)
def status(self) -> str:
"""Get the git status in porcelain format."""
return subprocess.check_output(
["git", "status", "--branch", "--porcelain"],
cwd=self.path,
encoding="utf-8",
).strip()
def is_git_repo(self) -> bool:
"""Check if the current directory is a git repository."""
try:
subprocess.check_output(
["git", "rev-parse", "--is-inside-work-tree"],
cwd=self.path,
encoding="utf-8",
)
return True
except subprocess.CalledProcessError:
return False
def has_uncommitted_changes(self) -> bool:
"""Check if the repository has uncommitted changes."""
return len(self.status().splitlines()) > 1
def is_ahead_or_behind(self) -> bool:
"""Check if the repository is ahead or behind the remote."""
for line in self.status().splitlines():
if line.startswith("##") and ("ahead" in line or "behind" in line):
return True
return False
def is_synced(self) -> bool:
"""Return True if the Git repository is fully synced with the remote, False otherwise."""
if self.has_uncommitted_changes() or self.is_ahead_or_behind():
return False
else:
return True
def origin_url(self) -> str | None:
"""Get the Git repository's remote URL."""
try:
result = subprocess.run(
["git", "remote", "get-url", "origin"],
cwd=self.path,
capture_output=True,
text=True,
check=True,
)
return result.stdout.strip()
except subprocess.CalledProcessError:
return None

View File

@@ -6,6 +6,7 @@ import subprocess
import tempfile
from crewai.cli.command import BaseCommand, PlusAPIMixin
from crewai.cli import git
from crewai.cli.utils import (
get_project_name,
get_project_description,
@@ -59,6 +60,17 @@ class ToolCommand(BaseCommand, PlusAPIMixin):
os.chdir(old_directory)
def publish(self, is_public: bool):
if not git.Repository().is_synced():
console.print(
"[bold red]Failed to publish tool.[/bold red]\n"
"Local changes need to be resolved before publishing. Please do the following:\n"
"* [bold]Commit[/bold] your changes.\n"
"* [bold]Push[/bold] to sync with the remote.\n"
"* [bold]Pull[/bold] the latest changes from the remote.\n"
"\nOnce your repository is up-to-date, retry publishing the tool."
)
raise SystemExit()
project_name = get_project_name(require=True)
assert isinstance(project_name, str)

View File

@@ -1,8 +1,6 @@
import os
import shutil
import click
import re
import subprocess
import sys
import importlib.metadata
@@ -61,38 +59,6 @@ def parse_toml(content):
return simple_toml_parser(content)
def get_git_remote_url() -> str | None:
"""Get the Git repository's remote URL."""
try:
# Run the git remote -v command
result = subprocess.run(
["git", "remote", "-v"], capture_output=True, text=True, check=True
)
# Get the output
output = result.stdout
# Parse the output to find the origin URL
matches = re.findall(r"origin\s+(.*?)\s+\(fetch\)", output)
if matches:
return matches[0] # Return the first match (origin URL)
else:
console.print("No origin remote found.", style="bold red")
except subprocess.CalledProcessError as e:
console.print(
f"Error running trying to fetch the Git Repository: {e}", style="bold red"
)
except FileNotFoundError:
console.print(
"Git command not found. Make sure Git is installed and in your PATH.",
style="bold red",
)
return None
def get_project_name(
pyproject_path: str = "pyproject.toml", require: bool = False
) -> str | None: