diff --git a/capabilities/release-management/src/release_management/utils/validation.py b/capabilities/release-management/src/release_management/utils/validation.py index 077d8236..c1944c64 100644 --- a/capabilities/release-management/src/release_management/utils/validation.py +++ b/capabilities/release-management/src/release_management/utils/validation.py @@ -4,6 +4,7 @@ Release validation utilities. This module provides validation functions for release readiness. """ +import subprocess from pathlib import Path from typing import List, Tuple, Optional @@ -48,6 +49,10 @@ class ReleaseValidator: config_issues = self._validate_configuration() issues.extend(config_issues) + # CHANGELOG validation + changelog_issues = self._validate_changelog() + issues.extend(changelog_issues) + return len(issues) == 0, issues def _validate_git_state(self) -> List[str]: @@ -186,6 +191,117 @@ class ReleaseValidator: return len(issues) == 0, issues + def _validate_changelog(self) -> List[str]: + """Validate CHANGELOG.md using changelog schema. + + Returns: + List of CHANGELOG-related issues + """ + issues = [] + changelog_path = self.project_root / 'CHANGELOG.md' + + # Check if CHANGELOG exists + if not changelog_path.exists(): + issues.append("Missing CHANGELOG.md file") + return issues + + # Check if changelog schema exists + schema_path = self.project_root / 'markitect' / 'schemas' / 'changelog-schema-v1.0.md' + if not schema_path.exists(): + # Schema doesn't exist, skip validation + return issues + + # Validate CHANGELOG with schema using markitect validate command + try: + result = subprocess.run( + [ + 'markitect', 'validate', str(changelog_path), + '--schema', str(schema_path), + '--semantic' + ], + capture_output=True, + text=True, + cwd=self.project_root + ) + + if result.returncode != 0: + issues.append("CHANGELOG.md validation failed against schema") + # Parse output for specific errors + if 'Unreleased section' in result.stdout: + issues.append(" - Missing [Unreleased] section in CHANGELOG") + if 'version format' in result.stdout.lower(): + issues.append(" - Invalid version format in CHANGELOG") + + except FileNotFoundError: + # markitect command not available + issues.append("Cannot validate CHANGELOG (markitect command not found)") + except Exception as e: + issues.append(f"Error validating CHANGELOG: {e}") + + return issues + + def validate_changelog_version(self, version: str) -> Tuple[bool, List[str]]: + """Validate that CHANGELOG has section for specified version. + + Args: + version: Version to check (e.g., "0.10.0") + + Returns: + Tuple of (is_valid, list_of_issues) + """ + issues = [] + changelog_path = self.project_root / 'CHANGELOG.md' + + if not changelog_path.exists(): + issues.append("CHANGELOG.md not found") + return False, issues + + try: + content = changelog_path.read_text() + + # Check for version section + version_header = f"## [{version}]" + if version_header not in content: + issues.append(f"CHANGELOG missing section for version {version}") + + # Check for Unreleased section + if "## [Unreleased]" not in content: + issues.append("CHANGELOG missing [Unreleased] section") + + # Check if version section has a date + import re + date_pattern = rf"## \[{re.escape(version)}\] - \d{{4}}-\d{{2}}-\d{{2}}" + if not re.search(date_pattern, content): + issues.append(f"Version {version} section missing date or has invalid date format") + + except Exception as e: + issues.append(f"Error reading CHANGELOG: {e}") + + return len(issues) == 0, issues + + def check_version_tag_consistency(self, version: str) -> Tuple[bool, List[str]]: + """Check consistency between CHANGELOG version and git tags. + + Args: + version: Version to check (e.g., "0.10.0") + + Returns: + Tuple of (is_consistent, list_of_issues) + """ + issues = [] + + # Check CHANGELOG has the version + changelog_valid, changelog_issues = self.validate_changelog_version(version) + if not changelog_valid: + issues.extend(changelog_issues) + + # Check git tag exists + tag_name = version if version.startswith('v') else f'v{version}' + if not self.git_manager.tag_exists(tag_name): + issues.append(f"Git tag {tag_name} doesn't exist for version in CHANGELOG") + + return len(issues) == 0, issues + def get_validation_summary(self) -> dict: """Get a comprehensive validation summary. @@ -224,6 +340,10 @@ class ReleaseValidator: if any('authentication' in issue.lower() for issue in issues): recommendations.append("Set up authentication tokens for package publishing") + if any('CHANGELOG' in issue for issue in issues): + recommendations.append("Fix CHANGELOG.md format and ensure [Unreleased] section exists") + recommendations.append("Validate with: markitect validate CHANGELOG.md --schema changelog-schema-v1.0.md --semantic") + if not issues: recommendations.append("Repository is ready for release!")