feat: implement comprehensive release process and automation (issue #81)
- Add complete release automation script (release.py) with version management - Add semantic versioning validation and git integration - Create automated changelog generation from git commits - Add comprehensive Makefile targets for release workflow - Set up package building with source and wheel distributions - Add git tagging and repository management - Create extensive release documentation (RELEASE.md) - Add CHANGELOG.md with standardized format - Update dependencies in pyproject.toml (add toml package) Release commands added: - make release-status - Show current release status - make release-validate - Validate repository for release - make release-prepare VERSION=x.y.z - Prepare new release - make release-build - Build release packages - make release-publish VERSION=x.y.z - Complete release workflow - make release-dry-run VERSION=x.y.z - Test release preparation Features: - Semantic versioning with pre-release support - Automated version updates across files - Git status validation and branch checking - Test execution validation - Package building with build tool integration - Git tagging with proper annotations - Comprehensive error handling and validation Resolves #81 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
63
CHANGELOG.md
Normal file
63
CHANGELOG.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to MarkiTect will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Comprehensive installer system with Python and shell scripts
|
||||
- Version and release information commands (`markitect version`, `markitect release`)
|
||||
- Global `--version` flag for quick version checking
|
||||
- Git integration for version metadata (commit, branch, tag information)
|
||||
- Multiple output formats for release information (text, JSON, YAML)
|
||||
- Installation documentation and troubleshooting guides
|
||||
|
||||
### Fixed
|
||||
- All test failures resolved (800/800 tests passing)
|
||||
- Visualization schema tests updated for correct tool paths
|
||||
- Cache management test isolation issues
|
||||
- Missing dependencies documentation and installation
|
||||
|
||||
### Documentation
|
||||
- Added comprehensive INSTALL.md with installation instructions
|
||||
- Added DEPENDENCIES.md with dependency information
|
||||
- Created release process documentation
|
||||
|
||||
## [0.1.0] - 2025-10-03
|
||||
|
||||
### Added
|
||||
- Initial MarkiTect implementation
|
||||
- Core markdown processing with AST caching
|
||||
- Front matter and content matter support
|
||||
- Database integration for document metadata
|
||||
- CLI interface with comprehensive commands
|
||||
- Schema generation and validation
|
||||
- Template rendering system
|
||||
- Issue management integration
|
||||
- TDD workflow tools (TDDAI)
|
||||
- Comprehensive test suite with architectural layers
|
||||
- Documentation and architectural guides
|
||||
|
||||
### Features
|
||||
- Document ingestion and processing
|
||||
- Metadata extraction and querying
|
||||
- AST analysis and caching
|
||||
- Content statistics and analysis
|
||||
- Template-based document generation
|
||||
- Associated file management
|
||||
- Database operations with multiple output formats
|
||||
- Performance monitoring and optimization
|
||||
- Legacy compatibility system
|
||||
|
||||
### Technical
|
||||
- Python 3.8+ support
|
||||
- Click-based CLI framework
|
||||
- SQLite database backend
|
||||
- Markdown-it-py parser integration
|
||||
- Comprehensive test coverage
|
||||
- Type checking with mypy
|
||||
- Code formatting with black
|
||||
- Project structure following clean architecture principles
|
||||
47
Makefile
47
Makefile
@@ -1,7 +1,7 @@
|
||||
# MarkiTect - Advanced Markdown Engine
|
||||
# Makefile for common development tasks
|
||||
|
||||
.PHONY: help setup install test build clean update status dev lint format check-deps venv-status update-digest add-diary-entry list-issues show-issue list-open-issues close-issue close-issue-enhanced close-issues-batch test-from-issue tdd-start tdd-add-test tdd-finish tdd-status test-status test-new test-coverage test-arch test-foundation test-infrastructure test-integration test-domain test-service test-application test-presentation test-quick test-layers test-random test-random-seed test-random-repeat test-install-randomly test-clean test-tdd test-changed test-module test-cache-clean test-efficient cli-help
|
||||
.PHONY: help setup install test build clean update status dev lint format check-deps venv-status update-digest add-diary-entry list-issues show-issue list-open-issues close-issue close-issue-enhanced close-issues-batch test-from-issue tdd-start tdd-add-test tdd-finish tdd-status test-status test-new test-coverage test-arch test-foundation test-infrastructure test-integration test-domain test-service test-application test-presentation test-quick test-layers test-random test-random-seed test-random-repeat test-install-randomly test-clean test-tdd test-changed test-module test-cache-clean test-efficient cli-help release-status release-validate release-prepare release-build release-publish release-dry-run
|
||||
|
||||
# Default target
|
||||
help:
|
||||
@@ -26,6 +26,14 @@ help:
|
||||
@echo " lint - Run code linting"
|
||||
@echo " format - Format code"
|
||||
@echo ""
|
||||
@echo "Release Management:"
|
||||
@echo " release-status - Show current release status"
|
||||
@echo " release-validate - Validate repository for release"
|
||||
@echo " release-prepare VERSION=x.y.z - Prepare new release"
|
||||
@echo " release-build - Build release packages"
|
||||
@echo " release-publish VERSION=x.y.z - Publish complete release"
|
||||
@echo " release-dry-run VERSION=x.y.z - Test release preparation"
|
||||
@echo ""
|
||||
@echo "Architectural Testing:"
|
||||
@echo " test-arch - Run all tests in architectural order"
|
||||
@echo " test-foundation - Run foundation layer tests only"
|
||||
@@ -200,6 +208,43 @@ build: $(VENV)/bin/activate
|
||||
$(VENV_PYTHON) -m build 2>/dev/null || \
|
||||
$(VENV_PIP) install build && $(VENV_PYTHON) -m build
|
||||
|
||||
# Release management
|
||||
release-status:
|
||||
@echo "🔍 Checking release status..."
|
||||
$(VENV_PYTHON) release.py status
|
||||
|
||||
release-validate:
|
||||
@echo "✅ Validating release readiness..."
|
||||
$(VENV_PYTHON) release.py validate
|
||||
|
||||
release-prepare:
|
||||
@echo "🚀 Preparing release..."
|
||||
@if [ -z "$(VERSION)" ]; then \
|
||||
echo "❌ Usage: make release-prepare VERSION=1.0.0"; \
|
||||
exit 1; \
|
||||
fi
|
||||
$(VENV_PYTHON) release.py prepare --version $(VERSION)
|
||||
|
||||
release-build:
|
||||
@echo "📦 Building release packages..."
|
||||
$(VENV_PYTHON) release.py build $(if $(VERSION),--version $(VERSION))
|
||||
|
||||
release-publish:
|
||||
@echo "📢 Publishing release..."
|
||||
@if [ -z "$(VERSION)" ]; then \
|
||||
echo "❌ Usage: make release-publish VERSION=1.0.0"; \
|
||||
exit 1; \
|
||||
fi
|
||||
$(VENV_PYTHON) release.py publish --version $(VERSION)
|
||||
|
||||
release-dry-run:
|
||||
@echo "🧪 Dry run release preparation..."
|
||||
@if [ -z "$(VERSION)" ]; then \
|
||||
echo "❌ Usage: make release-dry-run VERSION=1.0.0"; \
|
||||
exit 1; \
|
||||
fi
|
||||
$(VENV_PYTHON) release.py prepare --version $(VERSION) --dry-run
|
||||
|
||||
# Code linting
|
||||
lint: $(VENV)/bin/activate
|
||||
@echo "🔍 Running linting..."
|
||||
|
||||
332
RELEASE.md
Normal file
332
RELEASE.md
Normal file
@@ -0,0 +1,332 @@
|
||||
# MarkiTect Release Process
|
||||
|
||||
This document describes the release process for MarkiTect, including versioning strategy, automation tools, and distribution guidelines.
|
||||
|
||||
## Quick Start
|
||||
|
||||
The simplest way to create a release:
|
||||
|
||||
```bash
|
||||
# 1. Prepare the release
|
||||
make release-prepare VERSION=1.0.0
|
||||
|
||||
# 2. Review and commit changes
|
||||
git add -A && git commit -m "Prepare release 1.0.0"
|
||||
|
||||
# 3. Publish the release
|
||||
make release-publish VERSION=1.0.0
|
||||
```
|
||||
|
||||
## Release Commands
|
||||
|
||||
### Status and Validation
|
||||
|
||||
```bash
|
||||
# Check current release status
|
||||
make release-status
|
||||
|
||||
# Validate repository for release
|
||||
make release-validate
|
||||
```
|
||||
|
||||
### Release Preparation
|
||||
|
||||
```bash
|
||||
# Prepare a new release (updates version, changelog)
|
||||
make release-prepare VERSION=x.y.z
|
||||
|
||||
# Test preparation without making changes
|
||||
make release-dry-run VERSION=x.y.z
|
||||
```
|
||||
|
||||
### Building and Publishing
|
||||
|
||||
```bash
|
||||
# Build release packages only
|
||||
make release-build [VERSION=x.y.z]
|
||||
|
||||
# Complete release (build + tag + publish)
|
||||
make release-publish VERSION=x.y.z
|
||||
```
|
||||
|
||||
## Versioning Strategy
|
||||
|
||||
MarkiTect follows [Semantic Versioning](https://semver.org/):
|
||||
|
||||
- **MAJOR.MINOR.PATCH** (e.g., 1.2.3)
|
||||
- **Pre-release**: MAJOR.MINOR.PATCH-{alpha|beta|rc}.N (e.g., 1.2.3-beta.1)
|
||||
|
||||
### Version Types
|
||||
|
||||
- **Major (X.0.0)**: Breaking changes, incompatible API changes
|
||||
- **Minor (x.Y.0)**: New features, backward compatible
|
||||
- **Patch (x.y.Z)**: Bug fixes, backward compatible
|
||||
- **Pre-release**: Alpha, beta, or release candidate versions
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Major release
|
||||
make release-prepare VERSION=2.0.0
|
||||
|
||||
# Minor release
|
||||
make release-prepare VERSION=1.1.0
|
||||
|
||||
# Patch release
|
||||
make release-prepare VERSION=1.0.1
|
||||
|
||||
# Pre-release
|
||||
make release-prepare VERSION=1.1.0-beta.1
|
||||
```
|
||||
|
||||
## Release Validation
|
||||
|
||||
Before a release can be created, the following validations are performed:
|
||||
|
||||
### Required Conditions
|
||||
|
||||
1. **Clean Repository**: No uncommitted changes
|
||||
2. **Main Branch**: Must be on the `main` branch
|
||||
3. **Passing Tests**: All tests must pass
|
||||
4. **Valid Version**: Version must follow semantic versioning
|
||||
5. **Version Increment**: New version must be greater than current
|
||||
|
||||
### Override Validation
|
||||
|
||||
Use `--force` to override validation warnings:
|
||||
|
||||
```bash
|
||||
python release.py prepare --version 1.0.1 --force
|
||||
```
|
||||
|
||||
## Automated Release Process
|
||||
|
||||
### What `release-prepare` Does
|
||||
|
||||
1. **Version Update**: Updates `pyproject.toml` and `markitect/__version__.py`
|
||||
2. **Changelog Generation**: Creates/updates `CHANGELOG.md` from git commits
|
||||
3. **Validation**: Ensures repository is ready for release
|
||||
|
||||
### What `release-publish` Does
|
||||
|
||||
1. **Package Building**: Creates source distribution and wheel
|
||||
2. **Git Tagging**: Creates annotated git tag (e.g., `v1.0.0`)
|
||||
3. **Tag Push**: Pushes tag to remote repository
|
||||
|
||||
## Manual Release Process
|
||||
|
||||
If you prefer manual control:
|
||||
|
||||
### 1. Update Version
|
||||
|
||||
```bash
|
||||
# Edit pyproject.toml
|
||||
version = "1.0.0"
|
||||
|
||||
# Edit markitect/__version__.py
|
||||
__version__ = "1.0.0"
|
||||
```
|
||||
|
||||
### 2. Update Changelog
|
||||
|
||||
Edit `CHANGELOG.md` to add release notes for the new version.
|
||||
|
||||
### 3. Commit Changes
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "Prepare release 1.0.0"
|
||||
```
|
||||
|
||||
### 4. Build Packages
|
||||
|
||||
```bash
|
||||
make release-build
|
||||
```
|
||||
|
||||
### 5. Create Git Tag
|
||||
|
||||
```bash
|
||||
git tag -a v1.0.0 -m "Release 1.0.0"
|
||||
git push origin v1.0.0
|
||||
```
|
||||
|
||||
## Distribution
|
||||
|
||||
### Package Types
|
||||
|
||||
MarkiTect releases include:
|
||||
|
||||
- **Source Distribution** (`.tar.gz`): Full source code package
|
||||
- **Wheel** (`.whl`): Pre-built binary package for faster installation
|
||||
|
||||
### Installation Methods
|
||||
|
||||
Users can install MarkiTect in several ways:
|
||||
|
||||
```bash
|
||||
# From PyPI (when published)
|
||||
pip install markitect
|
||||
|
||||
# From wheel file
|
||||
pip install markitect-1.0.0-py3-none-any.whl
|
||||
|
||||
# From source
|
||||
pip install markitect-1.0.0.tar.gz
|
||||
|
||||
# Development installation
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
### Release Artifacts
|
||||
|
||||
Each release creates:
|
||||
|
||||
- Source and wheel packages in `dist/`
|
||||
- Git tag (e.g., `v1.0.0`)
|
||||
- Updated `CHANGELOG.md`
|
||||
- Updated version files
|
||||
|
||||
## Changelog Format
|
||||
|
||||
The automated changelog generation categorizes commits:
|
||||
|
||||
### Commit Prefixes
|
||||
|
||||
- `feat:` or `feature:` → **Added** section
|
||||
- `fix:` or `bugfix:` → **Fixed** section
|
||||
- `docs:` or `doc:` → **Documentation** section
|
||||
- Other commits → **Other** section
|
||||
|
||||
### Example Changelog Entry
|
||||
|
||||
```markdown
|
||||
## [1.0.0] - 2025-10-03
|
||||
|
||||
### Added
|
||||
- feat: add template rendering system
|
||||
- feature: implement cache management commands
|
||||
|
||||
### Fixed
|
||||
- fix: resolve test isolation issues
|
||||
- bugfix: correct version information display
|
||||
|
||||
### Documentation
|
||||
- docs: add comprehensive installation guide
|
||||
- doc: update API documentation
|
||||
|
||||
### Other
|
||||
- chore: cleanup repository structure
|
||||
- refactor: improve code organization
|
||||
```
|
||||
|
||||
## Release Checklist
|
||||
|
||||
### Pre-Release
|
||||
|
||||
- [ ] All tests passing (`make test`)
|
||||
- [ ] No uncommitted changes
|
||||
- [ ] On `main` branch
|
||||
- [ ] Version number decided
|
||||
- [ ] Release notes ready
|
||||
|
||||
### Release Process
|
||||
|
||||
- [ ] Run `make release-prepare VERSION=x.y.z`
|
||||
- [ ] Review generated changelog
|
||||
- [ ] Commit changes
|
||||
- [ ] Run `make release-publish VERSION=x.y.z`
|
||||
- [ ] Verify packages created
|
||||
- [ ] Verify git tag created
|
||||
|
||||
### Post-Release
|
||||
|
||||
- [ ] Packages available in `dist/`
|
||||
- [ ] Git tag pushed to remote
|
||||
- [ ] Changelog updated
|
||||
- [ ] Version information correct
|
||||
- [ ] Installation tested
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Validation Failures**
|
||||
```bash
|
||||
# Check what's wrong
|
||||
make release-validate
|
||||
|
||||
# Force release if needed
|
||||
python release.py prepare --version 1.0.0 --force
|
||||
```
|
||||
|
||||
2. **Build Failures**
|
||||
```bash
|
||||
# Install build dependencies
|
||||
pip install build
|
||||
|
||||
# Clean and rebuild
|
||||
rm -rf dist/ build/
|
||||
make release-build
|
||||
```
|
||||
|
||||
3. **Git Issues**
|
||||
```bash
|
||||
# Check git status
|
||||
git status
|
||||
|
||||
# Commit changes
|
||||
git add -A && git commit -m "Prepare release"
|
||||
```
|
||||
|
||||
4. **Version Conflicts**
|
||||
```bash
|
||||
# Check current version
|
||||
make release-status
|
||||
|
||||
# Use correct version number
|
||||
make release-prepare VERSION=1.0.1 # Must be > current
|
||||
```
|
||||
|
||||
### Getting Help
|
||||
|
||||
```bash
|
||||
# Release tool help
|
||||
python release.py --help
|
||||
|
||||
# Makefile targets
|
||||
make help
|
||||
|
||||
# Command-specific help
|
||||
python release.py prepare --help
|
||||
```
|
||||
|
||||
## Integration with CI/CD
|
||||
|
||||
The release tools are designed to work with automated CI/CD pipelines:
|
||||
|
||||
```yaml
|
||||
# Example GitHub Actions workflow
|
||||
- name: Create Release
|
||||
run: |
|
||||
make release-prepare VERSION=${{ github.event.inputs.version }}
|
||||
git add -A
|
||||
git commit -m "Prepare release ${{ github.event.inputs.version }}"
|
||||
make release-publish VERSION=${{ github.event.inputs.version }}
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Release artifacts should be signed
|
||||
- Use trusted publishing methods
|
||||
- Verify package contents before distribution
|
||||
- Keep release tools and dependencies updated
|
||||
|
||||
## Support
|
||||
|
||||
For release-related issues:
|
||||
|
||||
1. Check this documentation
|
||||
2. Run `make release-status` for diagnostics
|
||||
3. Use `--dry-run` to test changes
|
||||
4. Report issues on the project tracker
|
||||
@@ -8,7 +8,7 @@ version = "0.1.0"
|
||||
description = "Advanced Markdown engine for structured content"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.8"
|
||||
dependencies = ["markdown-it-py", "PyYAML", "click>=8.0.0", "tabulate>=0.9.0", "jsonpath-ng>=1.5.0", "aiohttp>=3.8.0"]
|
||||
dependencies = ["markdown-it-py", "PyYAML", "click>=8.0.0", "tabulate>=0.9.0", "jsonpath-ng>=1.5.0", "aiohttp>=3.8.0", "toml"]
|
||||
|
||||
[project.scripts]
|
||||
markitect = "markitect.cli:main"
|
||||
|
||||
492
release.py
Executable file
492
release.py
Executable file
@@ -0,0 +1,492 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
MarkiTect Release Management Tool
|
||||
|
||||
This script automates the release process for MarkiTect, including:
|
||||
- Version management and validation
|
||||
- Changelog generation
|
||||
- Git tagging and repository management
|
||||
- Package building and distribution
|
||||
- Release artifact creation
|
||||
|
||||
Usage:
|
||||
python release.py [command] [options]
|
||||
|
||||
Commands:
|
||||
prepare Prepare a new release (bump version, update changelog)
|
||||
build Build release packages
|
||||
tag Create git tag for release
|
||||
publish Publish release (build + tag + distribute)
|
||||
status Show current release status
|
||||
validate Validate current state for release
|
||||
|
||||
Options:
|
||||
--version VERSION Target version (e.g., 1.0.0, 1.0.1-rc1)
|
||||
--pre-release Mark as pre-release
|
||||
--dry-run Show what would be done without making changes
|
||||
--force Force operation even with warnings
|
||||
--help Show help message
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import json
|
||||
import subprocess
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
import tempfile
|
||||
|
||||
|
||||
class ReleaseManager:
|
||||
"""Manages the MarkiTect release process."""
|
||||
|
||||
def __init__(self, dry_run=False, force=False):
|
||||
self.dry_run = dry_run
|
||||
self.force = force
|
||||
self.project_root = Path(__file__).parent.absolute()
|
||||
self.pyproject_toml = self.project_root / "pyproject.toml"
|
||||
self.version_file = self.project_root / "markitect" / "__version__.py"
|
||||
self.changelog_file = self.project_root / "CHANGELOG.md"
|
||||
|
||||
def run_command(self, cmd: List[str], capture=True, check=True, skip_dry_run=False) -> subprocess.CompletedProcess:
|
||||
"""Run a command with optional dry-run support."""
|
||||
if self.dry_run and not skip_dry_run:
|
||||
print(f"[DRY RUN] Would run: {' '.join(cmd)}")
|
||||
return subprocess.CompletedProcess(cmd, 0, "", "")
|
||||
|
||||
return subprocess.run(cmd, capture_output=capture, text=True, check=check)
|
||||
|
||||
def get_current_version(self) -> str:
|
||||
"""Get current version from pyproject.toml."""
|
||||
with open(self.pyproject_toml, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
match = re.search(r'version\s*=\s*"([^"]+)"', content)
|
||||
if not match:
|
||||
raise ValueError("Could not find version in pyproject.toml")
|
||||
|
||||
return match.group(1)
|
||||
|
||||
def validate_version(self, version: str) -> bool:
|
||||
"""Validate version format (semantic versioning)."""
|
||||
pattern = r'^(\d+)\.(\d+)\.(\d+)(?:-(alpha|beta|rc)\.?(\d+))?$'
|
||||
return bool(re.match(pattern, version))
|
||||
|
||||
def compare_versions(self, v1: str, v2: str) -> int:
|
||||
"""Compare two versions. Returns: -1 if v1 < v2, 0 if equal, 1 if v1 > v2."""
|
||||
def version_tuple(v):
|
||||
parts = v.split('-')[0].split('.')
|
||||
main = tuple(int(x) for x in parts)
|
||||
|
||||
if '-' in v:
|
||||
pre = v.split('-')[1]
|
||||
if 'alpha' in pre:
|
||||
pre_num = int(re.search(r'(\d+)', pre).group(1)) if re.search(r'(\d+)', pre) else 0
|
||||
return main + (0, pre_num)
|
||||
elif 'beta' in pre:
|
||||
pre_num = int(re.search(r'(\d+)', pre).group(1)) if re.search(r'(\d+)', pre) else 0
|
||||
return main + (1, pre_num)
|
||||
elif 'rc' in pre:
|
||||
pre_num = int(re.search(r'(\d+)', pre).group(1)) if re.search(r'(\d+)', pre) else 0
|
||||
return main + (2, pre_num)
|
||||
|
||||
return main + (3, 0) # Release version
|
||||
|
||||
t1, t2 = version_tuple(v1), version_tuple(v2)
|
||||
if t1 < t2:
|
||||
return -1
|
||||
elif t1 > t2:
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
|
||||
def update_version(self, new_version: str):
|
||||
"""Update version in pyproject.toml and __version__.py."""
|
||||
print(f"📝 Updating version to {new_version}")
|
||||
|
||||
# Update pyproject.toml
|
||||
with open(self.pyproject_toml, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
new_content = re.sub(
|
||||
r'version\s*=\s*"[^"]+"',
|
||||
f'version = "{new_version}"',
|
||||
content
|
||||
)
|
||||
|
||||
if not self.dry_run:
|
||||
with open(self.pyproject_toml, 'w') as f:
|
||||
f.write(new_content)
|
||||
|
||||
# Update __version__.py
|
||||
with open(self.version_file, 'r') as f:
|
||||
version_content = f.read()
|
||||
|
||||
new_version_content = re.sub(
|
||||
r'__version__\s*=\s*"[^"]+"',
|
||||
f'__version__ = "{new_version}"',
|
||||
version_content
|
||||
)
|
||||
|
||||
if not self.dry_run:
|
||||
with open(self.version_file, 'w') as f:
|
||||
f.write(new_version_content)
|
||||
|
||||
def get_git_status(self) -> Dict[str, any]:
|
||||
"""Get current git repository status."""
|
||||
try:
|
||||
# Check if in git repo
|
||||
result = self.run_command(['git', 'rev-parse', '--git-dir'], skip_dry_run=True)
|
||||
|
||||
# Get current branch
|
||||
branch_result = self.run_command(['git', 'branch', '--show-current'], skip_dry_run=True)
|
||||
current_branch = branch_result.stdout.strip()
|
||||
|
||||
# Check for uncommitted changes
|
||||
status_result = self.run_command(['git', 'status', '--porcelain'], skip_dry_run=True)
|
||||
has_changes = bool(status_result.stdout.strip())
|
||||
|
||||
# Get latest commit
|
||||
commit_result = self.run_command(['git', 'rev-parse', '--short', 'HEAD'], skip_dry_run=True)
|
||||
latest_commit = commit_result.stdout.strip()
|
||||
|
||||
# Get latest tag
|
||||
try:
|
||||
tag_result = self.run_command(['git', 'describe', '--tags', '--abbrev=0'], skip_dry_run=True)
|
||||
latest_tag = tag_result.stdout.strip()
|
||||
except subprocess.CalledProcessError:
|
||||
latest_tag = None
|
||||
|
||||
return {
|
||||
'is_repo': True,
|
||||
'branch': current_branch,
|
||||
'has_changes': has_changes,
|
||||
'latest_commit': latest_commit,
|
||||
'latest_tag': latest_tag
|
||||
}
|
||||
except subprocess.CalledProcessError:
|
||||
return {'is_repo': False}
|
||||
|
||||
def generate_changelog_entry(self, version: str, since_tag: str = None) -> str:
|
||||
"""Generate changelog entry from git commits."""
|
||||
print(f"📋 Generating changelog for {version}")
|
||||
|
||||
# Get commits since last tag or all commits
|
||||
if since_tag:
|
||||
cmd = ['git', 'log', f'{since_tag}..HEAD', '--oneline', '--no-merges']
|
||||
else:
|
||||
cmd = ['git', 'log', '--oneline', '--no-merges']
|
||||
|
||||
try:
|
||||
result = self.run_command(cmd)
|
||||
commits = result.stdout.strip().split('\n') if result.stdout.strip() else []
|
||||
except subprocess.CalledProcessError:
|
||||
commits = []
|
||||
|
||||
# Categorize commits
|
||||
features = []
|
||||
fixes = []
|
||||
docs = []
|
||||
other = []
|
||||
|
||||
for commit in commits:
|
||||
if not commit:
|
||||
continue
|
||||
|
||||
commit_msg = commit.split(' ', 1)[1] if ' ' in commit else commit
|
||||
|
||||
if commit_msg.startswith(('feat:', 'feature:')):
|
||||
features.append(commit_msg)
|
||||
elif commit_msg.startswith(('fix:', 'bugfix:')):
|
||||
fixes.append(commit_msg)
|
||||
elif commit_msg.startswith(('docs:', 'doc:')):
|
||||
docs.append(commit_msg)
|
||||
else:
|
||||
other.append(commit_msg)
|
||||
|
||||
# Generate changelog entry
|
||||
date = datetime.now().strftime('%Y-%m-%d')
|
||||
entry = f"## [{version}] - {date}\n\n"
|
||||
|
||||
if features:
|
||||
entry += "### Added\n"
|
||||
for feat in features:
|
||||
entry += f"- {feat}\n"
|
||||
entry += "\n"
|
||||
|
||||
if fixes:
|
||||
entry += "### Fixed\n"
|
||||
for fix in fixes:
|
||||
entry += f"- {fix}\n"
|
||||
entry += "\n"
|
||||
|
||||
if docs:
|
||||
entry += "### Documentation\n"
|
||||
for doc in docs:
|
||||
entry += f"- {doc}\n"
|
||||
entry += "\n"
|
||||
|
||||
if other:
|
||||
entry += "### Other\n"
|
||||
for oth in other:
|
||||
entry += f"- {oth}\n"
|
||||
entry += "\n"
|
||||
|
||||
return entry
|
||||
|
||||
def update_changelog(self, version: str, since_tag: str = None):
|
||||
"""Update CHANGELOG.md with new version entry."""
|
||||
entry = self.generate_changelog_entry(version, since_tag)
|
||||
|
||||
# Read existing changelog or create new one
|
||||
if self.changelog_file.exists():
|
||||
with open(self.changelog_file, 'r') as f:
|
||||
existing_content = f.read()
|
||||
else:
|
||||
existing_content = "# Changelog\n\nAll notable changes to MarkiTect will be documented in this file.\n\n"
|
||||
|
||||
# Insert new entry after header
|
||||
lines = existing_content.split('\n')
|
||||
header_end = 0
|
||||
for i, line in enumerate(lines):
|
||||
if line.startswith('## [') or (i > 0 and not line.startswith('#')):
|
||||
header_end = i
|
||||
break
|
||||
|
||||
new_lines = lines[:header_end] + entry.split('\n') + lines[header_end:]
|
||||
new_content = '\n'.join(new_lines)
|
||||
|
||||
if not self.dry_run:
|
||||
with open(self.changelog_file, 'w') as f:
|
||||
f.write(new_content)
|
||||
|
||||
def validate_release_state(self) -> Tuple[bool, List[str]]:
|
||||
"""Validate that the repository is ready for release."""
|
||||
issues = []
|
||||
|
||||
git_status = self.get_git_status()
|
||||
|
||||
if not git_status['is_repo']:
|
||||
issues.append("Not in a git repository")
|
||||
else:
|
||||
if git_status['has_changes'] and not self.force:
|
||||
issues.append("Repository has uncommitted changes")
|
||||
|
||||
if git_status['branch'] != 'main' and not self.force:
|
||||
issues.append(f"Not on main branch (currently on {git_status['branch']})")
|
||||
|
||||
# Check if tests pass (skip for dry run)
|
||||
if not self.dry_run:
|
||||
try:
|
||||
print("🧪 Running tests...")
|
||||
test_result = self.run_command(['make', 'test'], capture=False)
|
||||
if test_result.returncode != 0:
|
||||
issues.append("Tests are failing")
|
||||
except subprocess.CalledProcessError:
|
||||
issues.append("Could not run tests (make test failed)")
|
||||
except FileNotFoundError:
|
||||
# Try pytest directly
|
||||
try:
|
||||
test_result = self.run_command(['python', '-m', 'pytest'])
|
||||
if test_result.returncode != 0:
|
||||
issues.append("Tests are failing")
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
issues.append("Could not run tests")
|
||||
else:
|
||||
print("🧪 Skipping tests in dry run mode")
|
||||
|
||||
return len(issues) == 0, issues
|
||||
|
||||
def build_packages(self, version: str):
|
||||
"""Build release packages."""
|
||||
print(f"📦 Building packages for version {version}")
|
||||
|
||||
# Clean previous builds
|
||||
build_dirs = ['build', 'dist', '*.egg-info']
|
||||
for pattern in build_dirs:
|
||||
self.run_command(['rm', '-rf'] + [str(self.project_root / pattern)])
|
||||
|
||||
# Build source distribution
|
||||
print("Building source distribution...")
|
||||
self.run_command(['python', '-m', 'build', '--sdist'], capture=False)
|
||||
|
||||
# Build wheel
|
||||
print("Building wheel...")
|
||||
self.run_command(['python', '-m', 'build', '--wheel'], capture=False)
|
||||
|
||||
print("✅ Packages built successfully")
|
||||
|
||||
def create_git_tag(self, version: str, message: str = None):
|
||||
"""Create and push git tag."""
|
||||
tag_name = f"v{version}"
|
||||
tag_message = message or f"Release {version}"
|
||||
|
||||
print(f"🏷️ Creating git tag {tag_name}")
|
||||
|
||||
# Create annotated tag
|
||||
self.run_command(['git', 'tag', '-a', tag_name, '-m', tag_message])
|
||||
|
||||
# Push tag
|
||||
print(f"📤 Pushing tag to origin...")
|
||||
self.run_command(['git', 'push', 'origin', tag_name])
|
||||
|
||||
def show_status(self):
|
||||
"""Show current release status."""
|
||||
print("🔍 MarkiTect Release Status")
|
||||
print("=" * 50)
|
||||
|
||||
current_version = self.get_current_version()
|
||||
print(f"Current Version: {current_version}")
|
||||
|
||||
git_status = self.get_git_status()
|
||||
if git_status['is_repo']:
|
||||
print(f"Git Branch: {git_status['branch']}")
|
||||
print(f"Latest Commit: {git_status['latest_commit']}")
|
||||
print(f"Latest Tag: {git_status['latest_tag'] or 'None'}")
|
||||
print(f"Uncommitted Changes: {'Yes' if git_status['has_changes'] else 'No'}")
|
||||
else:
|
||||
print("Git Repository: Not available")
|
||||
|
||||
# Check build tools
|
||||
print("\nBuild Tools:")
|
||||
try:
|
||||
self.run_command(['python', '-m', 'build', '--help'])
|
||||
print("✅ build module available")
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
print("❌ build module not available (pip install build)")
|
||||
|
||||
# Check if packages exist
|
||||
dist_dir = self.project_root / "dist"
|
||||
if dist_dir.exists():
|
||||
packages = list(dist_dir.glob("*"))
|
||||
print(f"\nExisting Packages: {len(packages)} files in dist/")
|
||||
for pkg in packages:
|
||||
print(f" - {pkg.name}")
|
||||
else:
|
||||
print("\nExisting Packages: None")
|
||||
|
||||
def prepare_release(self, version: str, pre_release: bool = False):
|
||||
"""Prepare a new release."""
|
||||
print(f"🚀 Preparing release {version}")
|
||||
|
||||
# Validate version format
|
||||
if not self.validate_version(version):
|
||||
raise ValueError(f"Invalid version format: {version}")
|
||||
|
||||
# Check if version is newer than current
|
||||
current_version = self.get_current_version()
|
||||
if self.compare_versions(version, current_version) <= 0 and not self.force:
|
||||
raise ValueError(f"New version {version} must be greater than current {current_version}")
|
||||
|
||||
# Validate release state
|
||||
is_valid, issues = self.validate_release_state()
|
||||
if not is_valid:
|
||||
print("❌ Release validation failed:")
|
||||
for issue in issues:
|
||||
print(f" - {issue}")
|
||||
if not self.force:
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("⚠️ Continuing with --force flag")
|
||||
|
||||
# Update version
|
||||
self.update_version(version)
|
||||
|
||||
# Update changelog
|
||||
git_status = self.get_git_status()
|
||||
since_tag = git_status.get('latest_tag') if git_status['is_repo'] else None
|
||||
self.update_changelog(version, since_tag)
|
||||
|
||||
print(f"✅ Release {version} prepared successfully")
|
||||
print("Next steps:")
|
||||
print("1. Review and edit CHANGELOG.md if needed")
|
||||
print("2. Commit changes: git add -A && git commit -m 'Prepare release {version}'")
|
||||
print("3. Run: python release.py publish --version {version}")
|
||||
|
||||
def publish_release(self, version: str):
|
||||
"""Publish a complete release."""
|
||||
print(f"📢 Publishing release {version}")
|
||||
|
||||
# Validate state
|
||||
is_valid, issues = self.validate_release_state()
|
||||
if not is_valid and not self.force:
|
||||
print("❌ Cannot publish release due to validation issues:")
|
||||
for issue in issues:
|
||||
print(f" - {issue}")
|
||||
sys.exit(1)
|
||||
|
||||
# Build packages
|
||||
self.build_packages(version)
|
||||
|
||||
# Create git tag
|
||||
self.create_git_tag(version)
|
||||
|
||||
print(f"✅ Release {version} published successfully!")
|
||||
print(f"📦 Packages available in dist/")
|
||||
print(f"🏷️ Git tag v{version} created and pushed")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="MarkiTect Release Management Tool",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=__doc__.split('\n\n')[1]
|
||||
)
|
||||
|
||||
parser.add_argument('command', choices=['prepare', 'build', 'tag', 'publish', 'status', 'validate'],
|
||||
help='Release command to execute')
|
||||
parser.add_argument('--version', type=str, help='Target version (e.g., 1.0.0)')
|
||||
parser.add_argument('--pre-release', action='store_true', help='Mark as pre-release')
|
||||
parser.add_argument('--dry-run', action='store_true', help='Show what would be done')
|
||||
parser.add_argument('--force', action='store_true', help='Force operation despite warnings')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
manager = ReleaseManager(dry_run=args.dry_run, force=args.force)
|
||||
|
||||
try:
|
||||
if args.command == 'status':
|
||||
manager.show_status()
|
||||
|
||||
elif args.command == 'validate':
|
||||
is_valid, issues = manager.validate_release_state()
|
||||
if is_valid:
|
||||
print("✅ Repository is ready for release")
|
||||
else:
|
||||
print("❌ Release validation failed:")
|
||||
for issue in issues:
|
||||
print(f" - {issue}")
|
||||
sys.exit(1)
|
||||
|
||||
elif args.command == 'prepare':
|
||||
if not args.version:
|
||||
print("❌ --version is required for prepare command")
|
||||
sys.exit(1)
|
||||
manager.prepare_release(args.version, args.pre_release)
|
||||
|
||||
elif args.command == 'build':
|
||||
version = args.version or manager.get_current_version()
|
||||
manager.build_packages(version)
|
||||
|
||||
elif args.command == 'tag':
|
||||
if not args.version:
|
||||
print("❌ --version is required for tag command")
|
||||
sys.exit(1)
|
||||
manager.create_git_tag(args.version)
|
||||
|
||||
elif args.command == 'publish':
|
||||
if not args.version:
|
||||
print("❌ --version is required for publish command")
|
||||
sys.exit(1)
|
||||
manager.publish_release(args.version)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user