# Release Management Optimization Implementation Plan **Date**: 2026-01-06 **Status**: Ready to implement **Total Optimizations**: 9 --- ## Implementation Order ### Phase 1: High Priority (Critical Issues) - 5 hours 1. Git status enhancement for unpushed tags (1 hour) 2. Automated tag pushing (1 hour) 3. CHANGELOG validation in release flow (2 hours) 4. Version-tag consistency check (1 hour) ### Phase 2: Medium Priority (UX & Automation) - 5.5 hours 5. CHANGELOG section generation (3 hours) 6. Explicit version command (30 minutes) 7. Release summary auto-generation (2 hours) ### Phase 3: Low Priority (Nice to Have) - 3 hours 8. Schema auto-ingestion (1 hour) 9. Release notes from CHANGELOG (2 hours) **Total Estimated Time**: 13.5 hours --- ## Optimization #1: Git Status Enhancement for Unpushed Tags **Priority**: HIGH **Estimated**: 1 hour **Files**: `capabilities/release-management/src/release_management/core/status.py` ### Implementation Approach **Option 1**: Enhance `release status` command (RECOMMENDED) - Add unpushed tag detection to ReleaseStatus class - Compare local tags with remote tags - Display unpushed tags prominently **Option 2**: Git post-commit hook - Create .git/hooks/post-commit script - Automatic check after each commit - Less portable (per-clone setup) **Option 3**: Git alias - Add custom git alias in .gitconfig - User needs to remember to use it ### Implementation Details ```python # In ReleaseStatus class def get_unpushed_tags(self) -> List[str]: """Get list of tags not pushed to origin.""" # Get local tags local_tags = subprocess.run( ['git', 'tag', '-l'], capture_output=True, text=True ).stdout.strip().split('\n') # Get remote tags remote_tags = subprocess.run( ['git', 'ls-remote', '--tags', 'origin'], capture_output=True, text=True ).stdout remote_tag_names = [ line.split('refs/tags/')[1] for line in remote_tags.split('\n') if 'refs/tags/' in line ] return [tag for tag in local_tags if tag and tag not in remote_tag_names] ``` ### Success Criteria - ✅ `release status` shows unpushed tags - ✅ Clear warning when tags haven't been pushed - ✅ Works with multiple remotes --- ## Optimization #2: Automated Tag Pushing **Priority**: HIGH **Estimated**: 1 hour **Files**: `capabilities/release-management/src/release_management/cli/main.py` ### Implementation Add `--push` flag to `release tag` command: ```python @click.command() @click.argument('version') @click.option('--push/--no-push', default=False, help='Automatically push tag to origin after creating') @click.option('--message', '-m', help='Tag annotation message') def tag(version, push, message): """Create git tag for version.""" # Existing tag creation logic if push: click.echo(f"Pushing tag {tag_name} to origin...") git_manager.push_tag(tag_name) click.echo("✅ Tag pushed successfully") ``` ### Success Criteria - ✅ `release tag v0.11.0 --push` creates AND pushes tag - ✅ Works with existing tag logic - ✅ Error handling for push failures --- ## Optimization #3: CHANGELOG Validation in Release Flow **Priority**: HIGH **Estimated**: 2 hours **Files**: - `capabilities/release-management/src/release_management/validators/changelog_validator.py` (new) - `capabilities/release-management/src/release_management/cli/main.py` ### Implementation Create ChangelogValidator class: ```python class ChangelogValidator: """Validates CHANGELOG.md using changelog schema.""" def __init__(self, changelog_path: Path = Path("CHANGELOG.md")): self.changelog_path = changelog_path self.schema_path = Path("markitect/schemas/changelog-schema-v1.0.md") def validate(self) -> ValidationResult: """Validate CHANGELOG with schema.""" # Use markitect validate command result = subprocess.run([ 'markitect', 'validate', str(self.changelog_path), '--schema', str(self.schema_path), '--semantic' ], capture_output=True, text=True) return ValidationResult.from_output(result.stdout, result.returncode) def check_version_exists(self, version: str) -> bool: """Check if version section exists in CHANGELOG.""" with open(self.changelog_path) as f: content = f.read() return f"## [{version}]" in content ``` Integrate into `release validate` command: ```python @click.command() def validate(): """Validate repository state for release readiness.""" # Existing validations... # Add CHANGELOG validation changelog_validator = ChangelogValidator() result = changelog_validator.validate() if not result.is_valid: click.echo("❌ CHANGELOG validation failed:") for error in result.errors: click.echo(f" - {error}") sys.exit(1) click.echo("✅ CHANGELOG is valid") ``` ### Success Criteria - ✅ `release validate` checks CHANGELOG.md - ✅ Validates using changelog-schema-v1.0.md - ✅ Reports errors clearly - ✅ Prevents release if invalid --- ## Optimization #4: Version-Tag Consistency Check **Priority**: HIGH **Estimated**: 1 hour **Files**: `capabilities/release-management/src/release_management/validators/changelog_validator.py` ### Implementation Add to ChangelogValidator: ```python def check_version_tag_consistency(self, target_version: str) -> ConsistencyResult: """Check CHANGELOG version matches git describe.""" # Check CHANGELOG has section if not self.check_version_exists(target_version): return ConsistencyResult( is_consistent=False, message=f"CHANGELOG missing section for {target_version}" ) # Check git tag exists tags = subprocess.run( ['git', 'tag', '-l', f'v{target_version}'], capture_output=True, text=True ).stdout.strip() if not tags: return ConsistencyResult( is_consistent=False, message=f"Git tag v{target_version} doesn't exist" ) # Check Unreleased section exists with open(self.changelog_path) as f: if "## [Unreleased]" not in f.read(): return ConsistencyResult( is_consistent=False, message="CHANGELOG missing [Unreleased] section" ) return ConsistencyResult(is_consistent=True) ``` ### Success Criteria - ✅ Detects CHANGELOG/tag mismatches - ✅ Ensures Unreleased section exists - ✅ Integrated into `release validate` --- ## Optimization #5: CHANGELOG Section Generation **Priority**: MEDIUM **Estimated**: 3 hours **Files**: - `capabilities/release-management/src/release_management/changelog/editor.py` (new) - `capabilities/release-management/src/release_management/cli/main.py` ### Implementation Create ChangelogEditor class: ```python class ChangelogEditor: """Edit CHANGELOG.md programmatically.""" def create_version_section(self, version: str, date: str = None): """Create new version section and move Unreleased content.""" if date is None: date = datetime.now().strftime("%Y-%m-%d") with open(self.changelog_path) as f: lines = f.readlines() # Find Unreleased section unreleased_idx = None for i, line in enumerate(lines): if line.strip() == "## [Unreleased]": unreleased_idx = i break if unreleased_idx is None: raise ValueError("No [Unreleased] section found") # Find next version section or end next_section_idx = None for i in range(unreleased_idx + 1, len(lines)): if lines[i].startswith("## ["): next_section_idx = i break # Extract Unreleased content if next_section_idx: unreleased_content = lines[unreleased_idx+1:next_section_idx] else: unreleased_content = lines[unreleased_idx+1:] # Create new version section new_section = [ f"## [{version}] - {date}\n", "\n" ] + unreleased_content + ["\n"] # Insert after Unreleased new_lines = ( lines[:unreleased_idx+2] + # Keep Unreleased header + blank line new_section + (lines[next_section_idx:] if next_section_idx else []) ) # Write back with open(self.changelog_path, 'w') as f: f.writelines(new_lines) ``` Add `release prepare` command: ```python @click.command() @click.argument('version') @click.option('--date', default=None, help='Release date (YYYY-MM-DD)') def prepare(version, date): """Prepare CHANGELOG for new version release.""" editor = ChangelogEditor() editor.create_version_section(version, date) # Validate result validator = ChangelogValidator() result = validator.validate() if result.is_valid: click.echo(f"✅ Created [{version}] section in CHANGELOG.md") else: click.echo("⚠️ CHANGELOG validation failed after edit") ``` ### Success Criteria - ✅ `release prepare v0.11.0` creates section - ✅ Moves Unreleased content to new section - ✅ Validates result - ✅ Preserves formatting --- ## Optimization #6: Explicit Version Command **Priority**: MEDIUM **Estimated**: 30 minutes **Files**: `markitect/cli.py` ### Implementation Add version subcommand to markitect CLI: ```python @cli.command() def version(): """Show detailed version information.""" from markitect.__version__ import get_version_info info = get_version_info() click.echo(f"MarkiTect version: {info['version']}") click.echo(f"Latest git tag: {info.get('latest_tag', 'N/A')}") click.echo(f"Commits since tag: {info.get('commits_since_tag', 'N/A')}") click.echo(f"Working tree: {'clean' if info.get('clean', False) else 'dirty'}") click.echo(f"Current commit: {info.get('commit_hash', 'N/A')}") ``` ### Success Criteria - ✅ `markitect version` works - ✅ Shows more detail than `--version` - ✅ Backwards compatible with `--version` --- ## Optimization #7: Release Summary Auto-Generation **Priority**: MEDIUM **Estimated**: 2 hours **Files**: - `capabilities/release-management/src/release_management/summary/generator.py` (new) - `capabilities/release-management/src/release_management/cli/main.py` ### Implementation Create SummaryGenerator: ```python class SummaryGenerator: """Generate release summary from CHANGELOG and git metadata.""" def generate(self, version: str) -> str: """Generate RELEASE_SUMMARY.md content.""" # Extract CHANGELOG section changelog_section = self.extract_changelog_section(version) # Get git statistics stats = self.get_git_statistics(version) # Build summary template = f"""# MarkiTect {version} Release Summary **Release Date**: {stats['release_date']} **Tag**: v{version} ## Changes {changelog_section} ## Git Statistics - **Commits**: {stats['commit_count']} - **Files Changed**: {stats['files_changed']} - **Insertions**: +{stats['insertions']} - **Deletions**: -{stats['deletions']} ## Build Artifacts {self.list_build_artifacts()} ## Validation {self.get_validation_results()} """ return template ``` Add `release summary` command: ```python @click.command() @click.argument('version') @click.option('--output', '-o', default='RELEASE_SUMMARY.md', help='Output file path') def summary(version, output): """Generate release summary document.""" generator = SummaryGenerator() content = generator.generate(version) Path(output).write_text(content) click.echo(f"✅ Generated {output}") ``` ### Success Criteria - ✅ Extracts CHANGELOG section - ✅ Includes git statistics - ✅ Lists build artifacts - ✅ Saves to file --- ## Optimization #8: Schema Auto-Ingestion **Priority**: LOW **Estimated**: 1 hour **Files**: `markitect/schema_loader.py` ### Implementation Add auto-ingestion on build/install: ```python def auto_ingest_schemas(): """Automatically ingest schemas from markitect/schemas/.""" schema_dir = Path(__file__).parent / "schemas" for schema_file in schema_dir.glob("*-schema-v*.md"): # Check if already ingested if not is_schema_ingested(schema_file): ingest_schema(schema_file) ``` Call from setup.py or as post-install hook. ### Success Criteria - ✅ New schemas auto-ingested on install - ✅ Doesn't re-ingest existing schemas - ✅ Works in development mode --- ## Optimization #9: Release Notes from CHANGELOG **Priority**: LOW **Estimated**: 2 hours **Files**: `capabilities/release-management/src/release_management/changelog/parser.py` (new) ### Implementation Create ChangelogParser: ```python class ChangelogParser: """Parse CHANGELOG.md and extract sections.""" def extract_version_section(self, version: str) -> str: """Extract content for specific version.""" # Parse CHANGELOG # Find version section # Extract content until next version # Return formatted for release notes ``` Add `release notes` command: ```python @click.command() @click.argument('version') @click.option('--format', type=click.Choice(['markdown', 'plain', 'html']), default='markdown') def notes(version, format): """Extract release notes from CHANGELOG.""" parser = ChangelogParser() content = parser.extract_version_section(version) if format == 'html': # Convert to HTML pass click.echo(content) ``` ### Success Criteria - ✅ Extracts version section - ✅ Multiple output formats - ✅ Can pipe to gh release or gitea --- ## Testing Strategy ### Per-Optimization Testing 1. Unit tests for each new class/function 2. Integration tests for CLI commands 3. Manual testing with real scenarios ### End-to-End Testing 1. Test full release workflow: prepare → validate → tag → build → summary 2. Test error cases (invalid CHANGELOG, missing tags, etc.) 3. Test with v0.11.0 as real-world scenario ### Regression Testing - Ensure existing release commands still work - Backward compatibility with current workflows - No breaking changes to public APIs --- ## Rollout Plan ### Phase 1: Foundation (Day 1, 5 hours) Implement high-priority items that prevent errors: 1. Git status enhancement 2. Automated tag pushing 3. CHANGELOG validation 4. Version-tag consistency **Deliverable**: Robust validation preventing v0.10.0-style issues ### Phase 2: Automation (Day 2, 5.5 hours) Implement medium-priority UX improvements: 5. CHANGELOG section generation 6. Explicit version command 7. Release summary auto-generation **Deliverable**: Streamlined release workflow ### Phase 3: Polish (Day 3, 3 hours) Implement low-priority nice-to-haves: 8. Schema auto-ingestion 9. Release notes extraction **Deliverable**: Complete automated release toolchain --- ## Success Metrics ### Before Optimizations (v0.10.0) - Manual steps: 8 - Errors: 2 (forgotten tags, version detection) - Time: ~3 hours - Documentation: Manual ### After Optimizations (Target) - Manual steps: 2-3 (review, approve) - Errors: 0 (automated validation) - Time: ~1.5 hours (50% reduction) - Documentation: Auto-generated ### Quality Improvements - ✅ No forgotten tag pushes (status + auto-push) - ✅ CHANGELOG always valid (schema validation) - ✅ Version consistency guaranteed (automated checks) - ✅ Consistent documentation (auto-generation) --- **Plan Created**: 2026-01-06 **Estimated Total Time**: 13.5 hours (3 days @ 4-5 hours/day) **Next Step**: Begin Phase 1 implementation