Files
markitect-main/roadmap/260106-release-management-optimization/IMPLEMENTATION_PLAN.md
tegwick 587d2f5889 feat: implement optimization #1 - unpushed tags detection
Added unpushed tag detection to release status command to prevent
forgotten tag pushes (the critical issue from v0.10.0 release).

**Implementation**:
- Added `get_unpushed_tags()` method to GitManager
- Compares local tags with remote tags (git ls-remote)
- Handles annotated tags correctly (strips ^{} suffix)
- Added unpushed_tags to repository status dict

**CLI Enhancement**:
- `release status` now shows unpushed tags with warning emoji
- Lists all unpushed tags
- Provides helpful command to push them

**Output Example**:
```
⚠️  Unpushed Tags: 2 tag(s) not pushed to origin
    - v0.9.0
    - v0.10.0

💡 Push tags with: git push origin v0.9.0 v0.10.0
   Or push all tags: git push --tags
```

**Testing**: Verified with current repo (no unpushed tags after push)

**Files Modified**:
- capabilities/release-management/src/release_management/git/manager.py
- capabilities/release-management/src/release_management/cli/main.py

**Documentation**: Added comprehensive IMPLEMENTATION_PLAN.md with
all 9 optimizations detailed (13.5 hours total estimated)

This solves the #1 critical issue from OPTIMIZATION_ASSESSMENT.md.
2026-01-06 17:26:09 +01:00

15 KiB

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

  1. CHANGELOG section generation (3 hours)
  2. Explicit version command (30 minutes)
  3. Release summary auto-generation (2 hours)

Phase 3: Low Priority (Nice to Have) - 3 hours

  1. Schema auto-ingestion (1 hour)
  2. 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

# 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:

@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:

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:

@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:

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:

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:

@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:

@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:

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:

@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:

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:

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:

@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