Skip to main content
Glama

MockLoop MCP Server

Official
by MockLoop
prepare_release.pyโ€ข14.6 kB
#!/usr/bin/env python3 """ Release preparation script for mockloop-mcp. This script provides an interactive guide for preparing releases, validating the project state, and ensuring all requirements are met before creating a release. Usage: python scripts/prepare_release.py python scripts/prepare_release.py --version 1.0.0 python scripts/prepare_release.py --check-only """ import argparse from pathlib import Path import re import subprocess import sys class ReleasePreparation: """Handles release preparation and validation.""" def __init__(self, project_root: Path): self.project_root = project_root self.pyproject_path = project_root / "pyproject.toml" self.init_path = project_root / "src" / "mockloop_mcp" / "__init__.py" self.changelog_path = project_root / "CHANGELOG.md" self.checks_passed = [] self.checks_failed = [] def print_header(self, title: str) -> None: """Print a formatted header.""" def print_check(self, name: str, passed: bool, details: str = "") -> None: """Print a check result.""" if details: pass if passed: self.checks_passed.append(name) else: self.checks_failed.append(name) def get_current_version(self) -> str: """Get the current version from pyproject.toml.""" if not self.pyproject_path.exists(): raise FileNotFoundError( f"pyproject.toml not found at {self.pyproject_path}" ) content = self.pyproject_path.read_text() match = re.search(r'version\s*=\s*"([^"]+)"', content) if not match: raise ValueError("Version not found in pyproject.toml") return match.group(1) def check_git_status(self) -> bool: """Check if git working directory is clean.""" try: result = subprocess.run( ["git", "status", "--porcelain"], capture_output=True, text=True, check=True, ) is_clean = len(result.stdout.strip()) == 0 if not is_clean: details = "Working directory has uncommitted changes" else: details = "Working directory is clean" self.print_check("Git working directory clean", is_clean, details) return is_clean except (subprocess.CalledProcessError, FileNotFoundError): self.print_check( "Git working directory clean", False, "Git not available or not a git repository", ) return False def check_version_consistency(self) -> bool: """Check if versions are consistent across files.""" try: pyproject_version = self.get_current_version() # Check __init__.py if self.init_path.exists(): init_content = self.init_path.read_text() init_match = re.search(r'__version__\s*=\s*"([^"]+)"', init_content) if init_match: init_version = init_match.group(1) is_consistent = pyproject_version == init_version details = f"pyproject.toml: {pyproject_version}, __init__.py: {init_version}" else: is_consistent = False details = "__version__ not found in __init__.py" else: is_consistent = False details = "__init__.py not found" self.print_check("Version consistency", is_consistent, details) return is_consistent except Exception as e: self.print_check("Version consistency", False, str(e)) return False def check_changelog_updated(self) -> bool: """Check if changelog has been updated for the current version.""" try: if not self.changelog_path.exists(): self.print_check("Changelog updated", False, "CHANGELOG.md not found") return False content = self.changelog_path.read_text() current_version = self.get_current_version() # Check if current version is in changelog version_pattern = rf"\[{re.escape(current_version)}\]" has_version = bool(re.search(version_pattern, content)) # Check if there's content in Unreleased section unreleased_match = re.search( r"## \[Unreleased\](.*?)(?=## \[|\Z)", content, re.DOTALL ) has_unreleased_content = False if unreleased_match: unreleased_content = unreleased_match.group(1).strip() # Remove section headers and check if there's actual content content_lines = [ line.strip() for line in unreleased_content.split("\n") if line.strip() and not line.strip().startswith("###") ] has_unreleased_content = len(content_lines) > 0 if has_version: details = f"Version {current_version} found in changelog" result = True elif has_unreleased_content: details = "Unreleased section has content ready for release" result = True else: details = "No version entry or unreleased content found" result = False self.print_check("Changelog updated", result, details) return result except Exception as e: self.print_check("Changelog updated", False, str(e)) return False def check_tests_pass(self) -> bool: """Check if all tests pass.""" try: result = subprocess.run( ["python", "-m", "pytest", "tests/", "-v", "--tb=short"], capture_output=True, text=True, cwd=self.project_root, check=False, ) tests_pass = result.returncode == 0 if tests_pass: # Count tests from output output_lines = result.stdout.split("\n") test_summary = [ line for line in output_lines if "passed" in line and ("failed" in line or "error" in line or "skipped" in line) ] details = test_summary[-1] if test_summary else "All tests passed" else: details = "Some tests failed - check output above" self.print_check("All tests pass", tests_pass, details) return tests_pass except FileNotFoundError: self.print_check( "All tests pass", False, "pytest not found - install dev dependencies" ) return False except Exception as e: self.print_check("All tests pass", False, str(e)) return False def check_security_scans(self) -> bool: """Run basic security checks.""" try: # Run bandit bandit_result = subprocess.run( ["bandit", "-r", "src/", "-f", "txt"], capture_output=True, text=True, cwd=self.project_root, check=False, ) # Bandit returns 1 if issues found, 0 if clean bandit_clean = bandit_result.returncode == 0 if bandit_clean: details = "No security issues found" else: # Count issues issues = bandit_result.stdout.count(">> Issue:") details = f"{issues} potential security issues found" self.print_check("Security scan (Bandit)", bandit_clean, details) return bandit_clean except FileNotFoundError: self.print_check( "Security scan (Bandit)", False, "bandit not found - install dev dependencies", ) return False except Exception as e: self.print_check("Security scan (Bandit)", False, str(e)) return False def check_dependencies_secure(self) -> bool: """Check for known vulnerabilities in dependencies.""" try: # Run safety check safety_result = subprocess.run( ["safety", "check"], capture_output=True, text=True, cwd=self.project_root, check=False, ) safety_clean = safety_result.returncode == 0 if safety_clean: details = "No known vulnerabilities found" else: details = "Known vulnerabilities detected in dependencies" self.print_check("Dependency security (Safety)", safety_clean, details) return safety_clean except FileNotFoundError: self.print_check( "Dependency security (Safety)", False, "safety not found - install dev dependencies", ) return False except Exception as e: self.print_check("Dependency security (Safety)", False, str(e)) return False def check_build_works(self) -> bool: """Check if the package builds successfully.""" try: # Clean any existing build artifacts build_dir = self.project_root / "build" dist_dir = self.project_root / "dist" if build_dir.exists(): subprocess.run(["rm", "-rf", str(build_dir)], check=True) if dist_dir.exists(): subprocess.run(["rm", "-rf", str(dist_dir)], check=True) # Build the package result = subprocess.run( ["python", "-m", "build"], capture_output=True, text=True, cwd=self.project_root, check=False, ) build_success = result.returncode == 0 if build_success: # Check if files were created dist_files = list(dist_dir.glob("*")) if dist_dir.exists() else [] details = f"Built {len(dist_files)} distribution files" else: details = "Build failed - check build dependencies" self.print_check("Package builds successfully", build_success, details) return build_success except FileNotFoundError: self.print_check( "Package builds successfully", False, "build module not found - install build dependencies", ) return False except Exception as e: self.print_check("Package builds successfully", False, str(e)) return False def generate_release_notes(self, version: str) -> str: """Generate release notes from changelog.""" if not self.changelog_path.exists(): return f"Release notes for version {version}" content = self.changelog_path.read_text() # Try to find the version section version_pattern = ( rf"## \[{re.escape(version)}\].*?\n(.*?)(?=\n## \[|\n\[.*?\]:|\Z)" ) match = re.search(version_pattern, content, re.DOTALL) if match: notes = match.group(1).strip() # Remove any trailing links section notes = re.sub(r"\n\[.*?\]:.*$", "", notes, flags=re.MULTILINE) return notes # If version not found, try unreleased section unreleased_match = re.search( r"## \[Unreleased\](.*?)(?=## \[|\Z)", content, re.DOTALL ) if unreleased_match: notes = unreleased_match.group(1).strip() return notes return f"Release notes for version {version}" def run_all_checks(self) -> bool: """Run all release preparation checks.""" self.print_header("Release Preparation Checks") # Reset check counters self.checks_passed = [] self.checks_failed = [] # Run all checks checks = [ self.check_git_status, self.check_version_consistency, self.check_changelog_updated, self.check_tests_pass, self.check_security_scans, self.check_dependencies_secure, self.check_build_works, ] for check in checks: check() # Summary if self.checks_failed: for _check in self.checks_failed: pass if self.checks_passed: for _check in self.checks_passed: pass return len(self.checks_failed) == 0 def interactive_release_preparation( self, target_version: str | None = None ) -> None: """Interactive release preparation workflow.""" self.print_header("MockLoop MCP Release Preparation") self.get_current_version() if target_version: pass else: pass # Run checks all_passed = self.run_all_checks() if not all_passed: return # Show next steps self.print_header("Next Steps") # Show release notes preview if target_version: notes = self.generate_release_notes(target_version) if notes: self.print_header("Release Notes Preview") def main(): """Main entry point for the release preparation script.""" parser = argparse.ArgumentParser(description="Prepare release for mockloop-mcp") parser.add_argument("--version", help="Target version for release") parser.add_argument( "--check-only", action="store_true", help="Only run checks, don't show interactive guide", ) args = parser.parse_args() # Find project root script_path = Path(__file__).resolve() project_root = script_path.parent.parent prep = ReleasePreparation(project_root) try: if args.check_only: # Just run checks and exit all_passed = prep.run_all_checks() sys.exit(0 if all_passed else 1) else: # Interactive mode prep.interactive_release_preparation(args.version) except Exception: sys.exit(1) if __name__ == "__main__": main()

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/MockLoop/mockloop-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server