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.
This commit is contained in:
@@ -55,6 +55,15 @@ def status(ctx):
|
|||||||
print(f"Latest Commit: {status_info['latest_commit']}")
|
print(f"Latest Commit: {status_info['latest_commit']}")
|
||||||
print(f"Latest Tag: {status_info['latest_tag'] or 'None'}")
|
print(f"Latest Tag: {status_info['latest_tag'] or 'None'}")
|
||||||
print(f"Uncommitted Changes: {'Yes' if status_info['has_changes'] else 'No'}")
|
print(f"Uncommitted Changes: {'Yes' if status_info['has_changes'] else 'No'}")
|
||||||
|
|
||||||
|
# Show unpushed tags
|
||||||
|
unpushed_tags = status_info.get('unpushed_tags', [])
|
||||||
|
if unpushed_tags:
|
||||||
|
print(f"\n⚠️ Unpushed Tags: {len(unpushed_tags)} tag(s) not pushed to origin")
|
||||||
|
for tag in unpushed_tags:
|
||||||
|
print(f" - {tag}")
|
||||||
|
print(f"\n💡 Push tags with: git push origin {' '.join(unpushed_tags)}")
|
||||||
|
print(f" Or push all tags: git push --tags")
|
||||||
else:
|
else:
|
||||||
print("Git Repository: Not available")
|
print("Git Repository: Not available")
|
||||||
|
|
||||||
|
|||||||
@@ -48,12 +48,16 @@ class GitManager:
|
|||||||
except subprocess.CalledProcessError:
|
except subprocess.CalledProcessError:
|
||||||
latest_tag = None
|
latest_tag = None
|
||||||
|
|
||||||
|
# Get unpushed tags
|
||||||
|
unpushed_tags = self.get_unpushed_tags()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'is_repo': True,
|
'is_repo': True,
|
||||||
'branch': current_branch,
|
'branch': current_branch,
|
||||||
'has_changes': has_changes,
|
'has_changes': has_changes,
|
||||||
'latest_commit': latest_commit,
|
'latest_commit': latest_commit,
|
||||||
'latest_tag': latest_tag
|
'latest_tag': latest_tag,
|
||||||
|
'unpushed_tags': unpushed_tags
|
||||||
}
|
}
|
||||||
except subprocess.CalledProcessError:
|
except subprocess.CalledProcessError:
|
||||||
return {'is_repo': False}
|
return {'is_repo': False}
|
||||||
@@ -178,6 +182,47 @@ class GitManager:
|
|||||||
except subprocess.CalledProcessError:
|
except subprocess.CalledProcessError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_unpushed_tags(self, remote: str = 'origin') -> List[str]:
|
||||||
|
"""Get list of tags that exist locally but not on remote.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
remote: Remote name to compare against (default: 'origin')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of unpushed tag names
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Get local tags
|
||||||
|
local_result = self._run_command(['git', 'tag', '-l'])
|
||||||
|
local_tags = set(tag.strip() for tag in local_result.stdout.strip().split('\n') if tag.strip())
|
||||||
|
|
||||||
|
# Get remote tags
|
||||||
|
try:
|
||||||
|
remote_result = self._run_command(['git', 'ls-remote', '--tags', remote])
|
||||||
|
remote_lines = remote_result.stdout.strip().split('\n')
|
||||||
|
|
||||||
|
# Parse remote tags (format: "hash refs/tags/tagname")
|
||||||
|
remote_tags = set()
|
||||||
|
for line in remote_lines:
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
parts = line.split('refs/tags/')
|
||||||
|
if len(parts) > 1:
|
||||||
|
# Remove ^{} suffix for annotated tags
|
||||||
|
tag_name = parts[1].replace('^{}', '')
|
||||||
|
remote_tags.add(tag_name)
|
||||||
|
|
||||||
|
# Find tags that are local but not remote
|
||||||
|
unpushed = sorted(local_tags - remote_tags)
|
||||||
|
return unpushed
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
# Remote not available, assume all tags are unpushed
|
||||||
|
return sorted(local_tags)
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return []
|
||||||
|
|
||||||
def _run_command(self, cmd: List[str]) -> subprocess.CompletedProcess:
|
def _run_command(self, cmd: List[str]) -> subprocess.CompletedProcess:
|
||||||
"""Run a git command.
|
"""Run a git command.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,587 @@
|
|||||||
|
# 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
|
||||||
Reference in New Issue
Block a user