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.
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
- Git status enhancement for unpushed tags (1 hour)
- Automated tag pushing (1 hour)
- CHANGELOG validation in release flow (2 hours)
- Version-tag consistency check (1 hour)
Phase 2: Medium Priority (UX & Automation) - 5.5 hours
- CHANGELOG section generation (3 hours)
- Explicit version command (30 minutes)
- Release summary auto-generation (2 hours)
Phase 3: Low Priority (Nice to Have) - 3 hours
- Schema auto-ingestion (1 hour)
- 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 statusshows 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 --pushcreates 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 validatechecks 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.0creates 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 versionworks - ✅ 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
- Unit tests for each new class/function
- Integration tests for CLI commands
- Manual testing with real scenarios
End-to-End Testing
- Test full release workflow: prepare → validate → tag → build → summary
- Test error cases (invalid CHANGELOG, missing tags, etc.)
- 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:
- Git status enhancement
- Automated tag pushing
- CHANGELOG validation
- 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