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 Tag: {status_info['latest_tag'] or 'None'}")
|
||||
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:
|
||||
print("Git Repository: Not available")
|
||||
|
||||
|
||||
@@ -48,12 +48,16 @@ class GitManager:
|
||||
except subprocess.CalledProcessError:
|
||||
latest_tag = None
|
||||
|
||||
# Get unpushed tags
|
||||
unpushed_tags = self.get_unpushed_tags()
|
||||
|
||||
return {
|
||||
'is_repo': True,
|
||||
'branch': current_branch,
|
||||
'has_changes': has_changes,
|
||||
'latest_commit': latest_commit,
|
||||
'latest_tag': latest_tag
|
||||
'latest_tag': latest_tag,
|
||||
'unpushed_tags': unpushed_tags
|
||||
}
|
||||
except subprocess.CalledProcessError:
|
||||
return {'is_repo': False}
|
||||
@@ -178,6 +182,47 @@ class GitManager:
|
||||
except subprocess.CalledProcessError:
|
||||
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:
|
||||
"""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