Compare commits
8 Commits
v0.7.0
...
61e820baf8
| Author | SHA1 | Date | |
|---|---|---|---|
| 61e820baf8 | |||
| d0ffdc057c | |||
| d505c15d40 | |||
| f546f3c175 | |||
| d8d823b101 | |||
| ab67997324 | |||
| 3298b0d911 | |||
| 8249296a43 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -96,3 +96,4 @@ ISSUES.index
|
|||||||
|
|
||||||
# Test artifacts and temporary files
|
# Test artifacts and temporary files
|
||||||
tmp/
|
tmp/
|
||||||
|
markitect/_version.py
|
||||||
|
|||||||
106
Makefile
106
Makefile
@@ -1,7 +1,10 @@
|
|||||||
# MarkiTect - Advanced Markdown Engine
|
# MarkiTect - Advanced Markdown Engine
|
||||||
# Makefile for common development tasks
|
# Makefile for common development tasks
|
||||||
|
|
||||||
.PHONY: help setup install install-dev uninstall install-home install-home-venv install-user-deps install-force-deps install-deps-venv install-system-deps list-deps setup-dev test build clean update status lint format check-deps venv-status update-digest add-diary-entry 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 chaos-validate chaos-matrix chaos-inject chaos-report cost-help
|
# Include capability discovery system
|
||||||
|
include scripts/capability_discovery.mk
|
||||||
|
|
||||||
|
.PHONY: help setup install install-dev uninstall install-home install-home-venv install-user-deps install-force-deps install-deps-venv install-system-deps list-deps setup-dev test build clean update status lint format check-deps venv-status update-digest add-diary-entry 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 chaos-validate chaos-matrix chaos-inject chaos-report cost-help
|
||||||
|
|
||||||
# Default target
|
# Default target
|
||||||
help:
|
help:
|
||||||
@@ -26,22 +29,24 @@ help:
|
|||||||
@echo ""
|
@echo ""
|
||||||
@echo "Development:"
|
@echo "Development:"
|
||||||
@echo " test - Run core tests (excluding capability-specific tests)"
|
@echo " test - Run core tests (excluding capability-specific tests)"
|
||||||
@echo " test-capabilities - Run all capability-specific tests"
|
@echo " test-capabilities - Run all capability tests (delegated to capabilities)"
|
||||||
@echo " test-capability-* - Run specific capability tests (content, utils, finance, etc.)"
|
|
||||||
@echo " test-status - Show test status summary without re-running"
|
@echo " test-status - Show test status summary without re-running"
|
||||||
@echo " test-new - Create new test file template"
|
@echo " test-new - Create new test file template"
|
||||||
@echo " test-coverage - Analyze test coverage"
|
@echo " test-coverage - Analyze test coverage"
|
||||||
@echo " build - Build the package"
|
@echo " build - Build the package"
|
||||||
|
@echo " package - Build distribution packages (wheel + sdist)"
|
||||||
@echo " lint - Run code linting"
|
@echo " lint - Run code linting"
|
||||||
@echo " format - Format code"
|
@echo " format - Format code"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Release Management:"
|
@echo "Capabilities & Extensions:"
|
||||||
@echo " release-status - Show current release status"
|
@echo " capabilities-list List all available capabilities"
|
||||||
@echo " release-validate - Validate repository for release"
|
@echo " capabilities-help Show help for all capabilities"
|
||||||
@echo " release-prepare VERSION=x.y.z - Prepare new release"
|
@echo " capabilities-status Show capability status"
|
||||||
@echo " release-build - Build release packages"
|
@echo ""
|
||||||
@echo " release-publish VERSION=x.y.z - Publish complete release"
|
@echo "Release Management (via capability):"
|
||||||
@echo " release-dry-run VERSION=x.y.z - Test release preparation"
|
@echo " release-status Show current release status"
|
||||||
|
@echo " release-publish-gitea VERSION=x.y.z Complete release + Gitea upload"
|
||||||
|
@echo " Run 'make capabilities-help' for all release commands"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Chaos Engineering:"
|
@echo "Chaos Engineering:"
|
||||||
@echo " chaos-validate - Run architectural independence validation"
|
@echo " chaos-validate - Run architectural independence validation"
|
||||||
@@ -380,32 +385,12 @@ test: $(VENV)/bin/activate
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Capability-Specific Test Targets
|
# Capability-Specific Test Targets
|
||||||
test-capabilities: test-capability-content test-capability-utils test-capability-finance test-capability-query test-capability-graphql test-capability-plugins
|
# Delegate to capability discovery system for testing capabilities
|
||||||
|
test-capabilities: capabilities-test
|
||||||
@echo "✅ All capability tests completed"
|
@echo "✅ All capability tests completed"
|
||||||
|
|
||||||
test-capability-content: $(VENV)/bin/activate
|
# Legacy test-capability-* targets are now handled by capability delegation
|
||||||
@echo "🧪 Running markitect-content capability tests..."
|
# Use 'make capability-name-test' instead (e.g., 'make markitect-content-test')
|
||||||
@cd capabilities/markitect-content && python -m pytest tests/ -v
|
|
||||||
|
|
||||||
test-capability-utils: $(VENV)/bin/activate
|
|
||||||
@echo "🧪 Running markitect-utils capability tests..."
|
|
||||||
@cd capabilities/markitect-utils && python -m pytest tests/ -v
|
|
||||||
|
|
||||||
test-capability-finance: $(VENV)/bin/activate
|
|
||||||
@echo "🧪 Running finance capability tests..."
|
|
||||||
@PYTHONPATH=. $(VENV_PYTHON) -m pytest markitect/finance/tests/ -v
|
|
||||||
|
|
||||||
test-capability-query: $(VENV)/bin/activate
|
|
||||||
@echo "🧪 Running query paradigms capability tests..."
|
|
||||||
@PYTHONPATH=. $(VENV_PYTHON) -m pytest markitect/query_paradigms/tests/ -v
|
|
||||||
|
|
||||||
test-capability-graphql: $(VENV)/bin/activate
|
|
||||||
@echo "🧪 Running GraphQL capability tests..."
|
|
||||||
@PYTHONPATH=. $(VENV_PYTHON) -m pytest markitect/graphql/tests/ -v
|
|
||||||
|
|
||||||
test-capability-plugins: $(VENV)/bin/activate
|
|
||||||
@echo "🧪 Running plugins capability tests..."
|
|
||||||
@PYTHONPATH=. $(VENV_PYTHON) -m pytest markitect/plugins/tests/ -v
|
|
||||||
|
|
||||||
# TDD8 Workflow Optimized Test Targets (Issue #57)
|
# TDD8 Workflow Optimized Test Targets (Issue #57)
|
||||||
|
|
||||||
@@ -482,42 +467,25 @@ build: $(VENV)/bin/activate
|
|||||||
$(VENV_PYTHON) -m build 2>/dev/null || \
|
$(VENV_PYTHON) -m build 2>/dev/null || \
|
||||||
$(VENV_PIP) install build && $(VENV_PYTHON) -m build
|
$(VENV_PIP) install build && $(VENV_PYTHON) -m build
|
||||||
|
|
||||||
# Release management
|
# Build distribution packages with version info
|
||||||
release-status:
|
package: $(VENV)/bin/activate
|
||||||
@echo "🔍 Checking release status..."
|
@echo "📦 Building distribution packages..."
|
||||||
$(VENV_PYTHON) release.py status
|
@echo ""
|
||||||
|
@echo "📍 Current version (setuptools-scm):"
|
||||||
|
@$(VENV_PYTHON) -m setuptools_scm 2>/dev/null || echo " setuptools-scm not available"
|
||||||
|
@echo ""
|
||||||
|
@echo "🧹 Cleaning previous builds..."
|
||||||
|
@rm -rf build/ dist/ *.egg-info/ 2>/dev/null || true
|
||||||
|
@echo "🏗️ Building wheel and source distribution..."
|
||||||
|
@$(VENV_PIP) install build setuptools-scm >/dev/null 2>&1 || true
|
||||||
|
$(VENV_PYTHON) -m build --wheel --sdist
|
||||||
|
@echo ""
|
||||||
|
@echo "✅ Packages built successfully:"
|
||||||
|
@ls -lah dist/ 2>/dev/null || echo " No packages found"
|
||||||
|
|
||||||
release-validate:
|
# Release management targets are provided by capabilities/release-management/Makefile
|
||||||
@echo "✅ Validating release readiness..."
|
# All capability targets are automatically discovered and available via delegation
|
||||||
$(VENV_PYTHON) release.py validate
|
# Run 'make capabilities-help' to see all available capability commands
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
# Chaos Engineering targets
|
# Chaos Engineering targets
|
||||||
chaos-validate:
|
chaos-validate:
|
||||||
|
|||||||
210
agents/agent-capability-manager.md
Normal file
210
agents/agent-capability-manager.md
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
# Capability Manager Agent
|
||||||
|
|
||||||
|
You are a specialized agent for managing MarkiTect's capability system. You understand the modular architecture where capabilities are self-contained packages in the `capabilities/` directory, each with their own Makefiles, documentation, and functionality.
|
||||||
|
|
||||||
|
## Your Role
|
||||||
|
|
||||||
|
You are responsible for:
|
||||||
|
- **Capability Discovery**: Finding and cataloging all capabilities in the project
|
||||||
|
- **Makefile Management**: Creating and maintaining Makefiles for capabilities
|
||||||
|
- **Target Delegation**: Ensuring proper target delegation from main Makefile to capabilities
|
||||||
|
- **Documentation**: Maintaining capability documentation and help systems
|
||||||
|
- **Quality Assurance**: Ensuring capabilities follow the established patterns
|
||||||
|
|
||||||
|
## Capability Architecture Understanding
|
||||||
|
|
||||||
|
### Directory Structure
|
||||||
|
```
|
||||||
|
markitect_project/
|
||||||
|
├── Makefile # Main project Makefile
|
||||||
|
├── scripts/
|
||||||
|
│ └── capability_discovery.mk # Auto-discovery and delegation system
|
||||||
|
└── capabilities/
|
||||||
|
├── capability-name/
|
||||||
|
│ ├── Makefile # Capability-specific targets
|
||||||
|
│ ├── README.md # Capability documentation
|
||||||
|
│ ├── pyproject.toml # Package configuration
|
||||||
|
│ └── src/capability_name/ # Source code
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Makefile System
|
||||||
|
|
||||||
|
#### Main Makefile Integration
|
||||||
|
- Includes `scripts/capability_discovery.mk` for auto-discovery
|
||||||
|
- Provides capability management targets:
|
||||||
|
- `capabilities-list` - Show all capabilities
|
||||||
|
- `capabilities-help` - Show help for all capabilities
|
||||||
|
- `capabilities-status` - Show capability status
|
||||||
|
- `capabilities-install` - Install all capabilities
|
||||||
|
|
||||||
|
#### Capability Makefile Pattern
|
||||||
|
Each capability should have a Makefile with:
|
||||||
|
1. **Capability metadata** (name, description)
|
||||||
|
2. **Help target** showing available commands
|
||||||
|
3. **Core functionality targets** specific to the capability
|
||||||
|
4. **Installation/setup targets**
|
||||||
|
5. **Testing targets**
|
||||||
|
6. **Meta information target** for discovery
|
||||||
|
|
||||||
|
#### Target Delegation System
|
||||||
|
- Direct delegation: `release-*` targets → `release-management` capability
|
||||||
|
- Generic delegation: `capability-name-target` → `capability-name/Makefile:target`
|
||||||
|
- Auto-discovery includes all capability Makefiles
|
||||||
|
|
||||||
|
### Established Patterns
|
||||||
|
|
||||||
|
#### Successful Example: release-management
|
||||||
|
```makefile
|
||||||
|
# Capability metadata
|
||||||
|
CAPABILITY_NAME := release-management
|
||||||
|
CAPABILITY_DESCRIPTION := Comprehensive release management for Python projects
|
||||||
|
|
||||||
|
# Help target
|
||||||
|
.PHONY: help
|
||||||
|
help: ## Show release management help
|
||||||
|
@echo "📦 Release Management Capability"
|
||||||
|
# ... help content
|
||||||
|
|
||||||
|
# Core targets
|
||||||
|
.PHONY: release-status release-build release-publish
|
||||||
|
release-status: ## Show current release status
|
||||||
|
release status
|
||||||
|
|
||||||
|
# Meta information
|
||||||
|
.PHONY: capability-info
|
||||||
|
capability-info: ## Show capability information
|
||||||
|
@echo "Name: $(CAPABILITY_NAME)"
|
||||||
|
@echo "Description: $(CAPABILITY_DESCRIPTION)"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### CLI Integration Pattern
|
||||||
|
- Capabilities can provide CLI tools (e.g., `release` command)
|
||||||
|
- Makefile targets can delegate to CLI commands
|
||||||
|
- CLI availability is checked before execution
|
||||||
|
|
||||||
|
## Current Capabilities to Manage
|
||||||
|
|
||||||
|
Based on the `capabilities/` directory, you need to manage:
|
||||||
|
|
||||||
|
1. **release-management** ✅ - Fully implemented with Makefile
|
||||||
|
2. **markitect-content** ❓ - Content parsing capability, needs Makefile
|
||||||
|
3. **markitect-utils** ❓ - Utility functions capability, needs Makefile
|
||||||
|
4. **issue-facade** ❓ - Issue tracking CLI, needs Makefile
|
||||||
|
5. **kaizen-agentic** ✅ - AI agent framework, has Makefile but may need review
|
||||||
|
|
||||||
|
## Your Tasks
|
||||||
|
|
||||||
|
### 1. Capability Audit
|
||||||
|
When asked to audit capabilities:
|
||||||
|
- Scan `capabilities/` directory
|
||||||
|
- Check each capability for:
|
||||||
|
- README.md existence and quality
|
||||||
|
- pyproject.toml configuration
|
||||||
|
- Makefile existence and completeness
|
||||||
|
- CLI tools or main functionality
|
||||||
|
- Integration with main project
|
||||||
|
|
||||||
|
### 2. Makefile Creation
|
||||||
|
For capabilities missing Makefiles:
|
||||||
|
- Follow the established pattern from `release-management/Makefile`
|
||||||
|
- Include appropriate targets based on capability type
|
||||||
|
- Ensure proper capability metadata
|
||||||
|
- Add help documentation
|
||||||
|
- Include installation and testing targets
|
||||||
|
|
||||||
|
### 3. Target Analysis
|
||||||
|
- Scan main Makefile for orphaned targets that should be in capabilities
|
||||||
|
- Identify targets that could benefit from delegation
|
||||||
|
- Recommend improvements to capability organization
|
||||||
|
|
||||||
|
### 4. Documentation Maintenance
|
||||||
|
- Ensure each capability has proper README.md
|
||||||
|
- Update capability descriptions and help text
|
||||||
|
- Maintain consistency across capability documentation
|
||||||
|
|
||||||
|
## Capability Types and Their Typical Targets
|
||||||
|
|
||||||
|
### Code/Library Capabilities (markitect-content, markitect-utils)
|
||||||
|
```makefile
|
||||||
|
# Typical targets
|
||||||
|
capability-name-test # Run tests
|
||||||
|
capability-name-install # Install capability
|
||||||
|
capability-name-install-dev # Install with dev dependencies
|
||||||
|
capability-name-build # Build packages
|
||||||
|
capability-name-clean # Clean build artifacts
|
||||||
|
capability-name-lint # Code linting
|
||||||
|
capability-name-format # Code formatting
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tool/CLI Capabilities (issue-facade, release-management)
|
||||||
|
```makefile
|
||||||
|
# Typical targets
|
||||||
|
capability-name-status # Show tool status
|
||||||
|
capability-name-help # Show CLI help
|
||||||
|
capability-name-install # Install tool
|
||||||
|
capability-name-config # Configure tool
|
||||||
|
capability-name-test # Run tests
|
||||||
|
```
|
||||||
|
|
||||||
|
### Framework Capabilities (kaizen-agentic)
|
||||||
|
```makefile
|
||||||
|
# Typical targets
|
||||||
|
capability-name-setup # Initial setup
|
||||||
|
capability-name-agents-list # List agents/components
|
||||||
|
capability-name-test # Run tests
|
||||||
|
capability-name-build # Build framework
|
||||||
|
capability-name-docs # Generate documentation
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quality Standards
|
||||||
|
|
||||||
|
### Makefile Requirements
|
||||||
|
- ✅ Must have capability metadata (NAME, DESCRIPTION)
|
||||||
|
- ✅ Must have help target with clear documentation
|
||||||
|
- ✅ Must have capability-info target for discovery
|
||||||
|
- ✅ Must check for dependencies/CLI availability
|
||||||
|
- ✅ Must follow consistent naming patterns
|
||||||
|
- ✅ Must include installation targets
|
||||||
|
|
||||||
|
### Documentation Requirements
|
||||||
|
- ✅ README.md with clear description
|
||||||
|
- ✅ Installation instructions
|
||||||
|
- ✅ Usage examples
|
||||||
|
- ✅ API documentation where applicable
|
||||||
|
- ✅ Integration with main project explained
|
||||||
|
|
||||||
|
### Integration Requirements
|
||||||
|
- ✅ Proper pyproject.toml configuration
|
||||||
|
- ✅ Compatible with capability discovery system
|
||||||
|
- ✅ No conflicts with existing targets
|
||||||
|
- ✅ Clear dependency management
|
||||||
|
|
||||||
|
## Commands You Should Use
|
||||||
|
|
||||||
|
When auditing and managing capabilities:
|
||||||
|
|
||||||
|
1. **Discovery Commands**:
|
||||||
|
- `make capabilities-list` - See current capabilities
|
||||||
|
- `make capabilities-status` - Check capability health
|
||||||
|
- `find capabilities/ -name "Makefile"` - Find existing Makefiles
|
||||||
|
|
||||||
|
2. **Testing Commands**:
|
||||||
|
- `make capabilities-help` - Test help system
|
||||||
|
- `make capability-name-help` - Test specific capability help
|
||||||
|
|
||||||
|
3. **File Operations**:
|
||||||
|
- Use Read tool to examine existing Makefiles and documentation
|
||||||
|
- Use Write tool to create new Makefiles
|
||||||
|
- Use Edit tool to update existing files
|
||||||
|
|
||||||
|
## Your Approach
|
||||||
|
|
||||||
|
When given a task:
|
||||||
|
1. **Assess Current State**: Use discovery commands to understand what exists
|
||||||
|
2. **Identify Gaps**: Compare what exists vs. what should exist
|
||||||
|
3. **Create Missing Components**: Generate Makefiles, documentation, etc.
|
||||||
|
4. **Validate Integration**: Test that everything works together
|
||||||
|
5. **Document Changes**: Update any necessary documentation
|
||||||
|
|
||||||
|
Remember: You're maintaining a sophisticated capability system that should be easy to extend, discover, and use. Every capability should follow the established patterns while being tailored to its specific functionality.
|
||||||
Submodule capabilities/issue-facade updated: 51aea5effb...00b9834d2f
114
capabilities/markitect-content/Makefile
Normal file
114
capabilities/markitect-content/Makefile
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# MarkiTect Content Capability Makefile
|
||||||
|
# Content parsing and statistics for MarkdownMatters documents
|
||||||
|
|
||||||
|
# Capability metadata
|
||||||
|
CAPABILITY_NAME := markitect-content
|
||||||
|
CAPABILITY_DESCRIPTION := Content parsing and statistics for MarkdownMatters documents
|
||||||
|
|
||||||
|
# Default target
|
||||||
|
.PHONY: help
|
||||||
|
help: ## Show content capability help
|
||||||
|
@echo "📄 MarkiTect Content Capability"
|
||||||
|
@echo "================================"
|
||||||
|
@echo ""
|
||||||
|
@echo "Content Operations:"
|
||||||
|
@echo " content-get FILE=file.md Extract content without frontmatter/tailmatter"
|
||||||
|
@echo " content-stats FILE=file.md Calculate content statistics (word count, etc.)"
|
||||||
|
@echo " content-stats-json FILE=file.md Get content statistics in JSON format"
|
||||||
|
@echo ""
|
||||||
|
@echo "Development & Setup:"
|
||||||
|
@echo " content-install Install content capability"
|
||||||
|
@echo " content-install-dev Install with development dependencies"
|
||||||
|
@echo " content-test Run content capability tests"
|
||||||
|
@echo " content-test-cov Run tests with coverage report"
|
||||||
|
@echo " content-lint Run code quality checks"
|
||||||
|
@echo " content-clean Clean build artifacts"
|
||||||
|
|
||||||
|
# Check if markitect command is available (assumes CLI integration)
|
||||||
|
MARKITECT_CLI := $(shell command -v markitect 2> /dev/null)
|
||||||
|
|
||||||
|
# Content Operations
|
||||||
|
.PHONY: content-get
|
||||||
|
content-get: ## Extract content without frontmatter and tailmatter (requires FILE=path/to/file.md)
|
||||||
|
ifndef FILE
|
||||||
|
@echo "❌ FILE is required. Usage: make content-get FILE=document.md"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
ifndef MARKITECT_CLI
|
||||||
|
@echo "⚠️ markitect CLI not available, trying direct Python execution..."
|
||||||
|
cd capabilities/markitect-content && python -m markitect_content.commands content-get --file "$(FILE)"
|
||||||
|
else
|
||||||
|
markitect content-get --file "$(FILE)"
|
||||||
|
endif
|
||||||
|
|
||||||
|
.PHONY: content-stats
|
||||||
|
content-stats: ## Calculate content statistics (requires FILE=path/to/file.md)
|
||||||
|
ifndef FILE
|
||||||
|
@echo "❌ FILE is required. Usage: make content-stats FILE=document.md"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
ifndef MARKITECT_CLI
|
||||||
|
@echo "⚠️ markitect CLI not available, trying direct Python execution..."
|
||||||
|
cd capabilities/markitect-content && python -m markitect_content.commands content-stats --file "$(FILE)" --format text
|
||||||
|
else
|
||||||
|
markitect content-stats --file "$(FILE)" --format text
|
||||||
|
endif
|
||||||
|
|
||||||
|
.PHONY: content-stats-json
|
||||||
|
content-stats-json: ## Get content statistics in JSON format (requires FILE=path/to/file.md)
|
||||||
|
ifndef FILE
|
||||||
|
@echo "❌ FILE is required. Usage: make content-stats-json FILE=document.md"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
ifndef MARKITECT_CLI
|
||||||
|
@echo "⚠️ markitect CLI not available, trying direct Python execution..."
|
||||||
|
cd capabilities/markitect-content && python -m markitect_content.commands content-stats --file "$(FILE)" --format json
|
||||||
|
else
|
||||||
|
markitect content-stats --file "$(FILE)" --format json
|
||||||
|
endif
|
||||||
|
|
||||||
|
# Development and Setup
|
||||||
|
.PHONY: content-install
|
||||||
|
content-install: ## Install content capability
|
||||||
|
pip install -e capabilities/markitect-content/
|
||||||
|
|
||||||
|
.PHONY: content-install-dev
|
||||||
|
content-install-dev: ## Install content capability with development dependencies
|
||||||
|
pip install -e "capabilities/markitect-content/[dev]"
|
||||||
|
|
||||||
|
.PHONY: content-test
|
||||||
|
content-test: ## Run content capability tests
|
||||||
|
cd capabilities/markitect-content && pytest tests/
|
||||||
|
|
||||||
|
.PHONY: content-test-cov
|
||||||
|
content-test-cov: ## Run tests with coverage report
|
||||||
|
cd capabilities/markitect-content && pytest tests/ --cov=markitect_content --cov-report=html --cov-report=term
|
||||||
|
|
||||||
|
.PHONY: content-lint
|
||||||
|
content-lint: ## Run code quality checks
|
||||||
|
@echo "🔍 Running code quality checks for markitect-content..."
|
||||||
|
cd capabilities/markitect-content && python -m py_compile src/markitect_content/*.py
|
||||||
|
@echo "✅ Code quality checks passed"
|
||||||
|
|
||||||
|
.PHONY: content-clean
|
||||||
|
content-clean: ## Clean build artifacts
|
||||||
|
cd capabilities/markitect-content && rm -rf build/ dist/ *.egg-info/ __pycache__/ .pytest_cache/ htmlcov/ .coverage
|
||||||
|
find capabilities/markitect-content -name "*.pyc" -delete
|
||||||
|
find capabilities/markitect-content -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
|
||||||
|
|
||||||
|
# Library Functions (for other capabilities to use)
|
||||||
|
.PHONY: content-api-test
|
||||||
|
content-api-test: ## Test content parsing API functionality
|
||||||
|
@echo "🧪 Testing content parsing API..."
|
||||||
|
cd capabilities/markitect-content && python -c "from src.markitect_content import ContentParser; parser = ContentParser(); content = parser.extract_content('---\\ntitle: Test\\n---\\n\\n# Hello\\n\\nContent here\\n\\n\`\`\`yaml tailmatter\\nfoo: bar\\n\`\`\`'); stats = parser.calculate_stats(content); print(f'Content: {repr(content)}'); print(f'Stats: {stats.to_dict()}')"
|
||||||
|
|
||||||
|
# Meta information for capability discovery
|
||||||
|
.PHONY: capability-info
|
||||||
|
capability-info: ## Show capability information
|
||||||
|
@echo "Name: $(CAPABILITY_NAME)"
|
||||||
|
@echo "Description: $(CAPABILITY_DESCRIPTION)"
|
||||||
|
@echo "Type: Library capability with CLI commands"
|
||||||
|
@echo "Main functions: Content extraction, statistics calculation"
|
||||||
|
@echo "CLI commands: content-get, content-stats"
|
||||||
|
@echo "Targets:"
|
||||||
|
@$(MAKE) --no-print-directory help | grep "^ " | sed 's/^ / /'
|
||||||
131
capabilities/markitect-utils/Makefile
Normal file
131
capabilities/markitect-utils/Makefile
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
# MarkiTect Utils Capability Makefile
|
||||||
|
# Utility functions library for the MarkiTect ecosystem
|
||||||
|
|
||||||
|
# Capability metadata
|
||||||
|
CAPABILITY_NAME := markitect-utils
|
||||||
|
CAPABILITY_DESCRIPTION := Common utility functions for the MarkiTect ecosystem
|
||||||
|
|
||||||
|
# Default target
|
||||||
|
.PHONY: help
|
||||||
|
help: ## Show utils capability help
|
||||||
|
@echo "🛠️ MarkiTect Utils Capability"
|
||||||
|
@echo "==============================="
|
||||||
|
@echo ""
|
||||||
|
@echo "Library Testing:"
|
||||||
|
@echo " utils-test-string Test string utility functions"
|
||||||
|
@echo " utils-test-file Test file utility functions"
|
||||||
|
@echo " utils-test-validation Test validation utility functions"
|
||||||
|
@echo " utils-test-api Test complete API functionality"
|
||||||
|
@echo ""
|
||||||
|
@echo "Development & Setup:"
|
||||||
|
@echo " utils-install Install utils capability"
|
||||||
|
@echo " utils-install-dev Install with development dependencies"
|
||||||
|
@echo " utils-test Run utils capability tests"
|
||||||
|
@echo " utils-test-cov Run tests with coverage report"
|
||||||
|
@echo " utils-lint Run code quality checks"
|
||||||
|
@echo " utils-clean Clean build artifacts"
|
||||||
|
@echo ""
|
||||||
|
@echo "Quality & Compliance:"
|
||||||
|
@echo " utils-validate-paradigm Validate ComposableRepositoryParadigm compliance"
|
||||||
|
@echo " utils-check-dependencies Verify zero external dependencies"
|
||||||
|
|
||||||
|
# Development and Setup
|
||||||
|
.PHONY: utils-install
|
||||||
|
utils-install: ## Install utils capability
|
||||||
|
pip install -e capabilities/markitect-utils/
|
||||||
|
|
||||||
|
.PHONY: utils-install-dev
|
||||||
|
utils-install-dev: ## Install utils capability with development dependencies
|
||||||
|
pip install -e "capabilities/markitect-utils/[dev]"
|
||||||
|
|
||||||
|
.PHONY: utils-test
|
||||||
|
utils-test: ## Run utils capability tests
|
||||||
|
cd capabilities/markitect-utils && pytest tests/
|
||||||
|
|
||||||
|
.PHONY: utils-test-cov
|
||||||
|
utils-test-cov: ## Run tests with coverage report
|
||||||
|
cd capabilities/markitect-utils && pytest tests/ --cov=markitect_utils --cov-report=html --cov-report=term
|
||||||
|
|
||||||
|
.PHONY: utils-lint
|
||||||
|
utils-lint: ## Run code quality checks
|
||||||
|
@echo "🔍 Running code quality checks for markitect-utils..."
|
||||||
|
cd capabilities/markitect-utils && python -m py_compile src/markitect_utils/*.py
|
||||||
|
@echo "✅ Code quality checks passed"
|
||||||
|
|
||||||
|
.PHONY: utils-clean
|
||||||
|
utils-clean: ## Clean build artifacts
|
||||||
|
cd capabilities/markitect-utils && rm -rf build/ dist/ *.egg-info/ __pycache__/ .pytest_cache/ htmlcov/ .coverage
|
||||||
|
find capabilities/markitect-utils -name "*.pyc" -delete
|
||||||
|
find capabilities/markitect-utils -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
|
||||||
|
|
||||||
|
# Library Function Testing
|
||||||
|
.PHONY: utils-test-string
|
||||||
|
utils-test-string: ## Test string utility functions
|
||||||
|
@echo "🧪 Testing string utilities..."
|
||||||
|
cd capabilities/markitect-utils && python -c "from src.markitect_utils import slugify, truncate, camel_to_snake, snake_to_camel, strip_ansi_codes; print('slugify(\"Hello World!\"):', slugify('Hello World!')); print('truncate(\"This is a long string\", 10):', truncate('This is a long string', 10)); print('camel_to_snake(\"camelCase\"):', camel_to_snake('camelCase')); print('snake_to_camel(\"snake_case\"):', snake_to_camel('snake_case')); print('strip_ansi_codes(\"\\\\033[31mRed\\\\033[0m\"):', strip_ansi_codes('\\033[31mRed\\033[0m')); print('✅ String utilities working')"
|
||||||
|
|
||||||
|
.PHONY: utils-test-file
|
||||||
|
utils-test-file: ## Test file utility functions
|
||||||
|
@echo "🧪 Testing file utilities..."
|
||||||
|
cd capabilities/markitect-utils && python -c "from src.markitect_utils import safe_filename, ensure_extension, normalize_path; import tempfile, os; print('safe_filename(\"file<name>.txt\"):', safe_filename('file<name>.txt')); print('ensure_extension(\"document\", \".md\"):', ensure_extension('document', '.md')); print('normalize_path(\"./test/../file.txt\"):', normalize_path('./test/../file.txt')); print('✅ File utilities working')"
|
||||||
|
|
||||||
|
.PHONY: utils-test-validation
|
||||||
|
utils-test-validation: ## Test validation utility functions
|
||||||
|
@echo "🧪 Testing validation utilities..."
|
||||||
|
cd capabilities/markitect-utils && python -c "from src.markitect_utils import is_valid_email, is_valid_url, is_valid_semver, validate_required_fields; print('is_valid_email(\"user@example.com\"):', is_valid_email('user@example.com')); print('is_valid_url(\"https://example.com\"):', is_valid_url('https://example.com')); print('is_valid_semver(\"1.0.0\"):', is_valid_semver('1.0.0')); result = validate_required_fields({'name': 'John', 'email': '', 'age': 30}, ['name', 'email', 'phone']); print('validate_required_fields test:', result); print('✅ Validation utilities working')"
|
||||||
|
|
||||||
|
.PHONY: utils-test-api
|
||||||
|
utils-test-api: ## Test complete API functionality
|
||||||
|
@echo "🧪 Testing complete utils API..."
|
||||||
|
@$(MAKE) --no-print-directory utils-test-string
|
||||||
|
@$(MAKE) --no-print-directory utils-test-file
|
||||||
|
@$(MAKE) --no-print-directory utils-test-validation
|
||||||
|
@echo "🎉 All utility functions tested successfully!"
|
||||||
|
|
||||||
|
# Quality & Compliance
|
||||||
|
.PHONY: utils-validate-paradigm
|
||||||
|
utils-validate-paradigm: ## Validate ComposableRepositoryParadigm compliance
|
||||||
|
@echo "🏛️ Validating ComposableRepositoryParadigm compliance..."
|
||||||
|
@echo "✅ Checking src layout structure..."
|
||||||
|
test -d capabilities/markitect-utils/src/markitect_utils
|
||||||
|
@echo "✅ Checking pyproject.toml exists..."
|
||||||
|
test -f capabilities/markitect-utils/pyproject.toml
|
||||||
|
@echo "✅ Checking README.md exists..."
|
||||||
|
test -f capabilities/markitect-utils/README.md
|
||||||
|
@echo "✅ Checking tests directory..."
|
||||||
|
test -d capabilities/markitect-utils/tests
|
||||||
|
@echo "✅ Verifying independent configuration..."
|
||||||
|
cd capabilities/markitect-utils && python -c "import tomllib; f=open('pyproject.toml','rb'); data=tomllib.load(f); assert data['project']['name']=='markitect-utils'; print('✅ Independent pyproject.toml configuration verified')"
|
||||||
|
@echo "🎉 ComposableRepositoryParadigm compliance validated!"
|
||||||
|
|
||||||
|
.PHONY: utils-check-dependencies
|
||||||
|
utils-check-dependencies: ## Verify zero external dependencies
|
||||||
|
@echo "📦 Checking dependency compliance..."
|
||||||
|
cd capabilities/markitect-utils && python -c "import tomllib; f=open('pyproject.toml','rb'); data=tomllib.load(f); deps=data.get('project',{}).get('dependencies',[]); print(f'❌ Found external dependencies: {deps}') if deps else print('✅ Zero external dependencies confirmed - paradigm compliant!'); exit(1) if deps else None"
|
||||||
|
|
||||||
|
# Demonstration Functions
|
||||||
|
.PHONY: utils-demo
|
||||||
|
utils-demo: ## Demonstrate utility functions with examples
|
||||||
|
@echo "🎬 MarkiTect Utils Capability Demonstration"
|
||||||
|
@echo "==========================================="
|
||||||
|
@echo ""
|
||||||
|
@echo "String Utilities:"
|
||||||
|
@$(MAKE) --no-print-directory utils-test-string
|
||||||
|
@echo ""
|
||||||
|
@echo "File Utilities:"
|
||||||
|
@$(MAKE) --no-print-directory utils-test-file
|
||||||
|
@echo ""
|
||||||
|
@echo "Validation Utilities:"
|
||||||
|
@$(MAKE) --no-print-directory utils-test-validation
|
||||||
|
|
||||||
|
# Meta information for capability discovery
|
||||||
|
.PHONY: capability-info
|
||||||
|
capability-info: ## Show capability information
|
||||||
|
@echo "Name: $(CAPABILITY_NAME)"
|
||||||
|
@echo "Description: $(CAPABILITY_DESCRIPTION)"
|
||||||
|
@echo "Type: Pure library capability (zero external dependencies)"
|
||||||
|
@echo "Main modules: string_utils, file_utils, validation_utils"
|
||||||
|
@echo "Paradigm role: Reference implementation for ComposableRepositoryParadigm"
|
||||||
|
@echo "Dependencies: None (Python standard library only)"
|
||||||
|
@echo "Targets:"
|
||||||
|
@$(MAKE) --no-print-directory help | grep "^ " | sed 's/^ / /'
|
||||||
398
capabilities/release-management/MIGRATION_PLAN.md
Normal file
398
capabilities/release-management/MIGRATION_PLAN.md
Normal file
@@ -0,0 +1,398 @@
|
|||||||
|
# Release Management Capability Migration Plan
|
||||||
|
|
||||||
|
This document outlines the step-by-step plan to migrate all version management, packaging, and release publication functionality from the main MarkiTect project into the `release-management` capability.
|
||||||
|
|
||||||
|
## 📋 Migration Overview
|
||||||
|
|
||||||
|
### Current State
|
||||||
|
Version management and release functionality is currently scattered across:
|
||||||
|
- `release.py` (main release script)
|
||||||
|
- `gitea/` directory (package registry client)
|
||||||
|
- `VERSION_MANAGEMENT.md` (documentation)
|
||||||
|
- `PACKAGE_PUBLISHING.md` (documentation)
|
||||||
|
- Makefile targets (release automation)
|
||||||
|
- setuptools-scm configuration in main `pyproject.toml`
|
||||||
|
|
||||||
|
### Target State
|
||||||
|
All release-related functionality consolidated into:
|
||||||
|
- `capabilities/release-management/` (self-contained capability)
|
||||||
|
- Main project depends on capability for release operations
|
||||||
|
- Makefile includes capability's release targets
|
||||||
|
- Clean separation of concerns
|
||||||
|
|
||||||
|
## 🚦 Migration Steps
|
||||||
|
|
||||||
|
### Phase 1: Create Capability Structure ✅ COMPLETED
|
||||||
|
- [x] Create directory structure
|
||||||
|
- [x] Create `README.md` with comprehensive documentation
|
||||||
|
- [x] Create `pyproject.toml` with full configuration
|
||||||
|
- [x] Create main `__init__.py` with API exports
|
||||||
|
- [x] Create `release.mk` for Makefile integration
|
||||||
|
|
||||||
|
### Phase 2: Move Core Files and Code
|
||||||
|
|
||||||
|
#### 2.1 Move Release Script
|
||||||
|
**Source:** `release.py` → **Target:** `src/release_management/cli/main.py`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Copy `release.py` to `src/release_management/cli/main.py`
|
||||||
|
2. Refactor into proper CLI module structure
|
||||||
|
3. Extract core logic into separate modules:
|
||||||
|
- `SimpleReleaseManager` → `src/release_management/core/manager.py`
|
||||||
|
- Git operations → `src/release_management/git/manager.py`
|
||||||
|
- Package building → `src/release_management/core/builder.py`
|
||||||
|
- Publishing logic → `src/release_management/core/publisher.py`
|
||||||
|
|
||||||
|
**Refactoring Plan:**
|
||||||
|
```python
|
||||||
|
# Current: release.py (monolithic)
|
||||||
|
class SimpleReleaseManager:
|
||||||
|
# All functionality in one class
|
||||||
|
|
||||||
|
# Target: Modular architecture
|
||||||
|
# src/release_management/core/manager.py
|
||||||
|
class ReleaseManager:
|
||||||
|
def __init__(self):
|
||||||
|
self.git_manager = GitManager()
|
||||||
|
self.builder = PackageBuilder()
|
||||||
|
self.publisher = PublishManager()
|
||||||
|
|
||||||
|
# src/release_management/git/manager.py
|
||||||
|
class GitManager:
|
||||||
|
# Git-specific operations
|
||||||
|
|
||||||
|
# src/release_management/core/builder.py
|
||||||
|
class PackageBuilder:
|
||||||
|
# Package building operations
|
||||||
|
|
||||||
|
# src/release_management/core/publisher.py
|
||||||
|
class PublishManager:
|
||||||
|
# Publishing and upload operations
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.2 Move Gitea Package Registry
|
||||||
|
**Source:** `gitea/` directory → **Target:** `src/release_management/registries/gitea/`
|
||||||
|
|
||||||
|
**File Mapping:**
|
||||||
|
```
|
||||||
|
gitea/config.py → src/release_management/registries/gitea/config.py
|
||||||
|
gitea/exceptions.py → src/release_management/registries/gitea/exceptions.py
|
||||||
|
gitea/package_registry.py → src/release_management/registries/gitea/registry.py
|
||||||
|
gitea/__init__.py → src/release_management/registries/gitea/__init__.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Refactoring:**
|
||||||
|
1. Create base registry interface: `src/release_management/registries/base.py`
|
||||||
|
2. Create registry factory: `src/release_management/registries/factory.py`
|
||||||
|
3. Adapt GiteaPackageRegistry to implement base interface
|
||||||
|
4. Add PyPI registry implementation for future use
|
||||||
|
|
||||||
|
#### 2.3 Move Documentation
|
||||||
|
**Source:** Documentation files → **Target:** `docs/` directory
|
||||||
|
|
||||||
|
**File Mapping:**
|
||||||
|
```
|
||||||
|
VERSION_MANAGEMENT.md → capabilities/release-management/docs/version_management.md
|
||||||
|
PACKAGE_PUBLISHING.md → capabilities/release-management/docs/package_publishing.md
|
||||||
|
```
|
||||||
|
|
||||||
|
**Updates Required:**
|
||||||
|
1. Update paths and references in documentation
|
||||||
|
2. Add API reference documentation
|
||||||
|
3. Create examples directory with usage samples
|
||||||
|
|
||||||
|
### Phase 3: Update Main Project Integration
|
||||||
|
|
||||||
|
#### 3.1 Update Main Makefile
|
||||||
|
**Target:** `Makefile` in main project
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
1. Include release management Makefile:
|
||||||
|
```makefile
|
||||||
|
# Add at top of Makefile
|
||||||
|
include capabilities/release-management/release.mk
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Update existing targets to use capability:
|
||||||
|
```makefile
|
||||||
|
# Old targets
|
||||||
|
release-status:
|
||||||
|
python release.py status
|
||||||
|
|
||||||
|
# New targets (provided by release.mk)
|
||||||
|
release-status:
|
||||||
|
release status
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Remove obsolete targets and replace with capability equivalents
|
||||||
|
|
||||||
|
#### 3.2 Update Main pyproject.toml
|
||||||
|
**Target:** `pyproject.toml` in main project
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
1. Add release-management as dependency:
|
||||||
|
```toml
|
||||||
|
[project.dependencies]
|
||||||
|
release-management = {path = "capabilities/release-management", develop = true}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Keep setuptools-scm configuration:
|
||||||
|
```toml
|
||||||
|
[tool.setuptools_scm]
|
||||||
|
write_to = "markitect/_version.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Remove release-specific configuration (moved to capability)
|
||||||
|
|
||||||
|
#### 3.3 Update Main Project Structure
|
||||||
|
**Cleanup Tasks:**
|
||||||
|
1. Remove `release.py` from root
|
||||||
|
2. Remove `gitea/` directory
|
||||||
|
3. Move `VERSION_MANAGEMENT.md` and `PACKAGE_PUBLISHING.md` to capability
|
||||||
|
4. Update `.gitignore` if needed
|
||||||
|
5. Update documentation references
|
||||||
|
|
||||||
|
### Phase 4: Testing and Validation
|
||||||
|
|
||||||
|
#### 4.1 Create Capability Tests
|
||||||
|
**Target:** `capabilities/release-management/tests/`
|
||||||
|
|
||||||
|
**Test Structure:**
|
||||||
|
```
|
||||||
|
tests/
|
||||||
|
├── test_manager.py # ReleaseManager tests
|
||||||
|
├── test_builder.py # PackageBuilder tests
|
||||||
|
├── test_publisher.py # PublishManager tests
|
||||||
|
├── test_git_manager.py # GitManager tests
|
||||||
|
├── test_gitea_registry.py # GiteaRegistry tests
|
||||||
|
├── test_cli.py # CLI command tests
|
||||||
|
├── test_integration.py # End-to-end tests
|
||||||
|
└── fixtures/
|
||||||
|
└── sample_packages/ # Test package artifacts
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test Coverage Goals:**
|
||||||
|
- Unit tests for all core classes
|
||||||
|
- Integration tests for registry interactions
|
||||||
|
- CLI command tests
|
||||||
|
- Mock-based tests for external dependencies
|
||||||
|
- Error handling and edge cases
|
||||||
|
|
||||||
|
#### 4.2 Validate Migration
|
||||||
|
**Verification Steps:**
|
||||||
|
1. Install capability: `pip install -e capabilities/release-management/`
|
||||||
|
2. Run capability tests: `cd capabilities/release-management && pytest`
|
||||||
|
3. Test CLI commands: `release --help`, `release status`
|
||||||
|
4. Test Makefile integration: `make release-status`
|
||||||
|
5. Perform test release workflow
|
||||||
|
6. Verify all existing functionality works
|
||||||
|
|
||||||
|
### Phase 5: Documentation and Examples
|
||||||
|
|
||||||
|
#### 5.1 Create Examples
|
||||||
|
**Target:** `capabilities/release-management/examples/`
|
||||||
|
|
||||||
|
**Example Scripts:**
|
||||||
|
- `basic_release.py` - Simple release workflow
|
||||||
|
- `custom_registry.py` - Adding new registry type
|
||||||
|
- `ci_integration.py` - CI/CD pipeline integration
|
||||||
|
- `configuration_examples.py` - Various configuration patterns
|
||||||
|
|
||||||
|
#### 5.2 Update Documentation
|
||||||
|
**Documentation Tasks:**
|
||||||
|
1. Update main project README to reference capability
|
||||||
|
2. Create API reference documentation
|
||||||
|
3. Add troubleshooting guide
|
||||||
|
4. Document configuration options
|
||||||
|
5. Provide migration guide for other projects
|
||||||
|
|
||||||
|
## 🎯 Detailed File Structure After Migration
|
||||||
|
|
||||||
|
```
|
||||||
|
markitect_project/
|
||||||
|
├── capabilities/
|
||||||
|
│ └── release-management/
|
||||||
|
│ ├── README.md ✅ CREATED
|
||||||
|
│ ├── pyproject.toml ✅ CREATED
|
||||||
|
│ ├── release.mk ✅ CREATED
|
||||||
|
│ ├── MIGRATION_PLAN.md ✅ CREATED
|
||||||
|
│ ├── src/release_management/
|
||||||
|
│ │ ├── __init__.py ✅ CREATED
|
||||||
|
│ │ ├── _version.py # Generated by setuptools-scm
|
||||||
|
│ │ ├── core/
|
||||||
|
│ │ │ ├── __init__.py
|
||||||
|
│ │ │ ├── manager.py # ReleaseManager class
|
||||||
|
│ │ │ ├── builder.py # PackageBuilder class
|
||||||
|
│ │ │ └── publisher.py # PublishManager class
|
||||||
|
│ │ ├── git/
|
||||||
|
│ │ │ ├── __init__.py
|
||||||
|
│ │ │ └── manager.py # GitManager class
|
||||||
|
│ │ ├── registries/
|
||||||
|
│ │ │ ├── __init__.py
|
||||||
|
│ │ │ ├── base.py # Registry interface
|
||||||
|
│ │ │ ├── factory.py # RegistryFactory
|
||||||
|
│ │ │ ├── gitea/
|
||||||
|
│ │ │ │ ├── __init__.py
|
||||||
|
│ │ │ │ ├── config.py # From gitea/config.py
|
||||||
|
│ │ │ │ ├── exceptions.py # From gitea/exceptions.py
|
||||||
|
│ │ │ │ └── registry.py # From gitea/package_registry.py
|
||||||
|
│ │ │ └── pypi/
|
||||||
|
│ │ │ ├── __init__.py
|
||||||
|
│ │ │ └── registry.py # PyPI registry implementation
|
||||||
|
│ │ ├── cli/
|
||||||
|
│ │ │ ├── __init__.py
|
||||||
|
│ │ │ ├── main.py # From release.py
|
||||||
|
│ │ │ ├── commands.py # CLI command implementations
|
||||||
|
│ │ │ └── utils.py # CLI utilities
|
||||||
|
│ │ └── utils/
|
||||||
|
│ │ ├── __init__.py
|
||||||
|
│ │ ├── version.py # Version utilities
|
||||||
|
│ │ └── validation.py # Release validation
|
||||||
|
│ ├── tests/
|
||||||
|
│ │ ├── __init__.py
|
||||||
|
│ │ ├── test_manager.py
|
||||||
|
│ │ ├── test_builder.py
|
||||||
|
│ │ ├── test_publisher.py
|
||||||
|
│ │ ├── test_git_manager.py
|
||||||
|
│ │ ├── test_gitea_registry.py
|
||||||
|
│ │ ├── test_cli.py
|
||||||
|
│ │ └── fixtures/
|
||||||
|
│ ├── docs/
|
||||||
|
│ │ ├── version_management.md # From VERSION_MANAGEMENT.md
|
||||||
|
│ │ ├── package_publishing.md # From PACKAGE_PUBLISHING.md
|
||||||
|
│ │ ├── api_reference.md
|
||||||
|
│ │ └── troubleshooting.md
|
||||||
|
│ └── examples/
|
||||||
|
│ ├── basic_release.py
|
||||||
|
│ ├── custom_registry.py
|
||||||
|
│ └── ci_integration.py
|
||||||
|
├── Makefile # Updated to include release.mk
|
||||||
|
├── pyproject.toml # Updated with capability dependency
|
||||||
|
└── markitect/
|
||||||
|
└── _version.py # Still generated by setuptools-scm
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📦 Files to Remove After Migration
|
||||||
|
|
||||||
|
**Root Directory:**
|
||||||
|
- [x] `release.py` (moved to capability CLI)
|
||||||
|
- [x] `gitea/` directory (moved to capability registries)
|
||||||
|
- [x] `VERSION_MANAGEMENT.md` (moved to capability docs)
|
||||||
|
- [x] `PACKAGE_PUBLISHING.md` (moved to capability docs)
|
||||||
|
|
||||||
|
**Makefile Targets to Update:**
|
||||||
|
- Replace individual release targets with capability imports
|
||||||
|
- Keep legacy aliases for backward compatibility
|
||||||
|
- Update target documentation
|
||||||
|
|
||||||
|
## 🔧 API Design for Capability
|
||||||
|
|
||||||
|
### Main API Classes
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Primary entry point
|
||||||
|
from release_management import ReleaseManager
|
||||||
|
|
||||||
|
manager = ReleaseManager()
|
||||||
|
success = manager.publish_release("1.0.0")
|
||||||
|
|
||||||
|
# Component access
|
||||||
|
from release_management import PackageBuilder, PublishManager, GitManager
|
||||||
|
|
||||||
|
builder = PackageBuilder()
|
||||||
|
builder.build_packages()
|
||||||
|
|
||||||
|
publisher = PublishManager()
|
||||||
|
publisher.upload_packages("gitea")
|
||||||
|
|
||||||
|
git = GitManager()
|
||||||
|
git.create_tag("v1.0.0")
|
||||||
|
|
||||||
|
# Registry access
|
||||||
|
from release_management import RegistryFactory
|
||||||
|
|
||||||
|
registry = RegistryFactory.create("gitea")
|
||||||
|
registry.upload_package("package.whl")
|
||||||
|
```
|
||||||
|
|
||||||
|
### CLI Interface
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Main commands
|
||||||
|
release status # Show release status
|
||||||
|
release validate # Validate release state
|
||||||
|
release tag --version 1.0.0 # Create git tag
|
||||||
|
release build # Build packages
|
||||||
|
release publish --version 1.0.0 # Complete release workflow
|
||||||
|
release upload --registry gitea # Upload existing packages
|
||||||
|
|
||||||
|
# Registry management
|
||||||
|
release registry-info --registry gitea
|
||||||
|
release registry-list
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Benefits After Migration
|
||||||
|
|
||||||
|
### For MarkiTect Project
|
||||||
|
1. **Cleaner main project**: Release logic separated from core functionality
|
||||||
|
2. **Better maintainability**: Clear module boundaries and responsibilities
|
||||||
|
3. **Easier testing**: Isolated testing of release functionality
|
||||||
|
4. **Reduced complexity**: Main project focuses on core features
|
||||||
|
|
||||||
|
### For Release Management Capability
|
||||||
|
1. **Reusability**: Can be used in other Python projects
|
||||||
|
2. **Independent development**: Own release cycle and versioning
|
||||||
|
3. **Comprehensive testing**: Full test coverage for release functionality
|
||||||
|
4. **Documentation**: Dedicated documentation and examples
|
||||||
|
5. **Extensibility**: Easy to add new registries and features
|
||||||
|
|
||||||
|
### For Users/Developers
|
||||||
|
1. **Consistent interface**: Same commands across all projects using capability
|
||||||
|
2. **Better documentation**: Comprehensive guides and API reference
|
||||||
|
3. **More features**: Enhanced functionality and registry support
|
||||||
|
4. **Easier contribution**: Clear structure for adding features
|
||||||
|
|
||||||
|
## 🎯 Success Criteria
|
||||||
|
|
||||||
|
Migration is considered successful when:
|
||||||
|
1. ✅ All existing release functionality works through capability
|
||||||
|
2. ✅ Main project Makefile targets work unchanged
|
||||||
|
3. ✅ CLI commands provide same functionality as current `release.py`
|
||||||
|
4. ✅ All tests pass for both capability and main project
|
||||||
|
5. ✅ Documentation is complete and accurate
|
||||||
|
6. ✅ Examples demonstrate capability usage
|
||||||
|
7. ✅ No regression in release workflow functionality
|
||||||
|
|
||||||
|
## 🔄 Rollback Plan
|
||||||
|
|
||||||
|
If migration issues arise:
|
||||||
|
1. **Keep backup**: Current files backed up before migration
|
||||||
|
2. **Incremental approach**: Migrate one component at a time
|
||||||
|
3. **Parallel operation**: Keep old and new systems running during transition
|
||||||
|
4. **Quick revert**: Ability to restore original structure if needed
|
||||||
|
|
||||||
|
**Rollback Steps:**
|
||||||
|
1. Remove capability dependency from main `pyproject.toml`
|
||||||
|
2. Restore backed up files (`release.py`, `gitea/`, docs)
|
||||||
|
3. Restore original Makefile targets
|
||||||
|
4. Remove capability directory
|
||||||
|
5. Test that original functionality works
|
||||||
|
|
||||||
|
## 📅 Migration Timeline
|
||||||
|
|
||||||
|
**Estimated Duration:** 1-2 weeks for complete migration
|
||||||
|
|
||||||
|
**Phase Breakdown:**
|
||||||
|
- **Phase 1 (Directory Structure):** ✅ COMPLETED
|
||||||
|
- **Phase 2 (Code Migration):** 2-3 days
|
||||||
|
- **Phase 3 (Integration):** 1-2 days
|
||||||
|
- **Phase 4 (Testing):** 2-3 days
|
||||||
|
- **Phase 5 (Documentation):** 1-2 days
|
||||||
|
|
||||||
|
**Critical Path:**
|
||||||
|
1. Code refactoring and migration
|
||||||
|
2. Testing and validation
|
||||||
|
3. Documentation updates
|
||||||
|
4. Final integration testing
|
||||||
|
|
||||||
|
This migration plan ensures a systematic, low-risk transition to the capability-based architecture while maintaining all existing functionality and improving the overall project structure.
|
||||||
231
capabilities/release-management/Makefile
Normal file
231
capabilities/release-management/Makefile
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
# Release Management Capability Makefile
|
||||||
|
# Provides release management targets for any Python project
|
||||||
|
|
||||||
|
# Capability metadata
|
||||||
|
CAPABILITY_NAME := release-management
|
||||||
|
CAPABILITY_DESCRIPTION := Comprehensive release management for Python projects
|
||||||
|
|
||||||
|
# Default target
|
||||||
|
.PHONY: help
|
||||||
|
help: ## Show release management help
|
||||||
|
@echo "📦 Release Management Capability"
|
||||||
|
@echo "================================"
|
||||||
|
@echo ""
|
||||||
|
@echo "Status & Validation:"
|
||||||
|
@echo " release-status Show current release status and version information"
|
||||||
|
@echo " release-validate Validate repository state for release readiness"
|
||||||
|
@echo " release-registry-info Show package registry information and status"
|
||||||
|
@echo ""
|
||||||
|
@echo "Git Tag Management:"
|
||||||
|
@echo " release-tag VERSION=x.y.z Create git tag for version"
|
||||||
|
@echo ""
|
||||||
|
@echo "Package Building:"
|
||||||
|
@echo " release-build Build release packages using setuptools-scm"
|
||||||
|
@echo " release-clean Clean build artifacts and temporary files"
|
||||||
|
@echo ""
|
||||||
|
@echo "Publishing Workflows:"
|
||||||
|
@echo " release-publish VERSION=x.y.z Complete release workflow (tag + build)"
|
||||||
|
@echo " release-publish-gitea VERSION=x.y.z Complete release + Gitea upload"
|
||||||
|
@echo " release-publish-pypi VERSION=x.y.z Complete release + PyPI upload"
|
||||||
|
@echo ""
|
||||||
|
@echo "Upload Existing Packages:"
|
||||||
|
@echo " release-upload-gitea Upload existing packages to Gitea registry"
|
||||||
|
@echo " release-upload-pypi Upload existing packages to PyPI"
|
||||||
|
@echo " release-upload-testpypi Upload existing packages to Test PyPI"
|
||||||
|
@echo ""
|
||||||
|
@echo "Dry Run Options:"
|
||||||
|
@echo " release-publish-dry-run VERSION=x.y.z Dry run of release workflow"
|
||||||
|
@echo " release-upload-dry-run Dry run of package upload"
|
||||||
|
@echo ""
|
||||||
|
@echo "Development & Setup:"
|
||||||
|
@echo " release-management-install Install release management capability"
|
||||||
|
@echo " release-management-install-dev Install with development dependencies"
|
||||||
|
@echo " release-management-test Run capability tests"
|
||||||
|
@echo " release-management-help Show CLI help"
|
||||||
|
|
||||||
|
# Check if release management capability is available
|
||||||
|
RELEASE_CLI := $(shell command -v release 2> /dev/null)
|
||||||
|
|
||||||
|
# Status and Information
|
||||||
|
.PHONY: release-status
|
||||||
|
release-status: ## Show current release status and version information
|
||||||
|
ifndef RELEASE_CLI
|
||||||
|
@echo "❌ Release management capability not installed"
|
||||||
|
@echo " Install with: pip install -e capabilities/release-management/"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
release status
|
||||||
|
|
||||||
|
.PHONY: release-validate
|
||||||
|
release-validate: ## Validate repository state for release readiness
|
||||||
|
ifndef RELEASE_CLI
|
||||||
|
@echo "❌ Release management capability not installed"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
release validate
|
||||||
|
|
||||||
|
.PHONY: release-registry-info
|
||||||
|
release-registry-info: ## Show package registry information and status
|
||||||
|
ifndef RELEASE_CLI
|
||||||
|
@echo "❌ Release management capability not installed"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
release registry-info
|
||||||
|
|
||||||
|
# Git Tag Management
|
||||||
|
.PHONY: release-tag
|
||||||
|
release-tag: ## Create git tag for version (requires VERSION=x.y.z)
|
||||||
|
ifndef VERSION
|
||||||
|
@echo "❌ VERSION is required. Usage: make release-tag VERSION=1.0.0"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
ifndef RELEASE_CLI
|
||||||
|
@echo "❌ Release management capability not installed"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
release tag --version $(VERSION)
|
||||||
|
|
||||||
|
# Package Building
|
||||||
|
.PHONY: release-build
|
||||||
|
release-build: ## Build release packages using setuptools-scm
|
||||||
|
ifndef RELEASE_CLI
|
||||||
|
@echo "❌ Release management capability not installed"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
release build
|
||||||
|
|
||||||
|
.PHONY: release-clean
|
||||||
|
release-clean: ## Clean build artifacts and temporary files
|
||||||
|
ifndef RELEASE_CLI
|
||||||
|
@echo "❌ Release management capability not installed"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
release clean
|
||||||
|
|
||||||
|
# Publishing Workflows
|
||||||
|
.PHONY: release-publish
|
||||||
|
release-publish: ## Complete release workflow: tag + build (requires VERSION=x.y.z)
|
||||||
|
ifndef VERSION
|
||||||
|
@echo "❌ VERSION is required. Usage: make release-publish VERSION=1.0.0"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
ifndef RELEASE_CLI
|
||||||
|
@echo "❌ Release management capability not installed"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
release publish --version $(VERSION)
|
||||||
|
|
||||||
|
.PHONY: release-publish-gitea
|
||||||
|
release-publish-gitea: ## Complete release workflow + Gitea upload (requires VERSION=x.y.z)
|
||||||
|
ifndef VERSION
|
||||||
|
@echo "❌ VERSION is required. Usage: make release-publish-gitea VERSION=1.0.0"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
ifndef RELEASE_CLI
|
||||||
|
@echo "❌ Release management capability not installed"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
release publish --version $(VERSION) --registry gitea
|
||||||
|
|
||||||
|
.PHONY: release-publish-pypi
|
||||||
|
release-publish-pypi: ## Complete release workflow + PyPI upload (requires VERSION=x.y.z)
|
||||||
|
ifndef VERSION
|
||||||
|
@echo "❌ VERSION is required. Usage: make release-publish-pypi VERSION=1.0.0"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
ifndef RELEASE_CLI
|
||||||
|
@echo "❌ Release management capability not installed"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
release publish --version $(VERSION) --registry pypi
|
||||||
|
|
||||||
|
# Upload Existing Packages
|
||||||
|
.PHONY: release-upload-gitea
|
||||||
|
release-upload-gitea: ## Upload existing packages to Gitea registry
|
||||||
|
ifndef RELEASE_CLI
|
||||||
|
@echo "❌ Release management capability not installed"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
release upload --registry gitea
|
||||||
|
|
||||||
|
.PHONY: release-upload-pypi
|
||||||
|
release-upload-pypi: ## Upload existing packages to PyPI
|
||||||
|
ifndef RELEASE_CLI
|
||||||
|
@echo "❌ Release management capability not installed"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
release upload --registry pypi
|
||||||
|
|
||||||
|
.PHONY: release-upload-testpypi
|
||||||
|
release-upload-testpypi: ## Upload existing packages to Test PyPI
|
||||||
|
ifndef RELEASE_CLI
|
||||||
|
@echo "❌ Release management capability not installed"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
release upload --registry testpypi
|
||||||
|
|
||||||
|
# Dry Run Options
|
||||||
|
.PHONY: release-publish-dry-run
|
||||||
|
release-publish-dry-run: ## Dry run of complete release workflow (requires VERSION=x.y.z)
|
||||||
|
ifndef VERSION
|
||||||
|
@echo "❌ VERSION is required. Usage: make release-publish-dry-run VERSION=1.0.0"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
ifndef RELEASE_CLI
|
||||||
|
@echo "❌ Release management capability not installed"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
release publish --version $(VERSION) --dry-run
|
||||||
|
|
||||||
|
.PHONY: release-upload-dry-run
|
||||||
|
release-upload-dry-run: ## Dry run of package upload to default registry
|
||||||
|
ifndef RELEASE_CLI
|
||||||
|
@echo "❌ Release management capability not installed"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
release upload --dry-run
|
||||||
|
|
||||||
|
# Development and Setup
|
||||||
|
.PHONY: release-management-install
|
||||||
|
release-management-install: ## Install release management capability
|
||||||
|
pip install -e capabilities/release-management/
|
||||||
|
|
||||||
|
.PHONY: release-management-install-dev
|
||||||
|
release-management-install-dev: ## Install release management capability with dev dependencies
|
||||||
|
pip install -e "capabilities/release-management/[dev]"
|
||||||
|
|
||||||
|
.PHONY: release-management-test
|
||||||
|
release-management-test: ## Run release management capability tests
|
||||||
|
cd capabilities/release-management && pytest tests/
|
||||||
|
|
||||||
|
.PHONY: release-management-help
|
||||||
|
release-management-help: ## Show release management CLI help
|
||||||
|
ifndef RELEASE_CLI
|
||||||
|
@echo "❌ Release management capability not installed"
|
||||||
|
@echo " Install with: make release-management-install"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
release --help
|
||||||
|
|
||||||
|
# Convenience aliases
|
||||||
|
.PHONY: release-upload
|
||||||
|
release-upload: release-upload-gitea ## Upload packages to default registry (gitea)
|
||||||
|
|
||||||
|
.PHONY: package
|
||||||
|
package: release-build ## Build packages (alias for release-build)
|
||||||
|
|
||||||
|
.PHONY: publish
|
||||||
|
publish: ## Publish release to default registry (requires VERSION=x.y.z)
|
||||||
|
ifndef VERSION
|
||||||
|
@echo "❌ VERSION is required. Usage: make publish VERSION=1.0.0"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
@make release-publish-gitea VERSION=$(VERSION)
|
||||||
|
|
||||||
|
# Meta information for capability discovery
|
||||||
|
.PHONY: capability-info
|
||||||
|
capability-info: ## Show capability information
|
||||||
|
@echo "Name: $(CAPABILITY_NAME)"
|
||||||
|
@echo "Description: $(CAPABILITY_DESCRIPTION)"
|
||||||
|
@echo "Targets:"
|
||||||
|
@$(MAKE) --no-print-directory help | grep "^ " | sed 's/^ / /'
|
||||||
334
capabilities/release-management/README.md
Normal file
334
capabilities/release-management/README.md
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
# Release Management Capability
|
||||||
|
|
||||||
|
A self-contained capability for version management, package building, and release publication with Git and package registry integration.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The release-management capability provides comprehensive release automation for Python projects using setuptools-scm for version management and supporting multiple publication targets including Gitea package registries.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Automatic Version Management**: Git tag-based versioning with setuptools-scm
|
||||||
|
- **Package Building**: Wheel and source distribution generation
|
||||||
|
- **Release Automation**: Complete release workflow from validation to publication
|
||||||
|
- **Multi-Platform Publishing**: Support for Gitea, GitHub, and other package registries
|
||||||
|
- **Fallback Publishing**: Release assets when package registries unavailable
|
||||||
|
- **CLI Integration**: Command-line tools for release management
|
||||||
|
- **Makefile Integration**: Convenient targets for common release tasks
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Core Components
|
||||||
|
|
||||||
|
#### `ReleaseManager`
|
||||||
|
Main orchestrator for release workflows, handling:
|
||||||
|
- Release state validation
|
||||||
|
- Git tag creation and management
|
||||||
|
- Package building coordination
|
||||||
|
- Publication orchestration
|
||||||
|
|
||||||
|
#### `PackageBuilder`
|
||||||
|
Responsible for package generation:
|
||||||
|
- setuptools-scm integration
|
||||||
|
- Wheel and source distribution building
|
||||||
|
- Build artifact management
|
||||||
|
|
||||||
|
#### `PublishManager`
|
||||||
|
Handles package publication:
|
||||||
|
- Multiple registry support (Gitea, PyPI, etc.)
|
||||||
|
- Fallback mechanisms (release assets)
|
||||||
|
- Upload progress tracking
|
||||||
|
|
||||||
|
#### `GitManager`
|
||||||
|
Git operations for releases:
|
||||||
|
- Tag creation and validation
|
||||||
|
- Repository state checking
|
||||||
|
- Branch and commit management
|
||||||
|
|
||||||
|
### Package Registry Support
|
||||||
|
|
||||||
|
#### `GiteaRegistry`
|
||||||
|
Gitea-specific package registry client:
|
||||||
|
- PyPI-compatible registry uploads
|
||||||
|
- Release asset fallback
|
||||||
|
- Authentication handling
|
||||||
|
|
||||||
|
#### `RegistryFactory`
|
||||||
|
Factory for creating registry clients:
|
||||||
|
- Auto-detection of registry types
|
||||||
|
- Configuration management
|
||||||
|
- Extensible for new registries
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### Core Classes
|
||||||
|
|
||||||
|
#### `ReleaseManager`
|
||||||
|
```python
|
||||||
|
from release_management import ReleaseManager
|
||||||
|
|
||||||
|
manager = ReleaseManager()
|
||||||
|
|
||||||
|
# Validate release readiness
|
||||||
|
is_valid, issues = manager.validate_release_state()
|
||||||
|
|
||||||
|
# Create complete release
|
||||||
|
success = manager.publish_release("0.8.0")
|
||||||
|
|
||||||
|
# Publish with specific registry
|
||||||
|
success = manager.publish_with_registry("0.8.0", registry_type="gitea")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `PackageBuilder`
|
||||||
|
```python
|
||||||
|
from release_management import PackageBuilder
|
||||||
|
|
||||||
|
builder = PackageBuilder()
|
||||||
|
|
||||||
|
# Build packages
|
||||||
|
builder.build_packages()
|
||||||
|
|
||||||
|
# Get current version
|
||||||
|
version = builder.get_current_version()
|
||||||
|
|
||||||
|
# Clean build artifacts
|
||||||
|
builder.clean_build()
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `PublishManager`
|
||||||
|
```python
|
||||||
|
from release_management import PublishManager
|
||||||
|
|
||||||
|
publisher = PublishManager()
|
||||||
|
|
||||||
|
# Publish to registry
|
||||||
|
success = publisher.publish_packages("gitea", dry_run=True)
|
||||||
|
|
||||||
|
# Upload specific files
|
||||||
|
success = publisher.upload_file("dist/package.whl", "gitea")
|
||||||
|
```
|
||||||
|
|
||||||
|
### CLI Commands
|
||||||
|
|
||||||
|
#### `release`
|
||||||
|
Main release command with subcommands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Show release status
|
||||||
|
release status
|
||||||
|
|
||||||
|
# Validate release readiness
|
||||||
|
release validate
|
||||||
|
|
||||||
|
# Create git tag
|
||||||
|
release tag --version 0.8.0
|
||||||
|
|
||||||
|
# Build packages
|
||||||
|
release build
|
||||||
|
|
||||||
|
# Complete release workflow
|
||||||
|
release publish --version 0.8.0
|
||||||
|
|
||||||
|
# Publish to specific registry
|
||||||
|
release publish --version 0.8.0 --registry gitea
|
||||||
|
|
||||||
|
# Upload existing packages
|
||||||
|
release upload --registry gitea
|
||||||
|
|
||||||
|
# Show registry information
|
||||||
|
release registry-info --registry gitea
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
#### Release Configuration
|
||||||
|
Configure release behavior in `pyproject.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[tool.release-management]
|
||||||
|
# Default registry for publishing
|
||||||
|
default_registry = "gitea"
|
||||||
|
|
||||||
|
# Validation requirements
|
||||||
|
require_clean_tree = true
|
||||||
|
require_main_branch = true
|
||||||
|
|
||||||
|
# Package building
|
||||||
|
build_wheel = true
|
||||||
|
build_sdist = true
|
||||||
|
clean_before_build = true
|
||||||
|
|
||||||
|
# Registry configurations
|
||||||
|
[tool.release-management.registries.gitea]
|
||||||
|
url = "http://92.205.130.254:32166"
|
||||||
|
owner = "coulomb"
|
||||||
|
repo = "markitect_project"
|
||||||
|
auth_token_env = "GITEA_API_TOKEN"
|
||||||
|
|
||||||
|
[tool.release-management.registries.pypi]
|
||||||
|
url = "https://upload.pypi.org/legacy/"
|
||||||
|
auth_token_env = "PYPI_TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Install as an editable dependency:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -e capabilities/release-management/
|
||||||
|
```
|
||||||
|
|
||||||
|
Or with development dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -e "capabilities/release-management/[dev]"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd capabilities/release-management/
|
||||||
|
pip install -e ".[dev]"
|
||||||
|
pytest tests/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration with Main Project
|
||||||
|
|
||||||
|
The main project integrates with this capability through:
|
||||||
|
|
||||||
|
### Makefile Integration
|
||||||
|
```makefile
|
||||||
|
# Include release management targets
|
||||||
|
include capabilities/release-management/release.mk
|
||||||
|
|
||||||
|
# Or call capability directly
|
||||||
|
release-status:
|
||||||
|
release status
|
||||||
|
|
||||||
|
release-publish:
|
||||||
|
release publish --version $(VERSION)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Setup Configuration
|
||||||
|
In main project's `pyproject.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[tool.setuptools_scm]
|
||||||
|
write_to = "markitect/_version.py"
|
||||||
|
|
||||||
|
[tool.release-management]
|
||||||
|
default_registry = "gitea"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
This capability consolidates the following existing components:
|
||||||
|
|
||||||
|
### Files to Move
|
||||||
|
1. **`release.py`** → `src/release_management/cli/main.py`
|
||||||
|
2. **`gitea/`** directory → `src/release_management/registries/gitea/`
|
||||||
|
3. **VERSION_MANAGEMENT.md** → `docs/version_management.md`
|
||||||
|
4. **PACKAGE_PUBLISHING.md** → `docs/package_publishing.md`
|
||||||
|
5. **Makefile release targets** → `release.mk`
|
||||||
|
|
||||||
|
### New Structure
|
||||||
|
```
|
||||||
|
capabilities/release-management/
|
||||||
|
├── README.md
|
||||||
|
├── pyproject.toml
|
||||||
|
├── release.mk # Makefile integration
|
||||||
|
├── src/release_management/
|
||||||
|
│ ├── __init__.py # Main API exports
|
||||||
|
│ ├── core/
|
||||||
|
│ │ ├── __init__.py
|
||||||
|
│ │ ├── manager.py # ReleaseManager class
|
||||||
|
│ │ ├── builder.py # PackageBuilder class
|
||||||
|
│ │ └── publisher.py # PublishManager class
|
||||||
|
│ ├── git/
|
||||||
|
│ │ ├── __init__.py
|
||||||
|
│ │ └── manager.py # GitManager class
|
||||||
|
│ ├── registries/
|
||||||
|
│ │ ├── __init__.py
|
||||||
|
│ │ ├── factory.py # RegistryFactory
|
||||||
|
│ │ ├── base.py # Registry interface
|
||||||
|
│ │ ├── gitea/
|
||||||
|
│ │ │ ├── __init__.py
|
||||||
|
│ │ │ ├── registry.py # GiteaRegistry
|
||||||
|
│ │ │ ├── config.py # GiteaConfig
|
||||||
|
│ │ │ └── exceptions.py # GiteaError
|
||||||
|
│ │ └── pypi/
|
||||||
|
│ │ ├── __init__.py
|
||||||
|
│ │ └── registry.py # PyPIRegistry
|
||||||
|
│ ├── cli/
|
||||||
|
│ │ ├── __init__.py
|
||||||
|
│ │ ├── main.py # Main CLI entry point
|
||||||
|
│ │ ├── commands.py # CLI command implementations
|
||||||
|
│ │ └── utils.py # CLI utilities
|
||||||
|
│ └── utils/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── version.py # Version management utilities
|
||||||
|
│ └── validation.py # Release validation utilities
|
||||||
|
├── tests/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── test_manager.py
|
||||||
|
│ ├── test_builder.py
|
||||||
|
│ ├── test_publisher.py
|
||||||
|
│ ├── test_git_manager.py
|
||||||
|
│ ├── test_gitea_registry.py
|
||||||
|
│ └── fixtures/
|
||||||
|
│ └── sample_packages/
|
||||||
|
├── docs/
|
||||||
|
│ ├── version_management.md
|
||||||
|
│ ├── package_publishing.md
|
||||||
|
│ ├── api_reference.md
|
||||||
|
│ └── examples/
|
||||||
|
└── examples/
|
||||||
|
├── basic_release.py
|
||||||
|
├── custom_registry.py
|
||||||
|
└── ci_integration.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Benefits of Capability Structure
|
||||||
|
|
||||||
|
### Modularity
|
||||||
|
- **Self-contained**: Independent testing and development
|
||||||
|
- **Reusable**: Can be used in other projects
|
||||||
|
- **Focused**: Single responsibility for release management
|
||||||
|
|
||||||
|
### Maintainability
|
||||||
|
- **Clear boundaries**: Well-defined API surface
|
||||||
|
- **Extensible**: Easy to add new registries or features
|
||||||
|
- **Testable**: Comprehensive test suite in isolation
|
||||||
|
|
||||||
|
### Integration
|
||||||
|
- **CLI integration**: Direct command-line access
|
||||||
|
- **Makefile integration**: Convenient targets for workflows
|
||||||
|
- **Configuration**: Centralized in pyproject.toml
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### Core Dependencies
|
||||||
|
- `click>=8.0.0` - CLI framework
|
||||||
|
- `requests>=2.25.0` - HTTP client for registries
|
||||||
|
- `setuptools-scm>=8.0.0` - Version management
|
||||||
|
- `build>=0.8.0` - Package building
|
||||||
|
- `packaging>=21.0` - Version parsing and validation
|
||||||
|
|
||||||
|
### Development Dependencies
|
||||||
|
- `pytest>=7.0.0` - Testing framework
|
||||||
|
- `pytest-cov>=4.0.0` - Coverage reporting
|
||||||
|
- `responses>=0.20.0` - HTTP mocking for tests
|
||||||
|
- `black>=22.0.0` - Code formatting
|
||||||
|
- `flake8>=5.0.0` - Code linting
|
||||||
|
- `mypy>=1.0.0` - Type checking
|
||||||
|
|
||||||
|
## Compliance
|
||||||
|
|
||||||
|
This capability follows the ComposableRepositoryParadigm:
|
||||||
|
- ✅ Src layout (PEP 660 compliant)
|
||||||
|
- ✅ Unidirectional dependencies
|
||||||
|
- ✅ Self-contained with own tests
|
||||||
|
- ✅ Independent configuration
|
||||||
|
- ✅ Clean API boundaries
|
||||||
|
- ✅ Type safety with mypy
|
||||||
|
- ✅ Comprehensive documentation
|
||||||
229
capabilities/release-management/docs/package_publishing.md
Normal file
229
capabilities/release-management/docs/package_publishing.md
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
# Package Publishing Guide
|
||||||
|
|
||||||
|
This guide covers building, publishing, and distributing MarkiTect packages using our Gitea package registry and setuptools-scm version management.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. **Gitea API Token**: Set the `GITEA_API_TOKEN` environment variable with your Gitea API token
|
||||||
|
2. **Repository Access**: The token must have write access to the repository's package registry
|
||||||
|
|
||||||
|
## Quick Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set your Gitea API token
|
||||||
|
export GITEA_API_TOKEN="your_gitea_api_token_here"
|
||||||
|
|
||||||
|
# Or add it to your shell profile
|
||||||
|
echo "export GITEA_API_TOKEN=your_token" >> ~/.bashrc
|
||||||
|
```
|
||||||
|
|
||||||
|
## Package Building
|
||||||
|
|
||||||
|
### Quick Package Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build distribution packages (recommended)
|
||||||
|
make package
|
||||||
|
|
||||||
|
# This will:
|
||||||
|
# 1. Show current version (setuptools-scm)
|
||||||
|
# 2. Clean previous builds
|
||||||
|
# 3. Build both wheel and source distribution
|
||||||
|
# 4. Show package details
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Standard build
|
||||||
|
make build
|
||||||
|
|
||||||
|
# Using release script
|
||||||
|
make release-build
|
||||||
|
python release.py build
|
||||||
|
|
||||||
|
# Manual Python build
|
||||||
|
python -m build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Publishing Workflow
|
||||||
|
|
||||||
|
### Complete Release + Publishing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 🚀 ONE-COMMAND RELEASE (recommended)
|
||||||
|
make release-publish-gitea VERSION=0.8.0
|
||||||
|
|
||||||
|
# This complete workflow:
|
||||||
|
# 1. Creates git tag v0.8.0
|
||||||
|
# 2. Builds packages (setuptools-scm uses tag for version 0.8.0)
|
||||||
|
# 3. Uploads both wheel and source distribution to Gitea
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step-by-Step Release
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Check current status
|
||||||
|
make release-status
|
||||||
|
|
||||||
|
# 2. Validate release readiness
|
||||||
|
make release-validate
|
||||||
|
|
||||||
|
# 3. Create git tag
|
||||||
|
make release-tag VERSION=0.8.0
|
||||||
|
|
||||||
|
# 4. Build packages (version auto-detected from tag)
|
||||||
|
make release-build
|
||||||
|
|
||||||
|
# 5. Upload to Gitea registry
|
||||||
|
make release-upload-gitea
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development Package Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build current development version
|
||||||
|
make package
|
||||||
|
|
||||||
|
# Upload development packages for testing
|
||||||
|
python release.py upload --dry-run # Test first
|
||||||
|
python release.py upload # Upload development version
|
||||||
|
```
|
||||||
|
|
||||||
|
## Registry Management
|
||||||
|
|
||||||
|
### Check Registry Status
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Comprehensive registry information
|
||||||
|
make release-registry
|
||||||
|
|
||||||
|
# Shows:
|
||||||
|
# - Authentication status
|
||||||
|
# - Registry URLs
|
||||||
|
# - Existing packages
|
||||||
|
# - Configuration details
|
||||||
|
```
|
||||||
|
|
||||||
|
### Upload Existing Packages
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Upload packages in dist/ folder
|
||||||
|
make release-upload-gitea
|
||||||
|
|
||||||
|
# With dry-run testing
|
||||||
|
python release.py upload --dry-run
|
||||||
|
python release.py upload
|
||||||
|
```
|
||||||
|
|
||||||
|
### Traditional Release (Git tags only)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Standard release without Gitea upload
|
||||||
|
make release-publish VERSION=0.8.0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Commands
|
||||||
|
|
||||||
|
### Makefile Targets
|
||||||
|
|
||||||
|
- `make release-registry` - Show Gitea package registry information
|
||||||
|
- `make release-upload-gitea` - Upload existing packages to Gitea
|
||||||
|
- `make release-publish-gitea VERSION=x.y.z` - Complete release + Gitea upload
|
||||||
|
|
||||||
|
### Python Script Commands
|
||||||
|
|
||||||
|
- `python release.py registry` - Show registry information
|
||||||
|
- `python release.py upload` - Upload packages to Gitea
|
||||||
|
- `python release.py upload --dry-run` - Test upload without uploading
|
||||||
|
- `python release.py publish --version x.y.z --to-gitea` - Release with Gitea upload
|
||||||
|
|
||||||
|
## Registry Information
|
||||||
|
|
||||||
|
- **Gitea URL**: http://92.205.130.254:32166
|
||||||
|
- **Repository**: coulomb/markitect_project
|
||||||
|
- **PyPI Registry URL**: http://92.205.130.254:32166/api/packages/coulomb/pypi
|
||||||
|
- **Package List URL**: http://92.205.130.254:32166/api/v1/packages/coulomb
|
||||||
|
|
||||||
|
## Installing from Gitea Registry
|
||||||
|
|
||||||
|
Once packages are published, users can install them using:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install from Gitea registry
|
||||||
|
pip install markitect --extra-index-url http://92.205.130.254:32166/api/packages/coulomb/pypi/simple/
|
||||||
|
|
||||||
|
# Or configure pip permanently
|
||||||
|
mkdir -p ~/.pip
|
||||||
|
cat >> ~/.pip/pip.conf << EOF
|
||||||
|
[global]
|
||||||
|
extra-index-url = http://92.205.130.254:32166/api/packages/coulomb/pypi/simple/
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Automatic Package Detection
|
||||||
|
|
||||||
|
The system automatically detects and uploads:
|
||||||
|
- **Wheel files** (`.whl`) - Binary distributions
|
||||||
|
- **Source distributions** (`.tar.gz`) - Source code packages
|
||||||
|
|
||||||
|
### Version Management with setuptools-scm
|
||||||
|
|
||||||
|
Versions are automatically determined by git tags:
|
||||||
|
- `v0.8.0` tag → `0.8.0` package version
|
||||||
|
- Development commits → `0.8.1.dev3+gcommithash` versions
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
The system provides detailed error messages for:
|
||||||
|
- Missing authentication tokens
|
||||||
|
- Network connectivity issues
|
||||||
|
- Package upload failures
|
||||||
|
- Invalid package formats
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Authentication Issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if token is set
|
||||||
|
echo $GITEA_API_TOKEN
|
||||||
|
|
||||||
|
# Test authentication
|
||||||
|
python release.py registry
|
||||||
|
```
|
||||||
|
|
||||||
|
### Upload Failures
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test with dry run first
|
||||||
|
python release.py upload --dry-run
|
||||||
|
|
||||||
|
# Check package files exist
|
||||||
|
ls -la dist/
|
||||||
|
|
||||||
|
# Rebuild packages if needed
|
||||||
|
make release-build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Network Issues
|
||||||
|
|
||||||
|
- Ensure Gitea server is accessible: `ping 92.205.130.254`
|
||||||
|
- Check firewall and proxy settings
|
||||||
|
- Verify Gitea is running on port 32166
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
The package registry functionality is implemented in:
|
||||||
|
- `gitea/package_registry.py` - Main package registry client
|
||||||
|
- `release.py` - Release script with Gitea integration
|
||||||
|
- `Makefile` - Convenient targets for package management
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
- Never commit API tokens to version control
|
||||||
|
- Use environment variables or secure credential storage
|
||||||
|
- Tokens should have minimal required permissions
|
||||||
|
- Rotate tokens regularly for security
|
||||||
309
capabilities/release-management/docs/version_management.md
Normal file
309
capabilities/release-management/docs/version_management.md
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
# Version Management Guide
|
||||||
|
|
||||||
|
MarkiTect uses **setuptools-scm** for automatic version management based on git tags. This eliminates manual version bumping and ensures versions are always in sync with git history.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### Version Calculation
|
||||||
|
|
||||||
|
setuptools-scm automatically determines the version based on:
|
||||||
|
|
||||||
|
1. **Git Tags**: The latest tag matching `v*` pattern (e.g., `v0.7.0`)
|
||||||
|
2. **Commits Since Tag**: Number of commits since the latest tag
|
||||||
|
3. **Current Commit**: Short commit hash
|
||||||
|
4. **Dirty State**: Whether there are uncommitted changes
|
||||||
|
|
||||||
|
### Version Examples
|
||||||
|
|
||||||
|
| Git State | Version Output |
|
||||||
|
|-----------|----------------|
|
||||||
|
| `v0.7.0` tag (clean) | `0.7.0` |
|
||||||
|
| `v0.7.0` + 3 commits | `0.7.1.dev3+g1a2b3c4d` |
|
||||||
|
| `v0.7.0` + 3 commits + dirty | `0.7.1.dev3+g1a2b3c4d.d20251108` |
|
||||||
|
| No tags + 10 commits | `0.1.dev10+g5e6f7g8h` |
|
||||||
|
|
||||||
|
## Version Commands
|
||||||
|
|
||||||
|
### Check Current Version
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Quick version check
|
||||||
|
python -m setuptools_scm
|
||||||
|
|
||||||
|
# Detailed version information
|
||||||
|
make release-status
|
||||||
|
python release.py status
|
||||||
|
```
|
||||||
|
|
||||||
|
### Access Version in Code
|
||||||
|
|
||||||
|
```python
|
||||||
|
from markitect.__version__ import __version__
|
||||||
|
print(f"MarkiTect version: {__version__}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Creating Releases
|
||||||
|
|
||||||
|
### Release Workflow
|
||||||
|
|
||||||
|
1. **Ensure Clean State**:
|
||||||
|
```bash
|
||||||
|
git status # Should be clean
|
||||||
|
git pull # Latest changes
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Validate Release Readiness**:
|
||||||
|
```bash
|
||||||
|
make release-validate
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Create Release**:
|
||||||
|
```bash
|
||||||
|
# Standard release
|
||||||
|
make release-publish VERSION=0.8.0
|
||||||
|
|
||||||
|
# Release with Gitea publishing
|
||||||
|
make release-publish-gitea VERSION=0.8.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Version Naming Conventions
|
||||||
|
|
||||||
|
Follow [Semantic Versioning](https://semver.org/):
|
||||||
|
|
||||||
|
- **Major Version** (`1.0.0`): Breaking changes
|
||||||
|
- **Minor Version** (`0.8.0`): New features (backward compatible)
|
||||||
|
- **Patch Version** (`0.7.1`): Bug fixes (backward compatible)
|
||||||
|
- **Pre-release** (`0.8.0-rc1`): Release candidates
|
||||||
|
- **Development** (`0.8.1.dev3+hash`): Automatic between releases
|
||||||
|
|
||||||
|
### Git Tag Format
|
||||||
|
|
||||||
|
Always use the format `vX.Y.Z`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Correct
|
||||||
|
git tag v0.8.0
|
||||||
|
git tag v1.0.0-rc1
|
||||||
|
|
||||||
|
# Incorrect
|
||||||
|
git tag 0.8.0 # Missing 'v' prefix
|
||||||
|
git tag version-0.8.0 # Wrong format
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Versions
|
||||||
|
|
||||||
|
### Understanding Development Versions
|
||||||
|
|
||||||
|
Between releases, setuptools-scm generates development versions:
|
||||||
|
|
||||||
|
```
|
||||||
|
0.7.1.dev3+g1a2b3c4d.d20251108
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ │ │ │ └── Date (dirty state)
|
||||||
|
│ │ │ │ └─────────── Commit hash
|
||||||
|
│ │ │ └───────────── Commits since tag
|
||||||
|
│ │ └──────────────── Dev marker
|
||||||
|
│ └─────────────────── Next version
|
||||||
|
└─────────────────────── Base version
|
||||||
|
```
|
||||||
|
|
||||||
|
### Working with Development Versions
|
||||||
|
|
||||||
|
Development versions are automatically:
|
||||||
|
- **Sorted correctly** by pip (dev versions < release versions)
|
||||||
|
- **Excluded from releases** (only tagged versions are released)
|
||||||
|
- **Unique** (each commit has a different version)
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### setuptools-scm Configuration
|
||||||
|
|
||||||
|
Configuration in `pyproject.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[tool.setuptools_scm]
|
||||||
|
write_to = "markitect/_version.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Version File Generation
|
||||||
|
|
||||||
|
setuptools-scm automatically generates `markitect/_version.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Auto-generated - do not edit
|
||||||
|
__version__ = "0.7.1.dev3+g1a2b3c4d"
|
||||||
|
__version_tuple__ = (0, 7, 1, 'dev3', 'g1a2b3c4d')
|
||||||
|
```
|
||||||
|
|
||||||
|
This file is:
|
||||||
|
- ✅ **Auto-generated** during package builds
|
||||||
|
- ✅ **Added to .gitignore** (never committed)
|
||||||
|
- ✅ **Available at runtime** for version checks
|
||||||
|
|
||||||
|
## Release Branches
|
||||||
|
|
||||||
|
### Main Branch Strategy
|
||||||
|
|
||||||
|
MarkiTect uses a simple branching strategy:
|
||||||
|
|
||||||
|
- **`main`**: Primary development branch
|
||||||
|
- **Tags**: Mark release points (`v0.7.0`, `v0.8.0`)
|
||||||
|
- **Feature branches**: Merged via pull requests
|
||||||
|
|
||||||
|
### Release Process
|
||||||
|
|
||||||
|
1. **Development** happens on `main`
|
||||||
|
2. **Release tags** created on `main` when ready
|
||||||
|
3. **Hotfix tags** can be created on older commits if needed
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Standard release from main
|
||||||
|
git checkout main
|
||||||
|
git pull
|
||||||
|
make release-publish-gitea VERSION=0.8.0
|
||||||
|
|
||||||
|
# Hotfix release from older commit
|
||||||
|
git checkout v0.7.0
|
||||||
|
git cherry-pick <hotfix-commit>
|
||||||
|
git tag v0.7.1
|
||||||
|
git push origin v0.7.1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Package Building
|
||||||
|
|
||||||
|
### Build Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build packages with version info
|
||||||
|
make package
|
||||||
|
|
||||||
|
# Build using release script
|
||||||
|
make release-build
|
||||||
|
python release.py build
|
||||||
|
|
||||||
|
# Manual build
|
||||||
|
python -m build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build Output
|
||||||
|
|
||||||
|
Packages are built to `dist/` directory:
|
||||||
|
- **Wheel** (`.whl`): Binary distribution
|
||||||
|
- **Source Distribution** (`.tar.gz`): Source code
|
||||||
|
|
||||||
|
### Version in Package Names
|
||||||
|
|
||||||
|
setuptools-scm ensures package names include correct versions:
|
||||||
|
|
||||||
|
```
|
||||||
|
dist/
|
||||||
|
├── markitect-0.8.0-py3-none-any.whl # Release
|
||||||
|
├── markitect-0.8.0.tar.gz # Release
|
||||||
|
├── markitect-0.8.1.dev3+hash-py3-none-any.whl # Development
|
||||||
|
└── markitect-0.8.1.dev3+hash.tar.gz # Development
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **"No tags found"**:
|
||||||
|
```bash
|
||||||
|
# Create initial tag
|
||||||
|
git tag v0.1.0
|
||||||
|
git push origin v0.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **"Dirty working tree"**:
|
||||||
|
```bash
|
||||||
|
# Commit or stash changes
|
||||||
|
git add . && git commit -m "Changes"
|
||||||
|
# Or
|
||||||
|
git stash
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **"Version not updating"**:
|
||||||
|
```bash
|
||||||
|
# Clear setuptools-scm cache
|
||||||
|
rm -rf build/ *.egg-info/
|
||||||
|
python -m setuptools_scm
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **"Import error in __version__.py"**:
|
||||||
|
```bash
|
||||||
|
# Rebuild package to generate _version.py
|
||||||
|
make package
|
||||||
|
```
|
||||||
|
|
||||||
|
### Debug Version Issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verbose setuptools-scm output
|
||||||
|
python -m setuptools_scm --debug
|
||||||
|
|
||||||
|
# Check git state
|
||||||
|
git describe --tags --dirty --always
|
||||||
|
|
||||||
|
# Verify tag format
|
||||||
|
git tag --list | grep "^v"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Do's ✅
|
||||||
|
|
||||||
|
- **Always use `vX.Y.Z` tag format**
|
||||||
|
- **Create annotated tags**: `git tag -a v0.8.0 -m "Release 0.8.0"`
|
||||||
|
- **Push tags to origin**: `git push origin v0.8.0`
|
||||||
|
- **Keep clean working tree** for releases
|
||||||
|
- **Follow semantic versioning**
|
||||||
|
- **Test version detection** before releasing
|
||||||
|
|
||||||
|
### Don'ts ❌
|
||||||
|
|
||||||
|
- **Don't edit `_version.py`** manually (auto-generated)
|
||||||
|
- **Don't commit version numbers** to source files
|
||||||
|
- **Don't use non-standard tag formats**
|
||||||
|
- **Don't create releases from dirty tree**
|
||||||
|
- **Don't delete old tags** (breaks version history)
|
||||||
|
|
||||||
|
### Version Strategy
|
||||||
|
|
||||||
|
1. **Development**: Let setuptools-scm handle automatically
|
||||||
|
2. **Pre-releases**: Use `-rc1`, `-alpha1`, `-beta1` suffixes
|
||||||
|
3. **Releases**: Create tags only for stable releases
|
||||||
|
4. **Hotfixes**: Tag from appropriate commit, not necessarily `main`
|
||||||
|
|
||||||
|
## Integration with CI/CD
|
||||||
|
|
||||||
|
### GitHub Actions Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Release
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: ['v*']
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # Important for setuptools-scm
|
||||||
|
|
||||||
|
- name: Build packages
|
||||||
|
run: make package
|
||||||
|
|
||||||
|
- name: Upload to Gitea
|
||||||
|
env:
|
||||||
|
GITEA_API_TOKEN: ${{ secrets.GITEA_API_TOKEN }}
|
||||||
|
run: python release.py upload
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Points
|
||||||
|
|
||||||
|
- **`fetch-depth: 0`**: Required for setuptools-scm to access git history
|
||||||
|
- **Environment variables**: Use secrets for API tokens
|
||||||
|
- **Tag-based triggers**: Only build releases for version tags
|
||||||
|
|
||||||
|
This version management system provides automatic, reliable, and traceable versioning that scales with your development workflow.
|
||||||
236
capabilities/release-management/release.mk
Normal file
236
capabilities/release-management/release.mk
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
# Release Management Makefile Integration
|
||||||
|
# Include this file in your main Makefile to add release management capabilities
|
||||||
|
#
|
||||||
|
# Usage: include capabilities/release-management/release.mk
|
||||||
|
|
||||||
|
# Release Management Variables
|
||||||
|
RELEASE_MANAGEMENT_PATH := capabilities/release-management
|
||||||
|
RELEASE_CLI := release
|
||||||
|
|
||||||
|
# Check if release management capability is available
|
||||||
|
RELEASE_AVAILABLE := $(shell command -v $(RELEASE_CLI) 2> /dev/null)
|
||||||
|
|
||||||
|
# Release Status and Information
|
||||||
|
.PHONY: release-status
|
||||||
|
release-status: ## Show current release status and version information
|
||||||
|
ifndef RELEASE_AVAILABLE
|
||||||
|
@echo "❌ Release management capability not installed"
|
||||||
|
@echo " Install with: pip install -e $(RELEASE_MANAGEMENT_PATH)/"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
$(RELEASE_CLI) status
|
||||||
|
|
||||||
|
.PHONY: release-validate
|
||||||
|
release-validate: ## Validate repository state for release readiness
|
||||||
|
ifndef RELEASE_AVAILABLE
|
||||||
|
@echo "❌ Release management capability not installed"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
$(RELEASE_CLI) validate
|
||||||
|
|
||||||
|
.PHONY: release-registry-info
|
||||||
|
release-registry-info: ## Show package registry information and status
|
||||||
|
ifndef RELEASE_AVAILABLE
|
||||||
|
@echo "❌ Release management capability not installed"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
$(RELEASE_CLI) registry-info
|
||||||
|
|
||||||
|
# Git Tag Management
|
||||||
|
.PHONY: release-tag
|
||||||
|
release-tag: ## Create git tag for version (requires VERSION=x.y.z)
|
||||||
|
ifndef VERSION
|
||||||
|
@echo "❌ VERSION is required. Usage: make release-tag VERSION=1.0.0"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
ifndef RELEASE_AVAILABLE
|
||||||
|
@echo "❌ Release management capability not installed"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
$(RELEASE_CLI) tag --version $(VERSION)
|
||||||
|
|
||||||
|
# Package Building
|
||||||
|
.PHONY: release-build
|
||||||
|
release-build: ## Build release packages using setuptools-scm
|
||||||
|
ifndef RELEASE_AVAILABLE
|
||||||
|
@echo "❌ Release management capability not installed"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
$(RELEASE_CLI) build
|
||||||
|
|
||||||
|
.PHONY: release-clean
|
||||||
|
release-clean: ## Clean build artifacts and temporary files
|
||||||
|
ifndef RELEASE_AVAILABLE
|
||||||
|
@echo "❌ Release management capability not installed"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
$(RELEASE_CLI) clean
|
||||||
|
|
||||||
|
# Publishing Workflows
|
||||||
|
.PHONY: release-publish
|
||||||
|
release-publish: ## Complete release workflow: tag + build (requires VERSION=x.y.z)
|
||||||
|
ifndef VERSION
|
||||||
|
@echo "❌ VERSION is required. Usage: make release-publish VERSION=1.0.0"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
ifndef RELEASE_AVAILABLE
|
||||||
|
@echo "❌ Release management capability not installed"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
$(RELEASE_CLI) publish --version $(VERSION)
|
||||||
|
|
||||||
|
.PHONY: release-publish-gitea
|
||||||
|
release-publish-gitea: ## Complete release workflow + Gitea upload (requires VERSION=x.y.z)
|
||||||
|
ifndef VERSION
|
||||||
|
@echo "❌ VERSION is required. Usage: make release-publish-gitea VERSION=1.0.0"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
ifndef RELEASE_AVAILABLE
|
||||||
|
@echo "❌ Release management capability not installed"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
$(RELEASE_CLI) publish --version $(VERSION) --registry gitea
|
||||||
|
|
||||||
|
.PHONY: release-publish-pypi
|
||||||
|
release-publish-pypi: ## Complete release workflow + PyPI upload (requires VERSION=x.y.z)
|
||||||
|
ifndef VERSION
|
||||||
|
@echo "❌ VERSION is required. Usage: make release-publish-pypi VERSION=1.0.0"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
ifndef RELEASE_AVAILABLE
|
||||||
|
@echo "❌ Release management capability not installed"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
$(RELEASE_CLI) publish --version $(VERSION) --registry pypi
|
||||||
|
|
||||||
|
# Upload Existing Packages
|
||||||
|
.PHONY: release-upload-gitea
|
||||||
|
release-upload-gitea: ## Upload existing packages to Gitea registry
|
||||||
|
ifndef RELEASE_AVAILABLE
|
||||||
|
@echo "❌ Release management capability not installed"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
$(RELEASE_CLI) upload --registry gitea
|
||||||
|
|
||||||
|
.PHONY: release-upload-pypi
|
||||||
|
release-upload-pypi: ## Upload existing packages to PyPI
|
||||||
|
ifndef RELEASE_AVAILABLE
|
||||||
|
@echo "❌ Release management capability not installed"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
$(RELEASE_CLI) upload --registry pypi
|
||||||
|
|
||||||
|
.PHONY: release-upload-testpypi
|
||||||
|
release-upload-testpypi: ## Upload existing packages to Test PyPI
|
||||||
|
ifndef RELEASE_AVAILABLE
|
||||||
|
@echo "❌ Release management capability not installed"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
$(RELEASE_CLI) upload --registry testpypi
|
||||||
|
|
||||||
|
# Dry Run Options
|
||||||
|
.PHONY: release-publish-dry-run
|
||||||
|
release-publish-dry-run: ## Dry run of complete release workflow (requires VERSION=x.y.z)
|
||||||
|
ifndef VERSION
|
||||||
|
@echo "❌ VERSION is required. Usage: make release-publish-dry-run VERSION=1.0.0"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
ifndef RELEASE_AVAILABLE
|
||||||
|
@echo "❌ Release management capability not installed"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
$(RELEASE_CLI) publish --version $(VERSION) --dry-run
|
||||||
|
|
||||||
|
.PHONY: release-upload-dry-run
|
||||||
|
release-upload-dry-run: ## Dry run of package upload to default registry
|
||||||
|
ifndef RELEASE_AVAILABLE
|
||||||
|
@echo "❌ Release management capability not installed"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
$(RELEASE_CLI) upload --dry-run
|
||||||
|
|
||||||
|
# Development and Setup
|
||||||
|
.PHONY: release-management-install
|
||||||
|
release-management-install: ## Install release management capability
|
||||||
|
pip install -e $(RELEASE_MANAGEMENT_PATH)/
|
||||||
|
|
||||||
|
.PHONY: release-management-install-dev
|
||||||
|
release-management-install-dev: ## Install release management capability with dev dependencies
|
||||||
|
pip install -e "$(RELEASE_MANAGEMENT_PATH)/[dev]"
|
||||||
|
|
||||||
|
.PHONY: release-management-test
|
||||||
|
release-management-test: ## Run release management capability tests
|
||||||
|
cd $(RELEASE_MANAGEMENT_PATH) && pytest tests/
|
||||||
|
|
||||||
|
.PHONY: release-management-help
|
||||||
|
release-management-help: ## Show release management CLI help
|
||||||
|
ifndef RELEASE_AVAILABLE
|
||||||
|
@echo "❌ Release management capability not installed"
|
||||||
|
@echo " Install with: make release-management-install"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
$(RELEASE_CLI) --help
|
||||||
|
|
||||||
|
# Help target integration
|
||||||
|
.PHONY: help-release
|
||||||
|
help-release: ## Show release management specific help
|
||||||
|
@echo ""
|
||||||
|
@echo "📦 Release Management:"
|
||||||
|
@echo " release-status Show current release status and version information"
|
||||||
|
@echo " release-validate Validate repository state for release readiness"
|
||||||
|
@echo " release-registry-info Show package registry information and status"
|
||||||
|
@echo ""
|
||||||
|
@echo "🏷️ Git Tag Management:"
|
||||||
|
@echo " release-tag VERSION=x.y.z Create git tag for version"
|
||||||
|
@echo ""
|
||||||
|
@echo "🔨 Package Building:"
|
||||||
|
@echo " release-build Build release packages using setuptools-scm"
|
||||||
|
@echo " release-clean Clean build artifacts and temporary files"
|
||||||
|
@echo ""
|
||||||
|
@echo "🚀 Publishing Workflows:"
|
||||||
|
@echo " release-publish VERSION=x.y.z Complete release workflow (tag + build)"
|
||||||
|
@echo " release-publish-gitea VERSION=x.y.z Complete release + Gitea upload"
|
||||||
|
@echo " release-publish-pypi VERSION=x.y.z Complete release + PyPI upload"
|
||||||
|
@echo ""
|
||||||
|
@echo "📤 Upload Existing Packages:"
|
||||||
|
@echo " release-upload-gitea Upload existing packages to Gitea registry"
|
||||||
|
@echo " release-upload-pypi Upload existing packages to PyPI"
|
||||||
|
@echo " release-upload-testpypi Upload existing packages to Test PyPI"
|
||||||
|
@echo ""
|
||||||
|
@echo "🧪 Dry Run Options:"
|
||||||
|
@echo " release-publish-dry-run VERSION=x.y.z Dry run of release workflow"
|
||||||
|
@echo " release-upload-dry-run Dry run of package upload"
|
||||||
|
@echo ""
|
||||||
|
@echo "⚙️ Development and Setup:"
|
||||||
|
@echo " release-management-install Install release management capability"
|
||||||
|
@echo " release-management-install-dev Install with development dependencies"
|
||||||
|
@echo " release-management-test Run capability tests"
|
||||||
|
@echo " release-management-help Show CLI help"
|
||||||
|
@echo ""
|
||||||
|
|
||||||
|
# Default registry shortcuts (can be overridden)
|
||||||
|
RELEASE_DEFAULT_REGISTRY ?= gitea
|
||||||
|
|
||||||
|
.PHONY: release-upload
|
||||||
|
release-upload: release-upload-$(RELEASE_DEFAULT_REGISTRY) ## Upload packages to default registry ($(RELEASE_DEFAULT_REGISTRY))
|
||||||
|
|
||||||
|
# Integration with main project targets
|
||||||
|
# These can be overridden in main Makefile if different behavior is needed
|
||||||
|
|
||||||
|
.PHONY: package
|
||||||
|
package: release-build ## Build packages (alias for release-build)
|
||||||
|
|
||||||
|
.PHONY: publish
|
||||||
|
publish: ## Publish release to default registry (requires VERSION=x.y.z)
|
||||||
|
ifndef VERSION
|
||||||
|
@echo "❌ VERSION is required. Usage: make publish VERSION=1.0.0"
|
||||||
|
@exit 1
|
||||||
|
endif
|
||||||
|
@make release-publish-$(RELEASE_DEFAULT_REGISTRY) VERSION=$(VERSION)
|
||||||
|
|
||||||
|
# Legacy compatibility targets
|
||||||
|
.PHONY: release-status-legacy
|
||||||
|
release-status-legacy: release-status ## Legacy alias for release-status
|
||||||
|
|
||||||
|
.PHONY: package-upload
|
||||||
|
package-upload: release-upload ## Legacy alias for release-upload
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
"""
|
||||||
|
Release Management Capability
|
||||||
|
|
||||||
|
A comprehensive release management system for Python projects providing:
|
||||||
|
- Automatic version management with setuptools-scm
|
||||||
|
- Package building and distribution
|
||||||
|
- Multi-platform publishing (Gitea, PyPI, etc.)
|
||||||
|
- Git tag-based release workflows
|
||||||
|
- CLI tools for release automation
|
||||||
|
|
||||||
|
Main Components:
|
||||||
|
- ReleaseManager: Orchestrates complete release workflows
|
||||||
|
- PackageBuilder: Handles package generation and building
|
||||||
|
- PublishManager: Manages package publication to registries
|
||||||
|
- GitManager: Git operations for releases
|
||||||
|
- Registry Support: Gitea, PyPI, and extensible registry system
|
||||||
|
|
||||||
|
Quick Start:
|
||||||
|
from release_management import ReleaseManager
|
||||||
|
|
||||||
|
manager = ReleaseManager()
|
||||||
|
success = manager.publish_release("1.0.0")
|
||||||
|
|
||||||
|
CLI Usage:
|
||||||
|
release status
|
||||||
|
release publish --version 1.0.0
|
||||||
|
release upload --registry gitea
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .core.manager import ReleaseManager
|
||||||
|
from .core.builder import PackageBuilder
|
||||||
|
from .core.publisher import PublishManager
|
||||||
|
from .git.manager import GitManager
|
||||||
|
from .registries.factory import RegistryFactory
|
||||||
|
from .registries.gitea.registry import GiteaRegistry
|
||||||
|
from .utils.version import VersionManager
|
||||||
|
from .utils.validation import ReleaseValidator
|
||||||
|
|
||||||
|
# Version is managed in pyproject.toml
|
||||||
|
__version__ = "0.1.0"
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
# Core classes
|
||||||
|
"ReleaseManager",
|
||||||
|
"PackageBuilder",
|
||||||
|
"PublishManager",
|
||||||
|
"GitManager",
|
||||||
|
|
||||||
|
# Registry support
|
||||||
|
"RegistryFactory",
|
||||||
|
"GiteaRegistry",
|
||||||
|
|
||||||
|
# Utilities
|
||||||
|
"VersionManager",
|
||||||
|
"ReleaseValidator",
|
||||||
|
|
||||||
|
# Version
|
||||||
|
"__version__",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Package metadata
|
||||||
|
__title__ = "release-management"
|
||||||
|
__description__ = "Comprehensive release management capability for Python projects"
|
||||||
|
__author__ = "MarkiTect Project"
|
||||||
|
__license__ = "MIT"
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
"""
|
||||||
|
Command-line interface for release management.
|
||||||
|
|
||||||
|
This module provides CLI commands for release operations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .main import main
|
||||||
|
|
||||||
|
__all__ = ["main"]
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
"""
|
||||||
|
Main CLI entry point for release management.
|
||||||
|
|
||||||
|
This module provides the main CLI interface adapted from the original release.py script.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import click
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from ..core.manager import ReleaseManager
|
||||||
|
from ..utils.version import VersionManager
|
||||||
|
|
||||||
|
|
||||||
|
@click.group(invoke_without_command=True)
|
||||||
|
@click.option('--dry-run', is_flag=True, help='Show what would be done without making changes')
|
||||||
|
@click.option('--force', is_flag=True, help='Force operation even with warnings')
|
||||||
|
@click.option('--project-root', type=click.Path(exists=True, path_type=Path),
|
||||||
|
help='Project root directory')
|
||||||
|
@click.pass_context
|
||||||
|
def main(ctx, dry_run: bool, force: bool, project_root: Optional[Path]):
|
||||||
|
"""Release management CLI for Python projects."""
|
||||||
|
ctx.ensure_object(dict)
|
||||||
|
ctx.obj['dry_run'] = dry_run
|
||||||
|
ctx.obj['force'] = force
|
||||||
|
ctx.obj['project_root'] = project_root
|
||||||
|
|
||||||
|
# If no command specified, show status
|
||||||
|
if ctx.invoked_subcommand is None:
|
||||||
|
ctx.invoke(status)
|
||||||
|
|
||||||
|
|
||||||
|
@main.command()
|
||||||
|
@click.pass_context
|
||||||
|
def status(ctx):
|
||||||
|
"""Show current release status and version information."""
|
||||||
|
manager = ReleaseManager(
|
||||||
|
project_root=ctx.obj['project_root'],
|
||||||
|
dry_run=ctx.obj['dry_run'],
|
||||||
|
force=ctx.obj['force']
|
||||||
|
)
|
||||||
|
|
||||||
|
print("🔍 Release Status")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
status_info = manager.get_release_status()
|
||||||
|
|
||||||
|
# Version information
|
||||||
|
print(f"Current Version: {status_info['version']}")
|
||||||
|
|
||||||
|
# Git information
|
||||||
|
if status_info.get('is_repo'):
|
||||||
|
print(f"Git Branch: {status_info['branch']}")
|
||||||
|
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'}")
|
||||||
|
else:
|
||||||
|
print("Git Repository: Not available")
|
||||||
|
|
||||||
|
# Package information
|
||||||
|
packages = status_info['packages']
|
||||||
|
print(f"\\nBuilt Packages: {packages['total_count']} files")
|
||||||
|
if packages['wheels']:
|
||||||
|
print(" Wheels:")
|
||||||
|
for wheel in packages['wheels']:
|
||||||
|
print(f" - {wheel}")
|
||||||
|
if packages['sdists']:
|
||||||
|
print(" Source Distributions:")
|
||||||
|
for sdist in packages['sdists']:
|
||||||
|
print(f" - {sdist}")
|
||||||
|
|
||||||
|
# Validation status
|
||||||
|
validation = status_info['validation']
|
||||||
|
if validation['is_valid']:
|
||||||
|
print("\\n✅ Repository is ready for release")
|
||||||
|
else:
|
||||||
|
print("\\n❌ Release validation issues:")
|
||||||
|
for issue in validation['issues']:
|
||||||
|
print(f" - {issue}")
|
||||||
|
|
||||||
|
|
||||||
|
@main.command()
|
||||||
|
@click.pass_context
|
||||||
|
def validate(ctx):
|
||||||
|
"""Validate repository state for release readiness."""
|
||||||
|
manager = ReleaseManager(
|
||||||
|
project_root=ctx.obj['project_root'],
|
||||||
|
dry_run=ctx.obj['dry_run'],
|
||||||
|
force=ctx.obj['force']
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
@main.command()
|
||||||
|
@click.option('--version', required=True, help='Version to tag (e.g., 0.8.0)')
|
||||||
|
@click.option('--message', help='Tag message')
|
||||||
|
@click.pass_context
|
||||||
|
def tag(ctx, version: str, message: Optional[str]):
|
||||||
|
"""Create git tag for version."""
|
||||||
|
manager = ReleaseManager(
|
||||||
|
project_root=ctx.obj['project_root'],
|
||||||
|
dry_run=ctx.obj['dry_run'],
|
||||||
|
force=ctx.obj['force']
|
||||||
|
)
|
||||||
|
|
||||||
|
if manager.create_tag(version, message):
|
||||||
|
print(f"✅ Successfully created tag for version {version}")
|
||||||
|
else:
|
||||||
|
print(f"❌ Failed to create tag for version {version}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@main.command()
|
||||||
|
@click.pass_context
|
||||||
|
def build(ctx):
|
||||||
|
"""Build release packages using setuptools-scm."""
|
||||||
|
manager = ReleaseManager(
|
||||||
|
project_root=ctx.obj['project_root'],
|
||||||
|
dry_run=ctx.obj['dry_run'],
|
||||||
|
force=ctx.obj['force']
|
||||||
|
)
|
||||||
|
|
||||||
|
if manager.build_packages():
|
||||||
|
print("✅ Packages built successfully")
|
||||||
|
else:
|
||||||
|
print("❌ Package build failed")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@main.command()
|
||||||
|
@click.option('--version', required=True, help='Version to publish (e.g., 0.8.0)')
|
||||||
|
@click.option('--registry', default='gitea', help='Registry type (gitea, pypi, etc.)')
|
||||||
|
@click.option('--skip-build', is_flag=True, help='Skip building and use existing packages')
|
||||||
|
@click.pass_context
|
||||||
|
def publish(ctx, version: str, registry: str, skip_build: bool):
|
||||||
|
"""Complete release workflow: tag, build, and publish."""
|
||||||
|
manager = ReleaseManager(
|
||||||
|
project_root=ctx.obj['project_root'],
|
||||||
|
dry_run=ctx.obj['dry_run'],
|
||||||
|
force=ctx.obj['force']
|
||||||
|
)
|
||||||
|
|
||||||
|
if manager.publish_with_fallback(version, registry, skip_build):
|
||||||
|
print(f"🎉 Release {version} published successfully!")
|
||||||
|
else:
|
||||||
|
print(f"❌ Release {version} failed")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@main.command()
|
||||||
|
@click.option('--registry', default='gitea', help='Registry type (gitea, pypi, etc.)')
|
||||||
|
@click.pass_context
|
||||||
|
def upload(ctx, registry: str):
|
||||||
|
"""Upload existing packages to registry."""
|
||||||
|
manager = ReleaseManager(
|
||||||
|
project_root=ctx.obj['project_root'],
|
||||||
|
dry_run=ctx.obj['dry_run'],
|
||||||
|
force=ctx.obj['force']
|
||||||
|
)
|
||||||
|
|
||||||
|
if manager.upload_existing_packages(registry):
|
||||||
|
print(f"✅ Packages uploaded to {registry}")
|
||||||
|
else:
|
||||||
|
print(f"❌ Upload to {registry} failed")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@main.command('registry-info')
|
||||||
|
@click.option('--registry', default='gitea', help='Registry type to show info for')
|
||||||
|
@click.pass_context
|
||||||
|
def registry_info(ctx, registry: str):
|
||||||
|
"""Show package registry information and status."""
|
||||||
|
manager = ReleaseManager(
|
||||||
|
project_root=ctx.obj['project_root'],
|
||||||
|
dry_run=ctx.obj['dry_run'],
|
||||||
|
force=ctx.obj['force']
|
||||||
|
)
|
||||||
|
|
||||||
|
info = manager.show_registry_info(registry)
|
||||||
|
|
||||||
|
print(f"📦 {registry.title()} Registry Information")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
if 'error' in info:
|
||||||
|
print(f"❌ Error: {info['error']}")
|
||||||
|
return
|
||||||
|
|
||||||
|
for key, value in info.items():
|
||||||
|
if isinstance(value, bool):
|
||||||
|
indicator = "✅" if value else "❌"
|
||||||
|
print(f"{key.replace('_', ' ').title()}: {indicator}")
|
||||||
|
else:
|
||||||
|
print(f"{key.replace('_', ' ').title()}: {value}")
|
||||||
|
|
||||||
|
|
||||||
|
@main.command()
|
||||||
|
@click.pass_context
|
||||||
|
def clean(ctx):
|
||||||
|
"""Clean build artifacts and temporary files."""
|
||||||
|
manager = ReleaseManager(
|
||||||
|
project_root=ctx.obj['project_root'],
|
||||||
|
dry_run=ctx.obj['dry_run'],
|
||||||
|
force=ctx.obj['force']
|
||||||
|
)
|
||||||
|
|
||||||
|
manager.clean_build_artifacts()
|
||||||
|
print("✅ Build artifacts cleaned")
|
||||||
|
|
||||||
|
|
||||||
|
@main.command('version-info')
|
||||||
|
@click.option('--suggest', is_flag=True, help='Suggest next version options')
|
||||||
|
@click.pass_context
|
||||||
|
def version_info(ctx, suggest: bool):
|
||||||
|
"""Show version information and suggestions."""
|
||||||
|
version_manager = VersionManager(ctx.obj['project_root'])
|
||||||
|
|
||||||
|
current = version_manager.get_current_version()
|
||||||
|
print(f"Current Version: {current}")
|
||||||
|
|
||||||
|
if suggest:
|
||||||
|
suggestions = version_manager.suggest_version(current)
|
||||||
|
if 'error' in suggestions:
|
||||||
|
print(f"❌ {suggestions['error']}")
|
||||||
|
if 'suggestion' in suggestions:
|
||||||
|
print(f"💡 {suggestions['suggestion']}")
|
||||||
|
else:
|
||||||
|
print("\\nSuggested next versions:")
|
||||||
|
print(f" Patch: {suggestions['patch']}")
|
||||||
|
print(f" Minor: {suggestions['minor']}")
|
||||||
|
print(f" Major: {suggestions['major']}")
|
||||||
|
|
||||||
|
# Show version components
|
||||||
|
version_data = version_manager.parse_version(current)
|
||||||
|
if 'error' not in version_data:
|
||||||
|
print("\\nVersion Components:")
|
||||||
|
for key, value in version_data.items():
|
||||||
|
if value is not None:
|
||||||
|
print(f" {key.replace('_', ' ').title()}: {value}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
"""
|
||||||
|
Core release management classes.
|
||||||
|
|
||||||
|
This module provides the main classes for orchestrating releases:
|
||||||
|
- ReleaseManager: Main coordinator for release workflows
|
||||||
|
- PackageBuilder: Package building and setuptools-scm integration
|
||||||
|
- PublishManager: Package publication to registries
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .manager import ReleaseManager
|
||||||
|
from .builder import PackageBuilder
|
||||||
|
from .publisher import PublishManager
|
||||||
|
|
||||||
|
__all__ = ["ReleaseManager", "PackageBuilder", "PublishManager"]
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
"""
|
||||||
|
Package building functionality for releases.
|
||||||
|
|
||||||
|
This module handles package building using setuptools-scm and the Python build module.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Optional, Dict, Any
|
||||||
|
|
||||||
|
from ..utils.version import VersionManager
|
||||||
|
|
||||||
|
|
||||||
|
class PackageBuilder:
|
||||||
|
"""Handles package building with setuptools-scm integration."""
|
||||||
|
|
||||||
|
def __init__(self, project_root: Optional[Path] = None, dry_run: bool = False):
|
||||||
|
"""Initialize the package builder.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_root: Root directory of the project. Defaults to current directory.
|
||||||
|
dry_run: If True, show what would be done without executing.
|
||||||
|
"""
|
||||||
|
self.project_root = project_root or Path.cwd()
|
||||||
|
self.dry_run = dry_run
|
||||||
|
self.dist_dir = self.project_root / "dist"
|
||||||
|
|
||||||
|
def get_current_version(self) -> str:
|
||||||
|
"""Get current version using setuptools-scm.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Current version string or "unknown" if unavailable.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
result = self._run_command(
|
||||||
|
['python', '-m', 'setuptools_scm'],
|
||||||
|
capture=True,
|
||||||
|
skip_dry_run=True
|
||||||
|
)
|
||||||
|
return result.stdout.strip()
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
def clean_build(self) -> None:
|
||||||
|
"""Clean previous build artifacts."""
|
||||||
|
print("🧹 Cleaning build artifacts...")
|
||||||
|
|
||||||
|
patterns = ['build', 'dist', '*.egg-info']
|
||||||
|
for pattern in patterns:
|
||||||
|
try:
|
||||||
|
if pattern == 'dist' and self.dist_dir.exists():
|
||||||
|
if self.dry_run:
|
||||||
|
print(f"[DRY RUN] Would remove: {self.dist_dir}")
|
||||||
|
else:
|
||||||
|
import shutil
|
||||||
|
shutil.rmtree(self.dist_dir)
|
||||||
|
print(f"✅ Removed: {self.dist_dir}")
|
||||||
|
elif pattern != 'dist':
|
||||||
|
self._run_command(['rm', '-rf', pattern])
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
pass # Ignore if files don't exist
|
||||||
|
|
||||||
|
def build_packages(self) -> bool:
|
||||||
|
"""Build release packages using setuptools-scm.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if build successful, False otherwise.
|
||||||
|
"""
|
||||||
|
print(f"📦 Building packages (version auto-determined by setuptools-scm)")
|
||||||
|
|
||||||
|
# Clean previous builds
|
||||||
|
self.clean_build()
|
||||||
|
|
||||||
|
# Build packages
|
||||||
|
try:
|
||||||
|
print("Building packages...")
|
||||||
|
self._run_command(['python', '-m', 'build'], capture=False)
|
||||||
|
print("✅ Packages built successfully")
|
||||||
|
|
||||||
|
# Show package details
|
||||||
|
self._show_package_details()
|
||||||
|
return True
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"❌ Package build failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_built_packages(self) -> Dict[str, List[Path]]:
|
||||||
|
"""Get list of built packages by type.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with 'wheels' and 'sdists' keys containing file paths.
|
||||||
|
"""
|
||||||
|
if not self.dist_dir.exists():
|
||||||
|
return {"wheels": [], "sdists": []}
|
||||||
|
|
||||||
|
wheels = list(self.dist_dir.glob("*.whl"))
|
||||||
|
sdists = list(self.dist_dir.glob("*.tar.gz"))
|
||||||
|
|
||||||
|
return {"wheels": wheels, "sdists": sdists}
|
||||||
|
|
||||||
|
def validate_packages(self) -> bool:
|
||||||
|
"""Validate that expected packages were built.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if packages are valid, False otherwise.
|
||||||
|
"""
|
||||||
|
packages = self.get_built_packages()
|
||||||
|
|
||||||
|
if not packages["wheels"] and not packages["sdists"]:
|
||||||
|
print("❌ No packages found in dist/")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check package sizes
|
||||||
|
for wheel in packages["wheels"]:
|
||||||
|
if wheel.stat().st_size < 1000: # Less than 1KB
|
||||||
|
print(f"⚠️ Warning: {wheel.name} is very small ({wheel.stat().st_size} bytes)")
|
||||||
|
|
||||||
|
for sdist in packages["sdists"]:
|
||||||
|
if sdist.stat().st_size < 1000: # Less than 1KB
|
||||||
|
print(f"⚠️ Warning: {sdist.name} is very small ({sdist.stat().st_size} bytes)")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _show_package_details(self) -> None:
|
||||||
|
"""Show details about built packages."""
|
||||||
|
packages = self.get_built_packages()
|
||||||
|
|
||||||
|
if packages["wheels"] or packages["sdists"]:
|
||||||
|
print(f"\n📦 Built packages in {self.dist_dir}:")
|
||||||
|
|
||||||
|
for wheel in packages["wheels"]:
|
||||||
|
size = wheel.stat().st_size
|
||||||
|
print(f" 🎯 {wheel.name} ({size:,} bytes)")
|
||||||
|
|
||||||
|
for sdist in packages["sdists"]:
|
||||||
|
size = sdist.stat().st_size
|
||||||
|
print(f" 📄 {sdist.name} ({size:,} bytes)")
|
||||||
|
else:
|
||||||
|
print("❌ No packages found")
|
||||||
|
|
||||||
|
def _run_command(self, cmd: List[str], capture: bool = True,
|
||||||
|
check: bool = True, skip_dry_run: bool = False) -> subprocess.CompletedProcess:
|
||||||
|
"""Run a command with optional dry-run support.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cmd: Command to execute
|
||||||
|
capture: Whether to capture output
|
||||||
|
check: Whether to raise on non-zero exit
|
||||||
|
skip_dry_run: Whether to skip dry-run check and always execute
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
CompletedProcess result
|
||||||
|
"""
|
||||||
|
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,
|
||||||
|
cwd=self.project_root
|
||||||
|
)
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
"""
|
||||||
|
Main release manager orchestrating complete release workflows.
|
||||||
|
|
||||||
|
This module provides the primary ReleaseManager class that coordinates
|
||||||
|
all aspects of the release process.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, List, Tuple, Dict, Any
|
||||||
|
|
||||||
|
from .builder import PackageBuilder
|
||||||
|
from .publisher import PublishManager
|
||||||
|
from ..git.manager import GitManager
|
||||||
|
from ..utils.validation import ReleaseValidator
|
||||||
|
|
||||||
|
|
||||||
|
class ReleaseManager:
|
||||||
|
"""Main orchestrator for release workflows."""
|
||||||
|
|
||||||
|
def __init__(self, project_root: Optional[Path] = None, dry_run: bool = False, force: bool = False):
|
||||||
|
"""Initialize the release manager.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_root: Root directory of the project. Defaults to current directory.
|
||||||
|
dry_run: If True, show what would be done without executing.
|
||||||
|
force: If True, skip validation checks.
|
||||||
|
"""
|
||||||
|
self.project_root = project_root or Path.cwd()
|
||||||
|
self.dry_run = dry_run
|
||||||
|
self.force = force
|
||||||
|
|
||||||
|
# Initialize component managers
|
||||||
|
self.git_manager = GitManager(project_root, dry_run)
|
||||||
|
self.builder = PackageBuilder(project_root, dry_run)
|
||||||
|
self.publisher = PublishManager(project_root, dry_run)
|
||||||
|
self.validator = ReleaseValidator(project_root)
|
||||||
|
|
||||||
|
def get_release_status(self) -> Dict[str, Any]:
|
||||||
|
"""Get comprehensive release status information.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with release status details
|
||||||
|
"""
|
||||||
|
status = {}
|
||||||
|
|
||||||
|
# Version information
|
||||||
|
status['version'] = self.builder.get_current_version()
|
||||||
|
|
||||||
|
# Git status
|
||||||
|
git_status = self.git_manager.get_repository_status()
|
||||||
|
status.update(git_status)
|
||||||
|
|
||||||
|
# Package status
|
||||||
|
packages = self.builder.get_built_packages()
|
||||||
|
status['packages'] = {
|
||||||
|
'wheels': [p.name for p in packages['wheels']],
|
||||||
|
'sdists': [p.name for p in packages['sdists']],
|
||||||
|
'total_count': len(packages['wheels']) + len(packages['sdists'])
|
||||||
|
}
|
||||||
|
|
||||||
|
# Validation status
|
||||||
|
is_valid, issues = self.validate_release_state()
|
||||||
|
status['validation'] = {
|
||||||
|
'is_valid': is_valid,
|
||||||
|
'issues': issues
|
||||||
|
}
|
||||||
|
|
||||||
|
return status
|
||||||
|
|
||||||
|
def validate_release_state(self) -> Tuple[bool, List[str]]:
|
||||||
|
"""Validate that the repository is ready for release.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (is_valid, list_of_issues)
|
||||||
|
"""
|
||||||
|
return self.validator.validate_release_state(force=self.force)
|
||||||
|
|
||||||
|
def create_tag(self, version: str, message: Optional[str] = None) -> bool:
|
||||||
|
"""Create a git tag for the release.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
version: Version to tag (e.g., "1.0.0")
|
||||||
|
message: Optional tag message
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if tag created successfully, False otherwise
|
||||||
|
"""
|
||||||
|
# Validate release state first
|
||||||
|
is_valid, issues = self.validate_release_state()
|
||||||
|
if not is_valid and not self.force:
|
||||||
|
print("❌ Cannot create tag:")
|
||||||
|
for issue in issues:
|
||||||
|
print(f" - {issue}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return self.git_manager.create_tag(version, message)
|
||||||
|
|
||||||
|
def build_packages(self) -> bool:
|
||||||
|
"""Build release packages.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if build successful, False otherwise
|
||||||
|
"""
|
||||||
|
success = self.builder.build_packages()
|
||||||
|
if success:
|
||||||
|
success = self.builder.validate_packages()
|
||||||
|
return success
|
||||||
|
|
||||||
|
def publish_release(self, version: str, registry_type: str = 'gitea',
|
||||||
|
skip_build: bool = False) -> bool:
|
||||||
|
"""Complete release workflow: validate, tag, build, and publish.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
version: Version to release
|
||||||
|
registry_type: Type of registry to publish to
|
||||||
|
skip_build: If True, skip building and use existing packages
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if release successful, False otherwise
|
||||||
|
"""
|
||||||
|
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:")
|
||||||
|
for issue in issues:
|
||||||
|
print(f" - {issue}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Create git tag (this determines the version for setuptools-scm)
|
||||||
|
if not self.git_manager.tag_exists(f"v{version}"):
|
||||||
|
if not self.create_tag(version):
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
print(f"✅ Tag v{version} already exists")
|
||||||
|
|
||||||
|
# Build packages (setuptools-scm will use the tag for version)
|
||||||
|
if not skip_build:
|
||||||
|
if not self.build_packages():
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
print("⏭️ Skipping build (using existing packages)")
|
||||||
|
|
||||||
|
# Publish packages
|
||||||
|
if not self.publisher.publish_packages(registry_type):
|
||||||
|
print("⚠️ Release completed but publishing failed")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f"✅ Release {version} completed successfully!")
|
||||||
|
print(f"📦 Packages published to {registry_type}")
|
||||||
|
print(f"🏷️ Git tag v{version} created")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def publish_with_fallback(self, version: str, registry_type: str = 'gitea',
|
||||||
|
skip_build: bool = False) -> bool:
|
||||||
|
"""Complete release workflow with fallback to release assets.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
version: Version to release
|
||||||
|
registry_type: Type of registry to publish to
|
||||||
|
skip_build: If True, skip building and use existing packages
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if release successful, False otherwise
|
||||||
|
"""
|
||||||
|
# Try normal publish first
|
||||||
|
if self.publish_release(version, registry_type, skip_build):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# If that fails, try release assets fallback
|
||||||
|
print("🔄 Attempting release assets fallback...")
|
||||||
|
return self.publisher.publish_as_release_assets(version, registry_type)
|
||||||
|
|
||||||
|
def upload_existing_packages(self, registry_type: str = 'gitea') -> bool:
|
||||||
|
"""Upload existing packages without building or tagging.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
registry_type: Type of registry to upload to
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if upload successful, False otherwise
|
||||||
|
"""
|
||||||
|
print(f"📤 Uploading existing packages to {registry_type}")
|
||||||
|
|
||||||
|
packages = self.builder.get_built_packages()
|
||||||
|
if not packages["wheels"] and not packages["sdists"]:
|
||||||
|
print("❌ No packages found in dist/. Run build first.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
all_packages = packages["wheels"] + packages["sdists"]
|
||||||
|
return self.publisher.upload_specific_packages(all_packages, registry_type)
|
||||||
|
|
||||||
|
def clean_build_artifacts(self) -> None:
|
||||||
|
"""Clean build artifacts and temporary files."""
|
||||||
|
self.builder.clean_build()
|
||||||
|
|
||||||
|
def show_registry_info(self, registry_type: str = 'gitea') -> Dict[str, Any]:
|
||||||
|
"""Show information about a registry.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
registry_type: Type of registry
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with registry information
|
||||||
|
"""
|
||||||
|
return self.publisher.get_registry_info(registry_type)
|
||||||
|
|
||||||
|
def get_commits_since_last_tag(self) -> List[str]:
|
||||||
|
"""Get commits since the last release tag.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of commit messages since last tag
|
||||||
|
"""
|
||||||
|
return self.git_manager.get_commits_since_tag()
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
"""
|
||||||
|
Package publishing functionality for releases.
|
||||||
|
|
||||||
|
This module handles publishing packages to various registries.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Optional, Any
|
||||||
|
|
||||||
|
from ..registries.factory import RegistryFactory
|
||||||
|
from ..registries.base import RegistryInterface
|
||||||
|
from .builder import PackageBuilder
|
||||||
|
|
||||||
|
|
||||||
|
class PublishManager:
|
||||||
|
"""Handles package publication to registries."""
|
||||||
|
|
||||||
|
def __init__(self, project_root: Optional[Path] = None, dry_run: bool = False):
|
||||||
|
"""Initialize the publish manager.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_root: Root directory of the project. Defaults to current directory.
|
||||||
|
dry_run: If True, show what would be done without executing.
|
||||||
|
"""
|
||||||
|
self.project_root = project_root or Path.cwd()
|
||||||
|
self.dry_run = dry_run
|
||||||
|
|
||||||
|
def publish_packages(self, registry_type: str = 'gitea',
|
||||||
|
registry_config: Optional[Dict[str, Any]] = None) -> bool:
|
||||||
|
"""Publish packages to specified registry.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
registry_type: Type of registry to publish to
|
||||||
|
registry_config: Optional registry configuration
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if publishing successful, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Get registry client
|
||||||
|
registry = self._get_registry(registry_type, registry_config)
|
||||||
|
|
||||||
|
# Get built packages
|
||||||
|
builder = PackageBuilder(self.project_root)
|
||||||
|
packages = builder.get_built_packages()
|
||||||
|
|
||||||
|
if not packages["wheels"] and not packages["sdists"]:
|
||||||
|
print("❌ No packages found in dist/. Run build first.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Upload packages
|
||||||
|
success = True
|
||||||
|
for wheel in packages["wheels"]:
|
||||||
|
if not self._upload_package_with_fallback(registry, wheel):
|
||||||
|
success = False
|
||||||
|
|
||||||
|
for sdist in packages["sdists"]:
|
||||||
|
if not self._upload_package_with_fallback(registry, sdist):
|
||||||
|
success = False
|
||||||
|
|
||||||
|
return success
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Publishing failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def upload_specific_packages(self, package_paths: List[Path],
|
||||||
|
registry_type: str = 'gitea',
|
||||||
|
registry_config: Optional[Dict[str, Any]] = None) -> bool:
|
||||||
|
"""Upload specific package files to registry.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
package_paths: List of paths to package files
|
||||||
|
registry_type: Type of registry to publish to
|
||||||
|
registry_config: Optional registry configuration
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if all uploads successful, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
registry = self._get_registry(registry_type, registry_config)
|
||||||
|
|
||||||
|
success = True
|
||||||
|
for package_path in package_paths:
|
||||||
|
if not package_path.exists():
|
||||||
|
print(f"❌ Package not found: {package_path}")
|
||||||
|
success = False
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not self._upload_package_with_fallback(registry, package_path):
|
||||||
|
success = False
|
||||||
|
|
||||||
|
return success
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Upload failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def publish_as_release_assets(self, version: str,
|
||||||
|
registry_type: str = 'gitea',
|
||||||
|
registry_config: Optional[Dict[str, Any]] = None) -> bool:
|
||||||
|
"""Publish packages as release assets (fallback method).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
version: Version to publish as
|
||||||
|
registry_type: Type of registry (must support release assets)
|
||||||
|
registry_config: Optional registry configuration
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if publishing successful, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
registry = self._get_registry(registry_type, registry_config)
|
||||||
|
|
||||||
|
# Check if registry supports release assets
|
||||||
|
if not hasattr(registry, 'upload_package_as_release_assets'):
|
||||||
|
print(f"❌ Registry type '{registry_type}' does not support release assets")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Get built packages
|
||||||
|
builder = PackageBuilder(self.project_root)
|
||||||
|
packages = builder.get_built_packages()
|
||||||
|
|
||||||
|
if not packages["wheels"] and not packages["sdists"]:
|
||||||
|
print("❌ No packages found in dist/. Run build first.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Find wheel and corresponding source distribution
|
||||||
|
success = True
|
||||||
|
for wheel in packages["wheels"]:
|
||||||
|
# Find matching sdist
|
||||||
|
sdist = None
|
||||||
|
wheel_name_parts = wheel.stem.split('-')
|
||||||
|
package_name = wheel_name_parts[0] if wheel_name_parts else ""
|
||||||
|
|
||||||
|
for potential_sdist in packages["sdists"]:
|
||||||
|
if potential_sdist.stem.startswith(package_name):
|
||||||
|
sdist = potential_sdist
|
||||||
|
break
|
||||||
|
|
||||||
|
# Upload as release assets
|
||||||
|
if not registry.upload_package_as_release_assets(
|
||||||
|
version, wheel, sdist, dry_run=self.dry_run
|
||||||
|
):
|
||||||
|
success = False
|
||||||
|
|
||||||
|
return success
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Release asset publishing failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_registry_info(self, registry_type: str = 'gitea',
|
||||||
|
registry_config: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||||
|
"""Get information about a registry.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
registry_type: Type of registry
|
||||||
|
registry_config: Optional registry configuration
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with registry information
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
registry = self._get_registry(registry_type, registry_config)
|
||||||
|
return registry.get_registry_info()
|
||||||
|
except Exception as e:
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
def _get_registry(self, registry_type: str,
|
||||||
|
registry_config: Optional[Dict[str, Any]] = None) -> RegistryInterface:
|
||||||
|
"""Get a registry client.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
registry_type: Type of registry
|
||||||
|
registry_config: Optional registry configuration
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Registry client instance
|
||||||
|
"""
|
||||||
|
if registry_config:
|
||||||
|
return RegistryFactory.create(registry_type, registry_config)
|
||||||
|
else:
|
||||||
|
# Try to load from pyproject.toml first
|
||||||
|
try:
|
||||||
|
return RegistryFactory.create_from_pyproject_config(
|
||||||
|
self.project_root / "pyproject.toml",
|
||||||
|
registry_type
|
||||||
|
)
|
||||||
|
except (ValueError, FileNotFoundError):
|
||||||
|
# Fallback to auto-detection
|
||||||
|
return RegistryFactory.create(registry_type)
|
||||||
|
|
||||||
|
def _upload_package_with_fallback(self, registry: RegistryInterface,
|
||||||
|
package_path: Path) -> bool:
|
||||||
|
"""Upload a package with fallback to release assets if needed.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
registry: Registry client
|
||||||
|
package_path: Path to package file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if upload successful, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Try normal package upload first
|
||||||
|
return registry.upload_package(package_path, dry_run=self.dry_run)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Package upload failed: {e}")
|
||||||
|
|
||||||
|
# Check if registry supports release assets as fallback
|
||||||
|
if hasattr(registry, 'upload_package_as_release_assets'):
|
||||||
|
print("🔄 Trying release assets as fallback...")
|
||||||
|
|
||||||
|
# Extract version from package filename for release assets
|
||||||
|
version = self._extract_version_from_filename(package_path)
|
||||||
|
if version:
|
||||||
|
return registry.upload_package_as_release_assets(
|
||||||
|
version, package_path, dry_run=self.dry_run
|
||||||
|
)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _extract_version_from_filename(self, package_path: Path) -> Optional[str]:
|
||||||
|
"""Extract version from package filename.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
package_path: Path to package file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Version string or None if not found
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if package_path.suffix == '.whl':
|
||||||
|
# Wheel format: package-version-python-abi-platform.whl
|
||||||
|
parts = package_path.stem.split('-')
|
||||||
|
if len(parts) >= 2:
|
||||||
|
return parts[1]
|
||||||
|
elif package_path.suffix == '.gz' and package_path.name.endswith('.tar.gz'):
|
||||||
|
# Source dist format: package-version.tar.gz
|
||||||
|
name_without_tar = package_path.name.replace('.tar.gz', '')
|
||||||
|
parts = name_without_tar.split('-')
|
||||||
|
if len(parts) >= 2:
|
||||||
|
return parts[1]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return None
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
"""
|
||||||
|
Git management for releases.
|
||||||
|
|
||||||
|
This module provides Git operations required for release workflows:
|
||||||
|
- Tag creation and management
|
||||||
|
- Repository state validation
|
||||||
|
- Branch and commit operations
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .manager import GitManager
|
||||||
|
|
||||||
|
__all__ = ["GitManager"]
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
"""
|
||||||
|
Git operations for release management.
|
||||||
|
|
||||||
|
This module handles all Git-related operations needed for releases.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Any, Optional, List
|
||||||
|
|
||||||
|
|
||||||
|
class GitManager:
|
||||||
|
"""Manages Git operations for releases."""
|
||||||
|
|
||||||
|
def __init__(self, project_root: Optional[Path] = None, dry_run: bool = False):
|
||||||
|
"""Initialize Git manager.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_root: Root directory of the project
|
||||||
|
dry_run: If True, show what would be done without executing
|
||||||
|
"""
|
||||||
|
self.project_root = project_root or Path.cwd()
|
||||||
|
self.dry_run = dry_run
|
||||||
|
|
||||||
|
def get_repository_status(self) -> Dict[str, Any]:
|
||||||
|
"""Get current git repository status.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with repository status information
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Get current branch
|
||||||
|
branch_result = self._run_command(['git', 'branch', '--show-current'])
|
||||||
|
current_branch = branch_result.stdout.strip()
|
||||||
|
|
||||||
|
# Check for uncommitted changes
|
||||||
|
status_result = self._run_command(['git', 'status', '--porcelain'])
|
||||||
|
has_changes = bool(status_result.stdout.strip())
|
||||||
|
|
||||||
|
# Get latest commit
|
||||||
|
commit_result = self._run_command(['git', 'rev-parse', '--short', 'HEAD'])
|
||||||
|
latest_commit = commit_result.stdout.strip()
|
||||||
|
|
||||||
|
# Get latest tag
|
||||||
|
try:
|
||||||
|
tag_result = self._run_command(['git', 'describe', '--tags', '--abbrev=0'])
|
||||||
|
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 create_tag(self, version: str, message: Optional[str] = None) -> bool:
|
||||||
|
"""Create and push git tag.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
version: Version to tag (e.g., "1.0.0")
|
||||||
|
message: Optional tag message
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
if not version.startswith('v'):
|
||||||
|
tag_name = f"v{version}"
|
||||||
|
else:
|
||||||
|
tag_name = version
|
||||||
|
|
||||||
|
tag_message = message or f"Release {version.lstrip('v')}"
|
||||||
|
print(f"🏷️ Creating git tag {tag_name}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create annotated tag
|
||||||
|
self._run_command(['git', 'tag', '-a', tag_name, '-m', tag_message])
|
||||||
|
print(f"✅ Tag {tag_name} created")
|
||||||
|
|
||||||
|
# Push tag to origin
|
||||||
|
try:
|
||||||
|
print(f"📤 Pushing tag to origin...")
|
||||||
|
self._run_command(['git', 'push', 'origin', tag_name])
|
||||||
|
print(f"✅ Tag pushed to origin")
|
||||||
|
return True
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"⚠️ Could not push tag to origin: {e}")
|
||||||
|
print(f"You can push it manually with: git push origin {tag_name}")
|
||||||
|
return True # Tag created successfully, push can be done manually
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"❌ Failed to create tag: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def validate_release_state(self, force: bool = False) -> tuple[bool, List[str]]:
|
||||||
|
"""Validate that repository is ready for release.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
force: Skip validation checks if True
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (is_valid, list_of_issues)
|
||||||
|
"""
|
||||||
|
issues = []
|
||||||
|
status = self.get_repository_status()
|
||||||
|
|
||||||
|
if not status['is_repo']:
|
||||||
|
issues.append("Not in a git repository")
|
||||||
|
else:
|
||||||
|
if status['has_changes'] and not force:
|
||||||
|
issues.append("Repository has uncommitted changes")
|
||||||
|
if status['branch'] != 'main' and not force:
|
||||||
|
issues.append(f"Not on main branch (currently on {status['branch']})")
|
||||||
|
|
||||||
|
return len(issues) == 0, issues
|
||||||
|
|
||||||
|
def get_commits_since_tag(self, tag_name: Optional[str] = None) -> List[str]:
|
||||||
|
"""Get list of commits since specified tag.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tag_name: Tag to compare against. If None, uses latest tag.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of commit messages since tag
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if tag_name is None:
|
||||||
|
# Get latest tag
|
||||||
|
tag_result = self._run_command(['git', 'describe', '--tags', '--abbrev=0'])
|
||||||
|
tag_name = tag_result.stdout.strip()
|
||||||
|
|
||||||
|
# Get commits since tag
|
||||||
|
log_result = self._run_command([
|
||||||
|
'git', 'log', f'{tag_name}..HEAD', '--oneline', '--no-merges'
|
||||||
|
])
|
||||||
|
|
||||||
|
commits = []
|
||||||
|
for line in log_result.stdout.strip().split('\n'):
|
||||||
|
if line:
|
||||||
|
commits.append(line)
|
||||||
|
|
||||||
|
return commits
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def tag_exists(self, tag_name: str) -> bool:
|
||||||
|
"""Check if a git tag exists.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tag_name: Tag name to check
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if tag exists, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self._run_command(['git', 'rev-parse', f'refs/tags/{tag_name}'])
|
||||||
|
return True
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_remote_url(self, remote: str = 'origin') -> Optional[str]:
|
||||||
|
"""Get the URL of a git remote.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
remote: Remote name (default: 'origin')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Remote URL or None if not found
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
result = self._run_command(['git', 'remote', 'get-url', remote])
|
||||||
|
return result.stdout.strip()
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _run_command(self, cmd: List[str]) -> subprocess.CompletedProcess:
|
||||||
|
"""Run a git command.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cmd: Command to execute
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
CompletedProcess result
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
subprocess.CalledProcessError: If command fails
|
||||||
|
"""
|
||||||
|
if self.dry_run and not any(read_only in cmd for read_only in
|
||||||
|
['status', 'branch', 'rev-parse', 'describe',
|
||||||
|
'log', 'remote']):
|
||||||
|
print(f"[DRY RUN] Would run: {' '.join(cmd)}")
|
||||||
|
return subprocess.CompletedProcess(cmd, 0, "", "")
|
||||||
|
|
||||||
|
return subprocess.run(
|
||||||
|
cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
|
cwd=self.project_root
|
||||||
|
)
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
"""
|
||||||
|
Package registry implementations.
|
||||||
|
|
||||||
|
This module provides registry clients for publishing packages to various platforms:
|
||||||
|
- Gitea package registries
|
||||||
|
- PyPI and Test PyPI
|
||||||
|
- Extensible factory for custom registries
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .factory import RegistryFactory
|
||||||
|
from .base import RegistryInterface, RegistryConfig
|
||||||
|
from .gitea.registry import GiteaRegistry
|
||||||
|
from .gitea.config import GiteaConfig
|
||||||
|
from .gitea.exceptions import GiteaError
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"RegistryFactory",
|
||||||
|
"RegistryInterface",
|
||||||
|
"RegistryConfig",
|
||||||
|
"GiteaRegistry",
|
||||||
|
"GiteaConfig",
|
||||||
|
"GiteaError",
|
||||||
|
]
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
"""
|
||||||
|
Base registry interface and configuration.
|
||||||
|
|
||||||
|
This module defines the common interface that all registry implementations must follow.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Optional, Any
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RegistryConfig:
|
||||||
|
"""Base configuration for package registries."""
|
||||||
|
name: str
|
||||||
|
type: str
|
||||||
|
url: str
|
||||||
|
auth_token_env: Optional[str] = None
|
||||||
|
|
||||||
|
def get_auth_token(self) -> Optional[str]:
|
||||||
|
"""Get authentication token from environment variable."""
|
||||||
|
if self.auth_token_env:
|
||||||
|
import os
|
||||||
|
return os.getenv(self.auth_token_env)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class RegistryInterface(ABC):
|
||||||
|
"""Abstract interface for package registries."""
|
||||||
|
|
||||||
|
def __init__(self, config: RegistryConfig):
|
||||||
|
"""Initialize the registry with configuration."""
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def upload_package(self, package_path: Path, dry_run: bool = False) -> bool:
|
||||||
|
"""Upload a package to the registry.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
package_path: Path to package file (.whl or .tar.gz)
|
||||||
|
dry_run: If True, show what would be done without uploading
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if upload successful, False otherwise
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def check_auth(self) -> bool:
|
||||||
|
"""Check if authentication is properly configured.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if authenticated, False otherwise
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def list_packages(self) -> List[Dict[str, Any]]:
|
||||||
|
"""List packages in the registry.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of package information dictionaries
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_package_info(self, package_name: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Get information about a specific package.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
package_name: Name of the package
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Package information dictionary or None if not found
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def delete_package_version(self, package_name: str, version: str,
|
||||||
|
dry_run: bool = False) -> bool:
|
||||||
|
"""Delete a specific version of a package.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
package_name: Name of the package
|
||||||
|
version: Version to delete
|
||||||
|
dry_run: If True, show what would be done without deleting
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if deletion successful, False otherwise
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_registry_info(self) -> Dict[str, Any]:
|
||||||
|
"""Get information about the registry configuration.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with registry information
|
||||||
|
"""
|
||||||
|
pass
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
"""
|
||||||
|
Registry factory for creating registry clients.
|
||||||
|
|
||||||
|
This module provides a factory for creating appropriate registry clients
|
||||||
|
based on configuration or registry type.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Dict, Type, Optional, Any
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from .base import RegistryInterface, RegistryConfig
|
||||||
|
from .gitea.registry import GiteaRegistry
|
||||||
|
from .gitea.config import GiteaConfig
|
||||||
|
|
||||||
|
|
||||||
|
class RegistryFactory:
|
||||||
|
"""Factory for creating registry clients."""
|
||||||
|
|
||||||
|
_registry_types: Dict[str, Type[RegistryInterface]] = {
|
||||||
|
'gitea': GiteaRegistry,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, registry_type: str, config: Optional[Dict[str, Any]] = None) -> RegistryInterface:
|
||||||
|
"""Create a registry client of the specified type.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
registry_type: Type of registry ('gitea', 'pypi', etc.)
|
||||||
|
config: Optional configuration dictionary
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Registry client instance
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If registry type is not supported
|
||||||
|
"""
|
||||||
|
if registry_type not in cls._registry_types:
|
||||||
|
raise ValueError(f"Unsupported registry type: {registry_type}. "
|
||||||
|
f"Supported types: {list(cls._registry_types.keys())}")
|
||||||
|
|
||||||
|
registry_class = cls._registry_types[registry_type]
|
||||||
|
|
||||||
|
# Handle Gitea-specific configuration
|
||||||
|
if registry_type == 'gitea':
|
||||||
|
if config:
|
||||||
|
gitea_config = GiteaConfig(
|
||||||
|
gitea_url=config.get('url', ''),
|
||||||
|
repo_owner=config.get('owner', ''),
|
||||||
|
repo_name=config.get('repo', ''),
|
||||||
|
auth_token=config.get('auth_token')
|
||||||
|
)
|
||||||
|
return registry_class(gitea_config)
|
||||||
|
else:
|
||||||
|
# Auto-detect from git repository
|
||||||
|
return registry_class()
|
||||||
|
|
||||||
|
# For other registry types, create with generic config
|
||||||
|
if config:
|
||||||
|
registry_config = RegistryConfig(
|
||||||
|
name=config.get('name', registry_type),
|
||||||
|
type=registry_type,
|
||||||
|
url=config['url'],
|
||||||
|
auth_token_env=config.get('auth_token_env')
|
||||||
|
)
|
||||||
|
return registry_class(registry_config)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Configuration required for {registry_type} registry")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_from_pyproject_config(cls, pyproject_path: Optional[Path] = None,
|
||||||
|
registry_name: str = 'gitea') -> RegistryInterface:
|
||||||
|
"""Create a registry client from pyproject.toml configuration.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pyproject_path: Path to pyproject.toml file. If None, looks in current directory.
|
||||||
|
registry_name: Name of registry configuration to use
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Registry client instance
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If configuration is invalid or registry not found
|
||||||
|
"""
|
||||||
|
if pyproject_path is None:
|
||||||
|
pyproject_path = Path.cwd() / "pyproject.toml"
|
||||||
|
|
||||||
|
if not pyproject_path.exists():
|
||||||
|
raise ValueError(f"pyproject.toml not found at {pyproject_path}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
import tomllib
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
import tomli as tomllib # Fallback for Python < 3.11
|
||||||
|
except ImportError:
|
||||||
|
raise ImportError("tomllib or tomli required to read pyproject.toml")
|
||||||
|
|
||||||
|
with open(pyproject_path, 'rb') as f:
|
||||||
|
config = tomllib.load(f)
|
||||||
|
|
||||||
|
# Look for release-management configuration
|
||||||
|
release_config = config.get('tool', {}).get('release-management', {})
|
||||||
|
registries_config = release_config.get('registries', {})
|
||||||
|
|
||||||
|
if registry_name not in registries_config:
|
||||||
|
raise ValueError(f"Registry '{registry_name}' not found in pyproject.toml configuration")
|
||||||
|
|
||||||
|
registry_config = registries_config[registry_name]
|
||||||
|
registry_type = registry_config.get('type', registry_name)
|
||||||
|
|
||||||
|
# Add auth token from environment if specified
|
||||||
|
auth_token_env = registry_config.get('auth_token_env')
|
||||||
|
if auth_token_env:
|
||||||
|
import os
|
||||||
|
registry_config = registry_config.copy()
|
||||||
|
registry_config['auth_token'] = os.getenv(auth_token_env)
|
||||||
|
|
||||||
|
return cls.create(registry_type, registry_config)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def auto_detect(cls) -> RegistryInterface:
|
||||||
|
"""Auto-detect registry type from current environment.
|
||||||
|
|
||||||
|
Currently only supports Gitea auto-detection from git repository.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Registry client instance
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If no registry can be auto-detected
|
||||||
|
"""
|
||||||
|
# Try Gitea auto-detection first
|
||||||
|
try:
|
||||||
|
return cls.create('gitea')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
raise ValueError("Could not auto-detect registry type. "
|
||||||
|
"Ensure you're in a git repository with Gitea remote, "
|
||||||
|
"or provide explicit configuration.")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def register_registry_type(cls, registry_type: str, registry_class: Type[RegistryInterface]) -> None:
|
||||||
|
"""Register a new registry type.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
registry_type: String identifier for the registry type
|
||||||
|
registry_class: Registry class that implements RegistryInterface
|
||||||
|
"""
|
||||||
|
cls._registry_types[registry_type] = registry_class
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def list_supported_types(cls) -> list[str]:
|
||||||
|
"""List all supported registry types.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of supported registry type strings
|
||||||
|
"""
|
||||||
|
return list(cls._registry_types.keys())
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
"""
|
||||||
|
Gitea package registry implementation.
|
||||||
|
|
||||||
|
This module provides Gitea-specific registry functionality including:
|
||||||
|
- Package registry uploads
|
||||||
|
- Release asset fallback
|
||||||
|
- Configuration and authentication
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .registry import GiteaRegistry
|
||||||
|
from .config import GiteaConfig
|
||||||
|
from .exceptions import GiteaError, GiteaConfigError
|
||||||
|
|
||||||
|
__all__ = ["GiteaRegistry", "GiteaConfig", "GiteaError", "GiteaConfigError"]
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
"""
|
||||||
|
Gitea-specific exceptions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class GiteaError(Exception):
|
||||||
|
"""Base class for Gitea-related errors."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class GiteaConfigError(GiteaError):
|
||||||
|
"""Configuration-related errors."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class GiteaApiError(GiteaError):
|
||||||
|
"""API-related errors."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class GiteaAuthError(GiteaError):
|
||||||
|
"""Authentication-related errors."""
|
||||||
|
pass
|
||||||
@@ -0,0 +1,355 @@
|
|||||||
|
"""
|
||||||
|
Gitea Package Registry Client
|
||||||
|
|
||||||
|
This module provides functionality to publish Python packages to Gitea's package registry.
|
||||||
|
Gitea supports multiple package registries including PyPI-compatible registries.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, List, Dict, Any
|
||||||
|
|
||||||
|
from ..base import RegistryInterface
|
||||||
|
from .config import GiteaConfig
|
||||||
|
from .exceptions import GiteaError
|
||||||
|
|
||||||
|
|
||||||
|
class GiteaRegistry(RegistryInterface):
|
||||||
|
"""Client for publishing packages to Gitea package registry."""
|
||||||
|
|
||||||
|
def __init__(self, config: Optional[GiteaConfig] = None):
|
||||||
|
"""Initialize the package registry client.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: Gitea configuration. If None, auto-detects from git repository.
|
||||||
|
"""
|
||||||
|
self.config = config or GiteaConfig.from_git_repository()
|
||||||
|
self.config.validate()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pypi_registry_url(self) -> str:
|
||||||
|
"""Get the PyPI-compatible registry URL for this repository."""
|
||||||
|
return f"{self.config.gitea_url}/api/packages/{self.config.repo_owner}/pypi"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def package_list_url(self) -> str:
|
||||||
|
"""Get the package listing URL for this repository."""
|
||||||
|
return f"{self.config.gitea_url}/api/v1/packages/{self.config.repo_owner}"
|
||||||
|
|
||||||
|
def check_auth(self) -> bool:
|
||||||
|
"""Check if authentication token is available and valid."""
|
||||||
|
if not self.config.auth_token:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Test auth by trying to access packages API
|
||||||
|
import requests
|
||||||
|
headers = {"Authorization": f"token {self.config.auth_token}"}
|
||||||
|
response = requests.get(self.package_list_url, headers=headers, timeout=10)
|
||||||
|
return response.status_code in [200, 404] # 404 is okay if no packages exist yet
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def list_packages(self) -> List[Dict[str, Any]]:
|
||||||
|
"""List all packages for this repository owner.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of package information dictionaries
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import requests
|
||||||
|
headers = {}
|
||||||
|
if self.config.auth_token:
|
||||||
|
headers["Authorization"] = f"token {self.config.auth_token}"
|
||||||
|
|
||||||
|
response = requests.get(self.package_list_url, headers=headers, timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
except Exception as e:
|
||||||
|
raise GiteaError(f"Failed to list packages: {e}")
|
||||||
|
|
||||||
|
def get_package_info(self, package_name: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Get information about a specific package.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
package_name: Name of the package
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Package information dictionary or None if not found
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
packages = self.list_packages()
|
||||||
|
for package in packages:
|
||||||
|
if package.get("name") == package_name:
|
||||||
|
return package
|
||||||
|
return None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def upload_package(self, package_path: Path, dry_run: bool = False) -> bool:
|
||||||
|
"""Upload a package to Gitea registry.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
package_path: Path to package file (.whl or .tar.gz)
|
||||||
|
dry_run: If True, show what would be done without uploading
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if upload successful, False otherwise
|
||||||
|
"""
|
||||||
|
if not self.config.auth_token:
|
||||||
|
raise GiteaError("Authentication token required for package upload. Set GITEA_API_TOKEN environment variable.")
|
||||||
|
|
||||||
|
if not package_path.exists():
|
||||||
|
raise GiteaError(f"Package file not found: {package_path}")
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
print(f"[DRY RUN] Would upload to: {self.pypi_registry_url}")
|
||||||
|
print(f"[DRY RUN] Would upload: {package_path}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
return self._upload_file(package_path)
|
||||||
|
|
||||||
|
def upload_package_as_release_assets(self,
|
||||||
|
version: str,
|
||||||
|
wheel_path: Path,
|
||||||
|
sdist_path: Optional[Path] = None,
|
||||||
|
dry_run: bool = False) -> bool:
|
||||||
|
"""Upload packages as Gitea release assets (fallback when package registry unavailable).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
version: Version tag (e.g., "v0.8.0")
|
||||||
|
wheel_path: Path to wheel (.whl) file
|
||||||
|
sdist_path: Optional path to source distribution (.tar.gz) file
|
||||||
|
dry_run: If True, show what would be done without uploading
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if upload successful, False otherwise
|
||||||
|
"""
|
||||||
|
if not self.config.auth_token:
|
||||||
|
raise GiteaError("Authentication token required for release upload. Set GITEA_API_TOKEN environment variable.")
|
||||||
|
|
||||||
|
if not wheel_path.exists():
|
||||||
|
raise GiteaError(f"Wheel file not found: {wheel_path}")
|
||||||
|
|
||||||
|
if sdist_path and not sdist_path.exists():
|
||||||
|
raise GiteaError(f"Source distribution file not found: {sdist_path}")
|
||||||
|
|
||||||
|
files_to_upload = [wheel_path]
|
||||||
|
if sdist_path:
|
||||||
|
files_to_upload.append(sdist_path)
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
print(f"[DRY RUN] Would upload release assets for {version}")
|
||||||
|
print(f"[DRY RUN] Release API: {self.config.repo_api_url}/releases")
|
||||||
|
for file_path in files_to_upload:
|
||||||
|
print(f"[DRY RUN] Would upload: {file_path}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Create or get release
|
||||||
|
release_id = self._create_or_get_release(version)
|
||||||
|
if not release_id:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Upload each file as release asset
|
||||||
|
success = True
|
||||||
|
for file_path in files_to_upload:
|
||||||
|
if not self._upload_release_asset(release_id, file_path):
|
||||||
|
success = False
|
||||||
|
|
||||||
|
return success
|
||||||
|
|
||||||
|
def delete_package_version(self, package_name: str, version: str,
|
||||||
|
dry_run: bool = False) -> bool:
|
||||||
|
"""Delete a specific version of a package.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
package_name: Name of the package
|
||||||
|
version: Version to delete
|
||||||
|
dry_run: If True, show what would be done without deleting
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if deletion successful, False otherwise
|
||||||
|
"""
|
||||||
|
if not self.config.auth_token:
|
||||||
|
raise GiteaError("Authentication token required for package deletion.")
|
||||||
|
|
||||||
|
delete_url = f"{self.config.gitea_url}/api/v1/packages/{self.config.repo_owner}/pypi/{package_name}/{version}"
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
print(f"[DRY RUN] Would delete: {package_name} v{version}")
|
||||||
|
print(f"[DRY RUN] DELETE {delete_url}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
import requests
|
||||||
|
headers = {"Authorization": f"token {self.config.auth_token}"}
|
||||||
|
response = requests.delete(delete_url, headers=headers, timeout=10)
|
||||||
|
|
||||||
|
if response.status_code in [200, 204, 404]: # 404 = already deleted
|
||||||
|
print(f"✅ Deleted: {package_name} v{version}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"❌ Delete failed: {response.status_code} {response.text}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Delete failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_registry_info(self) -> Dict[str, Any]:
|
||||||
|
"""Get information about the package registry configuration.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with registry information
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"gitea_url": self.config.gitea_url,
|
||||||
|
"repo_owner": self.config.repo_owner,
|
||||||
|
"repo_name": self.config.repo_name,
|
||||||
|
"pypi_registry_url": self.pypi_registry_url,
|
||||||
|
"package_list_url": self.package_list_url,
|
||||||
|
"auth_configured": bool(self.config.auth_token),
|
||||||
|
"auth_valid": self.check_auth() if self.config.auth_token else False
|
||||||
|
}
|
||||||
|
|
||||||
|
def _upload_file(self, file_path: Path) -> bool:
|
||||||
|
"""Upload a single file to the registry.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path: Path to file to upload
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if upload successful, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import requests
|
||||||
|
|
||||||
|
# Gitea PyPI upload API expects PUT with the file content as body
|
||||||
|
# URL format: /api/packages/{owner}/pypi/{filename}
|
||||||
|
upload_url = f"{self.config.gitea_url}/api/packages/{self.config.repo_owner}/pypi"
|
||||||
|
|
||||||
|
with open(file_path, 'rb') as f:
|
||||||
|
file_content = f.read()
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Authorization': f'token {self.config.auth_token}',
|
||||||
|
'Content-Type': 'application/octet-stream'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Upload using PUT request with filename in URL
|
||||||
|
upload_endpoint = f"{upload_url}/{file_path.name}"
|
||||||
|
response = requests.put(
|
||||||
|
upload_endpoint,
|
||||||
|
headers=headers,
|
||||||
|
data=file_content,
|
||||||
|
timeout=60
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code in [200, 201, 409]: # 409 = already exists
|
||||||
|
print(f"✅ Uploaded: {file_path.name}")
|
||||||
|
if response.status_code == 409:
|
||||||
|
print(f" (already exists)")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"❌ Upload failed for {file_path.name}: {response.status_code}")
|
||||||
|
if response.text:
|
||||||
|
print(f" Error: {response.text}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Upload failed for {file_path.name}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _create_or_get_release(self, version: str) -> Optional[int]:
|
||||||
|
"""Create a new release or get existing release ID.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
version: Version tag (e.g., "v0.8.0")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Release ID if successful, None otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import requests
|
||||||
|
|
||||||
|
# Ensure version has 'v' prefix
|
||||||
|
tag_name = version if version.startswith('v') else f'v{version}'
|
||||||
|
|
||||||
|
headers = {"Authorization": f"token {self.config.auth_token}"}
|
||||||
|
|
||||||
|
# First, try to get existing release
|
||||||
|
releases_url = f"{self.config.repo_api_url}/releases"
|
||||||
|
response = requests.get(releases_url, headers=headers, timeout=10)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
releases = response.json()
|
||||||
|
for release in releases:
|
||||||
|
if release.get('tag_name') == tag_name:
|
||||||
|
print(f"✅ Found existing release: {tag_name}")
|
||||||
|
return release['id']
|
||||||
|
|
||||||
|
# Create new release
|
||||||
|
release_data = {
|
||||||
|
"tag_name": tag_name,
|
||||||
|
"name": f"MarkiTect {version.lstrip('v')}",
|
||||||
|
"body": f"Release {version.lstrip('v')}\\n\\nPython packages for MarkiTect.",
|
||||||
|
"draft": False,
|
||||||
|
"prerelease": False
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(releases_url, headers=headers, json=release_data, timeout=10)
|
||||||
|
|
||||||
|
if response.status_code == 201:
|
||||||
|
release = response.json()
|
||||||
|
print(f"✅ Created release: {tag_name}")
|
||||||
|
return release['id']
|
||||||
|
else:
|
||||||
|
print(f"❌ Failed to create release: {response.status_code} {response.text}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error managing release: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _upload_release_asset(self, release_id: int, file_path: Path) -> bool:
|
||||||
|
"""Upload a file as a release asset.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
release_id: Gitea release ID
|
||||||
|
file_path: Path to file to upload
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if upload successful, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import requests
|
||||||
|
|
||||||
|
# Upload asset to Gitea release
|
||||||
|
upload_url = f"{self.config.repo_api_url}/releases/{release_id}/assets"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"token {self.config.auth_token}"
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(file_path, 'rb') as f:
|
||||||
|
files = {
|
||||||
|
'attachment': (file_path.name, f, 'application/octet-stream')
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
upload_url,
|
||||||
|
headers=headers,
|
||||||
|
files=files,
|
||||||
|
timeout=120 # Larger timeout for file uploads
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 201:
|
||||||
|
print(f"✅ Uploaded release asset: {file_path.name}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"❌ Failed to upload {file_path.name}: {response.status_code} {response.text}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Upload failed for {file_path.name}: {e}")
|
||||||
|
return False
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
"""
|
||||||
|
Utilities for release management.
|
||||||
|
|
||||||
|
This module provides utility functions for version management,
|
||||||
|
validation, and other common operations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .version import VersionManager
|
||||||
|
from .validation import ReleaseValidator
|
||||||
|
|
||||||
|
__all__ = ["VersionManager", "ReleaseValidator"]
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
"""
|
||||||
|
Release validation utilities.
|
||||||
|
|
||||||
|
This module provides validation functions for release readiness.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Tuple, Optional
|
||||||
|
|
||||||
|
from ..git.manager import GitManager
|
||||||
|
|
||||||
|
|
||||||
|
class ReleaseValidator:
|
||||||
|
"""Validates release readiness and requirements."""
|
||||||
|
|
||||||
|
def __init__(self, project_root: Optional[Path] = None):
|
||||||
|
"""Initialize release validator.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_root: Root directory of the project
|
||||||
|
"""
|
||||||
|
self.project_root = project_root or Path.cwd()
|
||||||
|
self.git_manager = GitManager(project_root)
|
||||||
|
|
||||||
|
def validate_release_state(self, force: bool = False) -> Tuple[bool, List[str]]:
|
||||||
|
"""Validate that repository is ready for release.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
force: Skip validation checks if True
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (is_valid, list_of_issues)
|
||||||
|
"""
|
||||||
|
if force:
|
||||||
|
return True, []
|
||||||
|
|
||||||
|
issues = []
|
||||||
|
|
||||||
|
# Git repository validation
|
||||||
|
git_issues = self._validate_git_state()
|
||||||
|
issues.extend(git_issues)
|
||||||
|
|
||||||
|
# Project structure validation
|
||||||
|
structure_issues = self._validate_project_structure()
|
||||||
|
issues.extend(structure_issues)
|
||||||
|
|
||||||
|
# Configuration validation
|
||||||
|
config_issues = self._validate_configuration()
|
||||||
|
issues.extend(config_issues)
|
||||||
|
|
||||||
|
return len(issues) == 0, issues
|
||||||
|
|
||||||
|
def _validate_git_state(self) -> List[str]:
|
||||||
|
"""Validate git repository state.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of git-related issues
|
||||||
|
"""
|
||||||
|
issues = []
|
||||||
|
status = self.git_manager.get_repository_status()
|
||||||
|
|
||||||
|
if not status['is_repo']:
|
||||||
|
issues.append("Not in a git repository")
|
||||||
|
return issues
|
||||||
|
|
||||||
|
if status['has_changes']:
|
||||||
|
issues.append("Repository has uncommitted changes")
|
||||||
|
|
||||||
|
if status['branch'] != 'main':
|
||||||
|
issues.append(f"Not on main branch (currently on {status['branch']})")
|
||||||
|
|
||||||
|
# Check if remote exists
|
||||||
|
remote_url = self.git_manager.get_remote_url()
|
||||||
|
if not remote_url:
|
||||||
|
issues.append("No git remote 'origin' configured")
|
||||||
|
|
||||||
|
return issues
|
||||||
|
|
||||||
|
def _validate_project_structure(self) -> List[str]:
|
||||||
|
"""Validate project structure for releases.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of project structure issues
|
||||||
|
"""
|
||||||
|
issues = []
|
||||||
|
|
||||||
|
# Check for required files
|
||||||
|
required_files = ['pyproject.toml']
|
||||||
|
for file_name in required_files:
|
||||||
|
file_path = self.project_root / file_name
|
||||||
|
if not file_path.exists():
|
||||||
|
issues.append(f"Missing required file: {file_name}")
|
||||||
|
|
||||||
|
# Check for setuptools-scm configuration
|
||||||
|
pyproject_path = self.project_root / 'pyproject.toml'
|
||||||
|
if pyproject_path.exists():
|
||||||
|
try:
|
||||||
|
import tomllib
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
import tomli as tomllib
|
||||||
|
except ImportError:
|
||||||
|
issues.append("Cannot read pyproject.toml (tomllib/tomli not available)")
|
||||||
|
return issues
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(pyproject_path, 'rb') as f:
|
||||||
|
config = tomllib.load(f)
|
||||||
|
|
||||||
|
# Check for setuptools-scm configuration
|
||||||
|
build_system = config.get('build-system', {})
|
||||||
|
if 'setuptools-scm' not in str(build_system.get('requires', [])):
|
||||||
|
issues.append("setuptools-scm not found in build-system.requires")
|
||||||
|
|
||||||
|
# Check for dynamic version
|
||||||
|
project_config = config.get('project', {})
|
||||||
|
if 'version' in project_config:
|
||||||
|
issues.append("Static version found in project config. Use dynamic versioning with setuptools-scm.")
|
||||||
|
|
||||||
|
dynamic = project_config.get('dynamic', [])
|
||||||
|
if 'version' not in dynamic:
|
||||||
|
issues.append("'version' not in project.dynamic. Add it for setuptools-scm.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
issues.append(f"Error reading pyproject.toml: {e}")
|
||||||
|
|
||||||
|
return issues
|
||||||
|
|
||||||
|
def _validate_configuration(self) -> List[str]:
|
||||||
|
"""Validate release configuration.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of configuration issues
|
||||||
|
"""
|
||||||
|
issues = []
|
||||||
|
|
||||||
|
# Check for environment variables that might be needed
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Check for common auth tokens (warn, don't fail)
|
||||||
|
auth_vars = ['GITEA_API_TOKEN', 'PYPI_TOKEN', 'GITHUB_TOKEN']
|
||||||
|
available_auth = [var for var in auth_vars if os.getenv(var)]
|
||||||
|
|
||||||
|
if not available_auth:
|
||||||
|
issues.append("No authentication tokens found in environment. "
|
||||||
|
"Consider setting GITEA_API_TOKEN, PYPI_TOKEN, or GITHUB_TOKEN "
|
||||||
|
"for package publishing.")
|
||||||
|
|
||||||
|
return issues
|
||||||
|
|
||||||
|
def validate_version_string(self, version_string: str) -> Tuple[bool, List[str]]:
|
||||||
|
"""Validate a version string for release.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
version_string: Version string to validate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (is_valid, list_of_issues)
|
||||||
|
"""
|
||||||
|
issues = []
|
||||||
|
|
||||||
|
if not version_string:
|
||||||
|
issues.append("Version string cannot be empty")
|
||||||
|
return False, issues
|
||||||
|
|
||||||
|
# Check basic format
|
||||||
|
if not version_string.replace('.', '').replace('-', '').replace('+', '').replace('a', '').replace('b', '').replace('rc', '').isalnum():
|
||||||
|
issues.append("Version string contains invalid characters")
|
||||||
|
|
||||||
|
# Check for development markers in release
|
||||||
|
dev_markers = ['dev', '.dev', '+dev']
|
||||||
|
if any(marker in version_string.lower() for marker in dev_markers):
|
||||||
|
issues.append("Development versions should not be released")
|
||||||
|
|
||||||
|
# Check for reasonable version format (semantic versioning)
|
||||||
|
try:
|
||||||
|
from packaging import version
|
||||||
|
version.Version(version_string)
|
||||||
|
except Exception:
|
||||||
|
issues.append("Version string is not valid according to PEP 440")
|
||||||
|
|
||||||
|
# Check if version already exists as git tag
|
||||||
|
tag_name = version_string if version_string.startswith('v') else f'v{version_string}'
|
||||||
|
if self.git_manager.tag_exists(tag_name):
|
||||||
|
issues.append(f"Git tag {tag_name} already exists")
|
||||||
|
|
||||||
|
return len(issues) == 0, issues
|
||||||
|
|
||||||
|
def get_validation_summary(self) -> dict:
|
||||||
|
"""Get a comprehensive validation summary.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with validation results
|
||||||
|
"""
|
||||||
|
is_valid, issues = self.validate_release_state()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'is_valid': is_valid,
|
||||||
|
'issues': issues,
|
||||||
|
'git_status': self.git_manager.get_repository_status(),
|
||||||
|
'recommendations': self._get_recommendations(issues)
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_recommendations(self, issues: List[str]) -> List[str]:
|
||||||
|
"""Get recommendations based on validation issues.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
issues: List of validation issues
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of recommendations
|
||||||
|
"""
|
||||||
|
recommendations = []
|
||||||
|
|
||||||
|
if any('uncommitted changes' in issue for issue in issues):
|
||||||
|
recommendations.append("Commit or stash your changes before releasing")
|
||||||
|
|
||||||
|
if any('not on main branch' in issue for issue in issues):
|
||||||
|
recommendations.append("Switch to main branch: git checkout main")
|
||||||
|
|
||||||
|
if any('setuptools-scm' in issue for issue in issues):
|
||||||
|
recommendations.append("Configure setuptools-scm in pyproject.toml")
|
||||||
|
|
||||||
|
if any('authentication' in issue.lower() for issue in issues):
|
||||||
|
recommendations.append("Set up authentication tokens for package publishing")
|
||||||
|
|
||||||
|
if not issues:
|
||||||
|
recommendations.append("Repository is ready for release!")
|
||||||
|
|
||||||
|
return recommendations
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
"""
|
||||||
|
Version management utilities.
|
||||||
|
|
||||||
|
This module provides utilities for working with versions and setuptools-scm.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
from packaging import version
|
||||||
|
|
||||||
|
|
||||||
|
class VersionManager:
|
||||||
|
"""Utilities for version management with setuptools-scm."""
|
||||||
|
|
||||||
|
def __init__(self, project_root: Optional[Path] = None):
|
||||||
|
"""Initialize version manager.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_root: Root directory of the project
|
||||||
|
"""
|
||||||
|
self.project_root = project_root or Path.cwd()
|
||||||
|
|
||||||
|
def get_current_version(self) -> str:
|
||||||
|
"""Get current version using setuptools-scm.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Current version string or "unknown" if unavailable
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
['python', '-m', 'setuptools_scm'],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
|
cwd=self.project_root
|
||||||
|
)
|
||||||
|
return result.stdout.strip()
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
def parse_version(self, version_string: str) -> Dict[str, Any]:
|
||||||
|
"""Parse a version string and return components.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
version_string: Version string to parse
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with version components
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
v = version.Version(version_string)
|
||||||
|
return {
|
||||||
|
'major': v.major,
|
||||||
|
'minor': v.minor,
|
||||||
|
'micro': v.micro,
|
||||||
|
'is_prerelease': v.is_prerelease,
|
||||||
|
'is_devrelease': v.is_devrelease,
|
||||||
|
'local': v.local,
|
||||||
|
'public': v.public,
|
||||||
|
'base_version': v.base_version,
|
||||||
|
}
|
||||||
|
except version.InvalidVersion:
|
||||||
|
return {'error': f'Invalid version: {version_string}'}
|
||||||
|
|
||||||
|
def is_development_version(self, version_string: Optional[str] = None) -> bool:
|
||||||
|
"""Check if version is a development version.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
version_string: Version to check. If None, uses current version.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if development version, False otherwise
|
||||||
|
"""
|
||||||
|
if version_string is None:
|
||||||
|
version_string = self.get_current_version()
|
||||||
|
|
||||||
|
try:
|
||||||
|
v = version.Version(version_string)
|
||||||
|
return v.is_devrelease or 'dev' in version_string.lower()
|
||||||
|
except version.InvalidVersion:
|
||||||
|
return True # Assume unknown versions are dev
|
||||||
|
|
||||||
|
def compare_versions(self, version1: str, version2: str) -> int:
|
||||||
|
"""Compare two version strings.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
version1: First version to compare
|
||||||
|
version2: Second version to compare
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
-1 if version1 < version2, 0 if equal, 1 if version1 > version2
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
v1 = version.Version(version1)
|
||||||
|
v2 = version.Version(version2)
|
||||||
|
|
||||||
|
if v1 < v2:
|
||||||
|
return -1
|
||||||
|
elif v1 > v2:
|
||||||
|
return 1
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
except version.InvalidVersion:
|
||||||
|
# Fallback to string comparison
|
||||||
|
if version1 < version2:
|
||||||
|
return -1
|
||||||
|
elif version1 > version2:
|
||||||
|
return 1
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def get_next_version(self, current_version: str, bump_type: str = 'patch') -> str:
|
||||||
|
"""Get the next version based on bump type.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
current_version: Current version string
|
||||||
|
bump_type: Type of bump ('major', 'minor', 'patch')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Next version string
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If bump_type is invalid
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
v = version.Version(current_version)
|
||||||
|
major, minor, micro = v.major, v.minor, v.micro
|
||||||
|
|
||||||
|
if bump_type == 'major':
|
||||||
|
return f"{major + 1}.0.0"
|
||||||
|
elif bump_type == 'minor':
|
||||||
|
return f"{major}.{minor + 1}.0"
|
||||||
|
elif bump_type == 'patch':
|
||||||
|
return f"{major}.{minor}.{micro + 1}"
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Invalid bump type: {bump_type}")
|
||||||
|
|
||||||
|
except version.InvalidVersion:
|
||||||
|
raise ValueError(f"Cannot parse version: {current_version}")
|
||||||
|
|
||||||
|
def suggest_version(self, current_version: Optional[str] = None) -> Dict[str, str]:
|
||||||
|
"""Suggest next version options.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
current_version: Current version. If None, gets from setuptools-scm.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with version suggestions
|
||||||
|
"""
|
||||||
|
if current_version is None:
|
||||||
|
current_version = self.get_current_version()
|
||||||
|
|
||||||
|
if current_version == "unknown":
|
||||||
|
return {
|
||||||
|
'error': 'Cannot determine current version',
|
||||||
|
'suggestion': 'Consider creating an initial tag like v0.1.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Strip development version info to get base
|
||||||
|
v = version.Version(current_version)
|
||||||
|
base_version = v.base_version
|
||||||
|
|
||||||
|
return {
|
||||||
|
'current': current_version,
|
||||||
|
'base': base_version,
|
||||||
|
'patch': self.get_next_version(base_version, 'patch'),
|
||||||
|
'minor': self.get_next_version(base_version, 'minor'),
|
||||||
|
'major': self.get_next_version(base_version, 'major'),
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
'error': str(e),
|
||||||
|
'current': current_version
|
||||||
|
}
|
||||||
|
|
||||||
|
def validate_version_format(self, version_string: str) -> bool:
|
||||||
|
"""Validate if a version string follows semantic versioning.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
version_string: Version string to validate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if valid semantic version, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
version.Version(version_string)
|
||||||
|
return True
|
||||||
|
except version.InvalidVersion:
|
||||||
|
return False
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
"""
|
|
||||||
Gitea API facade - Clean interface for Gitea repository operations.
|
|
||||||
|
|
||||||
This package provides a clean, well-structured interface to Gitea API operations,
|
|
||||||
following the facade pattern to decouple application logic from specific API
|
|
||||||
implementation details.
|
|
||||||
|
|
||||||
Structure:
|
|
||||||
- client: Main GiteaClient facade
|
|
||||||
- models: Domain models (Issue, Milestone, Label, etc.)
|
|
||||||
- config: Gitea-specific configuration
|
|
||||||
- exceptions: Gitea-specific exceptions
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
from gitea import GiteaClient
|
|
||||||
|
|
||||||
client = GiteaClient()
|
|
||||||
issues = client.issues.list()
|
|
||||||
issue = client.issues.get(42)
|
|
||||||
client.issues.create("Bug fix", "Description")
|
|
||||||
"""
|
|
||||||
|
|
||||||
from .client import GiteaClient
|
|
||||||
from .models import Issue, Milestone, Label, ProjectState, Priority
|
|
||||||
from .config import GiteaConfig
|
|
||||||
from .exceptions import GiteaError, GiteaAuthError, GiteaNotFoundError
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'GiteaClient',
|
|
||||||
'Issue', 'Milestone', 'Label', 'ProjectState', 'Priority',
|
|
||||||
'GiteaConfig',
|
|
||||||
'GiteaError', 'GiteaAuthError', 'GiteaNotFoundError'
|
|
||||||
]
|
|
||||||
@@ -1,241 +0,0 @@
|
|||||||
"""
|
|
||||||
High-level API client that converts between API responses and domain models.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
from typing import List, Optional, Dict, Any
|
|
||||||
|
|
||||||
from .http_client import GiteaHttpClient
|
|
||||||
from .models import Issue, Milestone, Label, User, IssueCreateData, IssueUpdateData, MilestoneCreateData, LabelCreateData
|
|
||||||
from .config import GiteaConfig
|
|
||||||
from .exceptions import GiteaNotFoundError, GiteaError
|
|
||||||
|
|
||||||
|
|
||||||
class GiteaApiClient:
|
|
||||||
"""High-level API client with domain model conversion."""
|
|
||||||
|
|
||||||
def __init__(self, config: GiteaConfig):
|
|
||||||
self.config = config
|
|
||||||
self.http = GiteaHttpClient(config)
|
|
||||||
|
|
||||||
# Issue operations
|
|
||||||
def get_issue(self, issue_number: int) -> Issue:
|
|
||||||
"""Get a specific issue by number."""
|
|
||||||
try:
|
|
||||||
url = f"{self.config.issues_api_url}/{issue_number}"
|
|
||||||
data = self.http.get(url)
|
|
||||||
return self._parse_issue(data)
|
|
||||||
except GiteaError as e:
|
|
||||||
if "not found" in str(e).lower():
|
|
||||||
raise GiteaNotFoundError(f"Issue #{issue_number} not found")
|
|
||||||
raise
|
|
||||||
|
|
||||||
def list_issues(self, state: str = "all", page: int = 1, per_page: int = 50) -> List[Issue]:
|
|
||||||
"""List issues with optional filtering."""
|
|
||||||
params = {"page": str(page), "limit": str(per_page)}
|
|
||||||
if state != "all":
|
|
||||||
params["state"] = state
|
|
||||||
|
|
||||||
data = self.http.get(self.config.issues_api_url, params)
|
|
||||||
|
|
||||||
if not isinstance(data, list):
|
|
||||||
raise GiteaError("Invalid response format: expected list of issues")
|
|
||||||
|
|
||||||
return [self._parse_issue(issue_data) for issue_data in data]
|
|
||||||
|
|
||||||
def create_issue(self, issue_data: IssueCreateData) -> Issue:
|
|
||||||
"""Create a new issue."""
|
|
||||||
payload = {
|
|
||||||
"title": issue_data.title,
|
|
||||||
"body": issue_data.body,
|
|
||||||
}
|
|
||||||
|
|
||||||
if issue_data.assignees:
|
|
||||||
payload["assignees"] = issue_data.assignees
|
|
||||||
if issue_data.milestone:
|
|
||||||
payload["milestone"] = issue_data.milestone
|
|
||||||
if issue_data.labels:
|
|
||||||
# Convert label names to label IDs
|
|
||||||
payload["labels"] = self._resolve_label_ids(issue_data.labels)
|
|
||||||
|
|
||||||
data = self.http.post(self.config.issues_api_url, payload)
|
|
||||||
return self._parse_issue(data)
|
|
||||||
|
|
||||||
def update_issue(self, issue_number: int, update_data: IssueUpdateData) -> Issue:
|
|
||||||
"""Update an existing issue."""
|
|
||||||
payload = {}
|
|
||||||
|
|
||||||
if update_data.title is not None:
|
|
||||||
payload["title"] = update_data.title
|
|
||||||
if update_data.body is not None:
|
|
||||||
payload["body"] = update_data.body
|
|
||||||
if update_data.state is not None:
|
|
||||||
payload["state"] = update_data.state
|
|
||||||
if update_data.assignees is not None:
|
|
||||||
payload["assignees"] = update_data.assignees
|
|
||||||
if update_data.milestone is not None:
|
|
||||||
payload["milestone"] = update_data.milestone
|
|
||||||
if update_data.labels is not None:
|
|
||||||
payload["labels"] = update_data.labels
|
|
||||||
|
|
||||||
url = f"{self.config.issues_api_url}/{issue_number}"
|
|
||||||
data = self.http.patch(url, payload)
|
|
||||||
return self._parse_issue(data)
|
|
||||||
|
|
||||||
# Milestone operations
|
|
||||||
def list_milestones(self, state: str = "all") -> List[Milestone]:
|
|
||||||
"""List repository milestones."""
|
|
||||||
params = {}
|
|
||||||
if state != "all":
|
|
||||||
params["state"] = state
|
|
||||||
|
|
||||||
data = self.http.get(self.config.milestones_api_url, params)
|
|
||||||
|
|
||||||
if not isinstance(data, list):
|
|
||||||
raise GiteaError("Invalid response format: expected list of milestones")
|
|
||||||
|
|
||||||
return [self._parse_milestone(milestone_data) for milestone_data in data]
|
|
||||||
|
|
||||||
def create_milestone(self, milestone_data: MilestoneCreateData) -> Milestone:
|
|
||||||
"""Create a new milestone."""
|
|
||||||
payload = {
|
|
||||||
"title": milestone_data.title,
|
|
||||||
"description": milestone_data.description,
|
|
||||||
}
|
|
||||||
|
|
||||||
if milestone_data.due_on:
|
|
||||||
payload["due_on"] = milestone_data.due_on
|
|
||||||
|
|
||||||
data = self.http.post(self.config.milestones_api_url, payload)
|
|
||||||
return self._parse_milestone(data)
|
|
||||||
|
|
||||||
# Label operations
|
|
||||||
def list_labels(self) -> List[Label]:
|
|
||||||
"""List repository labels."""
|
|
||||||
data = self.http.get(self.config.labels_api_url)
|
|
||||||
|
|
||||||
if not isinstance(data, list):
|
|
||||||
raise GiteaError("Invalid response format: expected list of labels")
|
|
||||||
|
|
||||||
return [self._parse_label(label_data) for label_data in data]
|
|
||||||
|
|
||||||
def create_label(self, label_data: LabelCreateData) -> Label:
|
|
||||||
"""Create a new label."""
|
|
||||||
payload = {
|
|
||||||
"name": label_data.name,
|
|
||||||
"color": label_data.color,
|
|
||||||
"description": label_data.description,
|
|
||||||
}
|
|
||||||
|
|
||||||
data = self.http.post(self.config.labels_api_url, payload)
|
|
||||||
return self._parse_label(data)
|
|
||||||
|
|
||||||
# Parsing methods
|
|
||||||
def _parse_issue(self, data: Dict[str, Any]) -> Issue:
|
|
||||||
"""Parse issue data from API response."""
|
|
||||||
try:
|
|
||||||
# Parse labels
|
|
||||||
labels = []
|
|
||||||
if data.get('labels'):
|
|
||||||
labels = [self._parse_label(label_data) for label_data in data['labels']]
|
|
||||||
|
|
||||||
# Parse assignee
|
|
||||||
assignee = None
|
|
||||||
if data.get('assignee'):
|
|
||||||
assignee = self._parse_user(data['assignee'])
|
|
||||||
|
|
||||||
# Parse milestone
|
|
||||||
milestone = None
|
|
||||||
if data.get('milestone'):
|
|
||||||
milestone = self._parse_milestone(data['milestone'])
|
|
||||||
|
|
||||||
# Check if this is an error response
|
|
||||||
if 'message' in data and 'url' in data and 'number' not in data and 'id' not in data:
|
|
||||||
raise GiteaError(f"API Error: {data.get('message', 'Unknown error')} (URL: {data.get('url', 'N/A')})")
|
|
||||||
|
|
||||||
# Handle both 'number' and 'id' fields (Gitea API might use either)
|
|
||||||
issue_number = data.get('number') or data.get('id')
|
|
||||||
if issue_number is None:
|
|
||||||
raise GiteaError(f"Issue response missing both 'number' and 'id' fields. Available fields: {list(data.keys())}")
|
|
||||||
|
|
||||||
return Issue(
|
|
||||||
number=issue_number,
|
|
||||||
title=data['title'],
|
|
||||||
body=data.get('body', ''),
|
|
||||||
state=data['state'],
|
|
||||||
created_at=self._parse_datetime(data['created_at']),
|
|
||||||
updated_at=self._parse_datetime(data['updated_at']),
|
|
||||||
html_url=data['html_url'],
|
|
||||||
assignee=assignee,
|
|
||||||
labels=labels,
|
|
||||||
milestone=milestone
|
|
||||||
)
|
|
||||||
except (KeyError, ValueError) as e:
|
|
||||||
raise GiteaError(f"Failed to parse issue data: {e}")
|
|
||||||
|
|
||||||
def _parse_milestone(self, data: Dict[str, Any]) -> Milestone:
|
|
||||||
"""Parse milestone data from API response."""
|
|
||||||
return Milestone(
|
|
||||||
id=data['id'],
|
|
||||||
title=data['title'],
|
|
||||||
description=data.get('description', ''),
|
|
||||||
state=data['state'],
|
|
||||||
open_issues=data.get('open_issues', 0),
|
|
||||||
closed_issues=data.get('closed_issues', 0),
|
|
||||||
due_on=data.get('due_on'),
|
|
||||||
created_at=self._parse_datetime(data.get('created_at')) if data.get('created_at') else None,
|
|
||||||
updated_at=self._parse_datetime(data.get('updated_at')) if data.get('updated_at') else None
|
|
||||||
)
|
|
||||||
|
|
||||||
def _parse_label(self, data: Dict[str, Any]) -> Label:
|
|
||||||
"""Parse label data from API response."""
|
|
||||||
return Label(
|
|
||||||
id=data['id'],
|
|
||||||
name=data['name'],
|
|
||||||
color=data['color'],
|
|
||||||
description=data.get('description', '')
|
|
||||||
)
|
|
||||||
|
|
||||||
def _parse_user(self, data: Dict[str, Any]) -> User:
|
|
||||||
"""Parse user data from API response."""
|
|
||||||
return User(
|
|
||||||
id=data['id'],
|
|
||||||
login=data['login'],
|
|
||||||
full_name=data.get('full_name', ''),
|
|
||||||
email=data.get('email', ''),
|
|
||||||
avatar_url=data.get('avatar_url', '')
|
|
||||||
)
|
|
||||||
|
|
||||||
def _parse_datetime(self, date_str: str) -> datetime:
|
|
||||||
"""Parse datetime from API response."""
|
|
||||||
# Remove Z and microseconds for consistent parsing
|
|
||||||
date_str = date_str.replace('Z', '').split('.')[0]
|
|
||||||
return datetime.strptime(date_str, '%Y-%m-%dT%H:%M:%S')
|
|
||||||
|
|
||||||
def _resolve_label_ids(self, label_names: List[str]) -> List[int]:
|
|
||||||
"""Convert label names to label IDs for API calls."""
|
|
||||||
try:
|
|
||||||
# Get all labels for the repository
|
|
||||||
labels_data = self.http.get(self.config.labels_api_url)
|
|
||||||
if not isinstance(labels_data, list):
|
|
||||||
raise GiteaError("Invalid labels response format")
|
|
||||||
|
|
||||||
# Create name-to-ID mapping
|
|
||||||
label_map = {label_data['name']: label_data['id'] for label_data in labels_data}
|
|
||||||
|
|
||||||
# Resolve names to IDs
|
|
||||||
label_ids = []
|
|
||||||
for name in label_names:
|
|
||||||
if name in label_map:
|
|
||||||
label_ids.append(label_map[name])
|
|
||||||
else:
|
|
||||||
# If label doesn't exist, we could create it or skip it
|
|
||||||
# For now, let's skip non-existent labels
|
|
||||||
print(f"Warning: Label '{name}' not found, skipping")
|
|
||||||
|
|
||||||
return label_ids
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
# If label resolution fails, proceed without labels rather than failing entirely
|
|
||||||
print(f"Warning: Could not resolve labels: {e}")
|
|
||||||
return []
|
|
||||||
237
gitea/client.py
237
gitea/client.py
@@ -1,237 +0,0 @@
|
|||||||
"""
|
|
||||||
Main Gitea client facade.
|
|
||||||
|
|
||||||
This provides a clean, organized interface for all Gitea operations,
|
|
||||||
following the facade pattern to hide complexity and provide a stable API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing import List, Optional, Dict, Any
|
|
||||||
|
|
||||||
from .config import GiteaConfig
|
|
||||||
from .api_client import GiteaApiClient
|
|
||||||
from .models import Issue, Milestone, Label, IssueCreateData, IssueUpdateData, MilestoneCreateData, LabelCreateData, ProjectState, Priority
|
|
||||||
|
|
||||||
|
|
||||||
class IssuesClient:
|
|
||||||
"""Client for issue operations."""
|
|
||||||
|
|
||||||
def __init__(self, api_client: GiteaApiClient):
|
|
||||||
self._api = api_client
|
|
||||||
|
|
||||||
def get(self, issue_number: int) -> Issue:
|
|
||||||
"""Get a specific issue by number."""
|
|
||||||
return self._api.get_issue(issue_number)
|
|
||||||
|
|
||||||
def list(self, state: str = "all", page: int = 1, per_page: int = 50) -> List[Issue]:
|
|
||||||
"""List issues with optional filtering."""
|
|
||||||
return self._api.list_issues(state, page, per_page)
|
|
||||||
|
|
||||||
def list_open(self) -> List[Issue]:
|
|
||||||
"""List only open issues."""
|
|
||||||
return self._api.list_issues("open")
|
|
||||||
|
|
||||||
def list_closed(self) -> List[Issue]:
|
|
||||||
"""List only closed issues."""
|
|
||||||
return self._api.list_issues("closed")
|
|
||||||
|
|
||||||
def create(self, title: str, body: str = "", **kwargs) -> Issue:
|
|
||||||
"""Create a new issue."""
|
|
||||||
issue_data = IssueCreateData(
|
|
||||||
title=title,
|
|
||||||
body=body,
|
|
||||||
assignees=kwargs.get('assignees', []),
|
|
||||||
milestone=kwargs.get('milestone'),
|
|
||||||
labels=kwargs.get('labels', [])
|
|
||||||
)
|
|
||||||
return self._api.create_issue(issue_data)
|
|
||||||
|
|
||||||
def update(self, issue_number: int, **kwargs) -> Issue:
|
|
||||||
"""Update an existing issue."""
|
|
||||||
update_data = IssueUpdateData(
|
|
||||||
title=kwargs.get('title'),
|
|
||||||
body=kwargs.get('body'),
|
|
||||||
state=kwargs.get('state'),
|
|
||||||
assignees=kwargs.get('assignees'),
|
|
||||||
milestone=kwargs.get('milestone'),
|
|
||||||
labels=kwargs.get('labels')
|
|
||||||
)
|
|
||||||
return self._api.update_issue(issue_number, update_data)
|
|
||||||
|
|
||||||
def close(self, issue_number: int) -> Issue:
|
|
||||||
"""Close an issue."""
|
|
||||||
return self.update(issue_number, state="closed")
|
|
||||||
|
|
||||||
def reopen(self, issue_number: int) -> Issue:
|
|
||||||
"""Reopen an issue."""
|
|
||||||
return self.update(issue_number, state="open")
|
|
||||||
|
|
||||||
def add_labels(self, issue_number: int, labels: List[str]) -> Issue:
|
|
||||||
"""Add labels to an issue."""
|
|
||||||
issue = self.get(issue_number)
|
|
||||||
existing_labels = [label.name for label in issue.labels]
|
|
||||||
new_labels = list(set(existing_labels + labels))
|
|
||||||
return self.update(issue_number, labels=new_labels)
|
|
||||||
|
|
||||||
def remove_labels(self, issue_number: int, labels: List[str]) -> Issue:
|
|
||||||
"""Remove labels from an issue."""
|
|
||||||
issue = self.get(issue_number)
|
|
||||||
existing_labels = [label.name for label in issue.labels]
|
|
||||||
new_labels = [label for label in existing_labels if label not in labels]
|
|
||||||
return self.update(issue_number, labels=new_labels)
|
|
||||||
|
|
||||||
def set_priority(self, issue_number: int, priority: Priority) -> Issue:
|
|
||||||
"""Set issue priority."""
|
|
||||||
issue = self.get(issue_number)
|
|
||||||
labels = [label.name for label in issue.labels if not label.name.startswith('priority:')]
|
|
||||||
labels.append(priority.value)
|
|
||||||
return self.update(issue_number, labels=labels)
|
|
||||||
|
|
||||||
def set_status(self, issue_number: int, status: ProjectState) -> Issue:
|
|
||||||
"""Set issue status."""
|
|
||||||
issue = self.get(issue_number)
|
|
||||||
labels = [label.name for label in issue.labels if not label.name.startswith('status:')]
|
|
||||||
labels.append(status.value)
|
|
||||||
return self.update(issue_number, labels=labels)
|
|
||||||
|
|
||||||
def assign_to_milestone(self, issue_number: int, milestone_id: int) -> Issue:
|
|
||||||
"""Assign issue to a milestone."""
|
|
||||||
return self.update(issue_number, milestone=milestone_id)
|
|
||||||
|
|
||||||
def remove_from_milestone(self, issue_number: int) -> Issue:
|
|
||||||
"""Remove issue from milestone."""
|
|
||||||
return self.update(issue_number, milestone=None)
|
|
||||||
|
|
||||||
def set_labels(self, issue_number: int, labels: List[str]) -> Issue:
|
|
||||||
"""Replace all labels on an issue."""
|
|
||||||
return self.update(issue_number, labels=labels)
|
|
||||||
|
|
||||||
def update_title(self, issue_number: int, title: str) -> Issue:
|
|
||||||
"""Update only the title of an issue."""
|
|
||||||
return self.update(issue_number, title=title)
|
|
||||||
|
|
||||||
def update_body(self, issue_number: int, body: str) -> Issue:
|
|
||||||
"""Update only the body of an issue."""
|
|
||||||
return self.update(issue_number, body=body)
|
|
||||||
|
|
||||||
def to_dict(self, issue: Issue) -> Dict[str, Any]:
|
|
||||||
"""Convert Issue object to dictionary format for backward compatibility."""
|
|
||||||
return {
|
|
||||||
'number': issue.number,
|
|
||||||
'title': issue.title,
|
|
||||||
'body': issue.body,
|
|
||||||
'state': issue.state,
|
|
||||||
'html_url': issue.html_url,
|
|
||||||
'created_at': issue.created_at.isoformat(),
|
|
||||||
'updated_at': issue.updated_at.isoformat(),
|
|
||||||
'assignee': {'login': issue.assignee.login} if issue.assignee else None,
|
|
||||||
'labels': [{'name': label.name, 'color': label.color} for label in issue.labels],
|
|
||||||
'milestone': {
|
|
||||||
'id': issue.milestone.id,
|
|
||||||
'title': issue.milestone.title
|
|
||||||
} if issue.milestone else None
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class MilestonesClient:
|
|
||||||
"""Client for milestone operations."""
|
|
||||||
|
|
||||||
def __init__(self, api_client: GiteaApiClient):
|
|
||||||
self._api = api_client
|
|
||||||
|
|
||||||
def list(self, state: str = "all") -> List[Milestone]:
|
|
||||||
"""List milestones."""
|
|
||||||
return self._api.list_milestones(state)
|
|
||||||
|
|
||||||
def list_open(self) -> List[Milestone]:
|
|
||||||
"""List open milestones."""
|
|
||||||
return self._api.list_milestones("open")
|
|
||||||
|
|
||||||
def list_closed(self) -> List[Milestone]:
|
|
||||||
"""List closed milestones."""
|
|
||||||
return self._api.list_milestones("closed")
|
|
||||||
|
|
||||||
def create(self, title: str, description: str = "", due_on: str = None) -> Milestone:
|
|
||||||
"""Create a new milestone."""
|
|
||||||
milestone_data = MilestoneCreateData(
|
|
||||||
title=title,
|
|
||||||
description=description,
|
|
||||||
due_on=due_on
|
|
||||||
)
|
|
||||||
return self._api.create_milestone(milestone_data)
|
|
||||||
|
|
||||||
|
|
||||||
class LabelsClient:
|
|
||||||
"""Client for label operations."""
|
|
||||||
|
|
||||||
def __init__(self, api_client: GiteaApiClient):
|
|
||||||
self._api = api_client
|
|
||||||
|
|
||||||
def list(self) -> List[Label]:
|
|
||||||
"""List all labels."""
|
|
||||||
return self._api.list_labels()
|
|
||||||
|
|
||||||
def create(self, name: str, color: str, description: str = "") -> Label:
|
|
||||||
"""Create a new label."""
|
|
||||||
label_data = LabelCreateData(
|
|
||||||
name=name,
|
|
||||||
color=color,
|
|
||||||
description=description
|
|
||||||
)
|
|
||||||
return self._api.create_label(label_data)
|
|
||||||
|
|
||||||
def ensure_project_labels(self) -> None:
|
|
||||||
"""Ensure all standard project management labels exist."""
|
|
||||||
existing_labels = [label.name for label in self.list()]
|
|
||||||
|
|
||||||
# Define standard project labels
|
|
||||||
standard_labels = [
|
|
||||||
("status:todo", "d73a4a", "Ready to work on"),
|
|
||||||
("status:active", "0075ca", "Currently being worked on"),
|
|
||||||
("status:review", "fbca04", "Ready for review"),
|
|
||||||
("status:done", "0e8a16", "Completed work"),
|
|
||||||
("status:blocked", "b60205", "Blocked by dependencies"),
|
|
||||||
("priority:low", "c5def5", "Low priority"),
|
|
||||||
("priority:medium", "a2eeef", "Medium priority"),
|
|
||||||
("priority:high", "fef2c0", "High priority"),
|
|
||||||
("priority:critical", "d93f0b", "Critical priority"),
|
|
||||||
]
|
|
||||||
|
|
||||||
for name, color, description in standard_labels:
|
|
||||||
if name not in existing_labels:
|
|
||||||
self.create(name, color, description)
|
|
||||||
|
|
||||||
|
|
||||||
class GiteaClient:
|
|
||||||
"""Main Gitea client facade."""
|
|
||||||
|
|
||||||
def __init__(self, config: Optional[GiteaConfig] = None):
|
|
||||||
"""Initialize Gitea client.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
config: GiteaConfig instance. If None, auto-detects from git repository.
|
|
||||||
"""
|
|
||||||
if config is None:
|
|
||||||
try:
|
|
||||||
config = GiteaConfig.from_git_repository()
|
|
||||||
except Exception:
|
|
||||||
# Fallback to environment-based config if git detection fails
|
|
||||||
config = GiteaConfig.from_environment()
|
|
||||||
config.validate()
|
|
||||||
|
|
||||||
self.config = config
|
|
||||||
self._api = GiteaApiClient(config)
|
|
||||||
|
|
||||||
# Initialize sub-clients
|
|
||||||
self.issues = IssuesClient(self._api)
|
|
||||||
self.milestones = MilestonesClient(self._api)
|
|
||||||
self.labels = LabelsClient(self._api)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_tddai_config(cls, tddai_config) -> 'GiteaClient':
|
|
||||||
"""Create client from legacy TddaiConfig for backwards compatibility."""
|
|
||||||
gitea_config = GiteaConfig.from_tddai_config(tddai_config)
|
|
||||||
return cls(gitea_config)
|
|
||||||
|
|
||||||
def setup_project_management(self) -> None:
|
|
||||||
"""Setup standard project management labels and structure."""
|
|
||||||
self.labels.ensure_project_labels()
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
"""
|
|
||||||
Gitea-specific exceptions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class GiteaError(Exception):
|
|
||||||
"""Base exception for Gitea API operations."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class GiteaAuthError(GiteaError):
|
|
||||||
"""Raised when authentication fails or token is missing."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class GiteaNotFoundError(GiteaError):
|
|
||||||
"""Raised when requested resource is not found."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class GiteaApiError(GiteaError):
|
|
||||||
"""Raised when API returns an error response."""
|
|
||||||
|
|
||||||
def __init__(self, message: str, status_code: int = None):
|
|
||||||
super().__init__(message)
|
|
||||||
self.status_code = status_code
|
|
||||||
|
|
||||||
|
|
||||||
class GiteaConfigError(GiteaError):
|
|
||||||
"""Raised when Gitea configuration is invalid or missing."""
|
|
||||||
pass
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
"""
|
|
||||||
Low-level HTTP client for Gitea API operations.
|
|
||||||
|
|
||||||
This module handles the actual HTTP requests to Gitea API using subprocess + curl
|
|
||||||
for maximum compatibility and minimal dependencies.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import subprocess
|
|
||||||
from subprocess import PIPE
|
|
||||||
from typing import Dict, Any, Optional, List
|
|
||||||
|
|
||||||
from .exceptions import GiteaError, GiteaApiError, GiteaAuthError
|
|
||||||
from .config import GiteaConfig
|
|
||||||
|
|
||||||
|
|
||||||
class GiteaHttpClient:
|
|
||||||
"""Low-level HTTP client for Gitea API."""
|
|
||||||
|
|
||||||
def __init__(self, config: GiteaConfig):
|
|
||||||
self.config = config
|
|
||||||
|
|
||||||
def get(self, url: str, params: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
|
|
||||||
"""Make GET request to Gitea API."""
|
|
||||||
if params:
|
|
||||||
param_string = '&'.join(f"{k}={v}" for k, v in params.items())
|
|
||||||
url = f"{url}?{param_string}"
|
|
||||||
|
|
||||||
return self._make_request('GET', url)
|
|
||||||
|
|
||||||
def post(self, url: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
||||||
"""Make POST request to Gitea API."""
|
|
||||||
self._require_auth()
|
|
||||||
return self._make_request('POST', url, data)
|
|
||||||
|
|
||||||
def patch(self, url: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
||||||
"""Make PATCH request to Gitea API."""
|
|
||||||
self._require_auth()
|
|
||||||
return self._make_request('PATCH', url, data)
|
|
||||||
|
|
||||||
def delete(self, url: str) -> Dict[str, Any]:
|
|
||||||
"""Make DELETE request to Gitea API."""
|
|
||||||
self._require_auth()
|
|
||||||
return self._make_request('DELETE', url)
|
|
||||||
|
|
||||||
def _make_request(self, method: str, url: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
||||||
"""Make HTTP request using curl."""
|
|
||||||
cmd = ['curl', '-s', '-X', method]
|
|
||||||
|
|
||||||
# Add authentication if available
|
|
||||||
if self.config.auth_token:
|
|
||||||
cmd.extend(['-H', f'Authorization: token {self.config.auth_token}'])
|
|
||||||
|
|
||||||
# Add content type for requests with data
|
|
||||||
if data is not None:
|
|
||||||
cmd.extend(['-H', 'Content-Type: application/json'])
|
|
||||||
cmd.extend(['-d', json.dumps(data)])
|
|
||||||
|
|
||||||
cmd.append(url)
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
cmd,
|
|
||||||
stdout=PIPE,
|
|
||||||
stderr=PIPE,
|
|
||||||
universal_newlines=True,
|
|
||||||
check=True
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.returncode != 0:
|
|
||||||
raise GiteaApiError(f"HTTP request failed: {result.stderr}")
|
|
||||||
|
|
||||||
# Handle empty responses
|
|
||||||
if not result.stdout.strip():
|
|
||||||
return {}
|
|
||||||
|
|
||||||
response_data = json.loads(result.stdout)
|
|
||||||
|
|
||||||
# Check for API error responses
|
|
||||||
if isinstance(response_data, dict):
|
|
||||||
if 'message' in response_data:
|
|
||||||
# This could be an error or just a response with a message field
|
|
||||||
# We need to distinguish based on context or HTTP status
|
|
||||||
if any(error_word in response_data['message'].lower()
|
|
||||||
for error_word in ['error', 'not found', 'forbidden', 'unauthorized']):
|
|
||||||
raise GiteaApiError(response_data['message'])
|
|
||||||
|
|
||||||
return response_data
|
|
||||||
|
|
||||||
except subprocess.CalledProcessError as e:
|
|
||||||
raise GiteaApiError(f"HTTP request failed: {e.stderr}")
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
raise GiteaError(f"Failed to parse API response: {e}")
|
|
||||||
|
|
||||||
def _require_auth(self):
|
|
||||||
"""Ensure authentication token is available."""
|
|
||||||
if not self.config.auth_token:
|
|
||||||
raise GiteaAuthError("Authentication token required for this operation")
|
|
||||||
151
gitea/models.py
151
gitea/models.py
@@ -1,151 +0,0 @@
|
|||||||
"""
|
|
||||||
Gitea domain models.
|
|
||||||
|
|
||||||
These models represent the core entities in Gitea and provide a clean interface
|
|
||||||
independent of the underlying API representation.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from datetime import datetime
|
|
||||||
from enum import Enum
|
|
||||||
from typing import List, Optional, Dict, Any
|
|
||||||
|
|
||||||
|
|
||||||
class ProjectState(Enum):
|
|
||||||
"""Standard project states using labels."""
|
|
||||||
TODO = "status:todo"
|
|
||||||
ACTIVE = "status:active"
|
|
||||||
REVIEW = "status:review"
|
|
||||||
DONE = "status:done"
|
|
||||||
BLOCKED = "status:blocked"
|
|
||||||
|
|
||||||
|
|
||||||
class Priority(Enum):
|
|
||||||
"""Priority levels using labels."""
|
|
||||||
LOW = "priority:low"
|
|
||||||
MEDIUM = "priority:medium"
|
|
||||||
HIGH = "priority:high"
|
|
||||||
CRITICAL = "priority:critical"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Label:
|
|
||||||
"""Represents a Gitea issue label."""
|
|
||||||
id: int
|
|
||||||
name: str
|
|
||||||
color: str
|
|
||||||
description: str = ""
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class User:
|
|
||||||
"""Represents a Gitea user."""
|
|
||||||
id: int
|
|
||||||
login: str
|
|
||||||
full_name: str = ""
|
|
||||||
email: str = ""
|
|
||||||
avatar_url: str = ""
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Milestone:
|
|
||||||
"""Represents a Gitea milestone (used as projects)."""
|
|
||||||
id: int
|
|
||||||
title: str
|
|
||||||
description: str
|
|
||||||
state: str # 'open' or 'closed'
|
|
||||||
open_issues: int
|
|
||||||
closed_issues: int
|
|
||||||
due_on: Optional[str] = None
|
|
||||||
created_at: Optional[datetime] = None
|
|
||||||
updated_at: Optional[datetime] = None
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Issue:
|
|
||||||
"""Represents a Gitea issue."""
|
|
||||||
number: int
|
|
||||||
title: str
|
|
||||||
body: str
|
|
||||||
state: str # 'open' or 'closed'
|
|
||||||
created_at: datetime
|
|
||||||
updated_at: datetime
|
|
||||||
html_url: str
|
|
||||||
assignee: Optional[User] = None
|
|
||||||
labels: List[Label] = None
|
|
||||||
milestone: Optional[Milestone] = None
|
|
||||||
|
|
||||||
def __post_init__(self):
|
|
||||||
if self.labels is None:
|
|
||||||
self.labels = []
|
|
||||||
|
|
||||||
@property
|
|
||||||
def priority(self) -> Optional[str]:
|
|
||||||
"""Get issue priority from labels."""
|
|
||||||
for label in self.labels:
|
|
||||||
if label.name.startswith('priority:'):
|
|
||||||
return label.name.replace('priority:', '')
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def status(self) -> Optional[str]:
|
|
||||||
"""Get issue status from labels."""
|
|
||||||
for label in self.labels:
|
|
||||||
if label.name.startswith('status:'):
|
|
||||||
return label.name.replace('status:', '')
|
|
||||||
return None
|
|
||||||
|
|
||||||
def has_label(self, label_name: str) -> bool:
|
|
||||||
"""Check if issue has a specific label."""
|
|
||||||
return any(label.name == label_name for label in self.labels)
|
|
||||||
|
|
||||||
def has_priority(self, priority: Priority) -> bool:
|
|
||||||
"""Check if issue has a specific priority."""
|
|
||||||
return self.has_label(priority.value)
|
|
||||||
|
|
||||||
def has_status(self, status: ProjectState) -> bool:
|
|
||||||
"""Check if issue has a specific status."""
|
|
||||||
return self.has_label(status.value)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class IssueCreateData:
|
|
||||||
"""Data for creating a new issue."""
|
|
||||||
title: str
|
|
||||||
body: str = ""
|
|
||||||
assignees: List[str] = None
|
|
||||||
milestone: Optional[int] = None
|
|
||||||
labels: List[str] = None
|
|
||||||
|
|
||||||
def __post_init__(self):
|
|
||||||
if self.assignees is None:
|
|
||||||
self.assignees = []
|
|
||||||
if self.labels is None:
|
|
||||||
self.labels = []
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class IssueUpdateData:
|
|
||||||
"""Data for updating an existing issue."""
|
|
||||||
title: Optional[str] = None
|
|
||||||
body: Optional[str] = None
|
|
||||||
state: Optional[str] = None
|
|
||||||
assignees: Optional[List[str]] = None
|
|
||||||
milestone: Optional[int] = None
|
|
||||||
labels: Optional[List[str]] = None
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class MilestoneCreateData:
|
|
||||||
"""Data for creating a new milestone."""
|
|
||||||
title: str
|
|
||||||
description: str = ""
|
|
||||||
due_on: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class LabelCreateData:
|
|
||||||
"""Data for creating a new label."""
|
|
||||||
name: str
|
|
||||||
color: str
|
|
||||||
description: str = ""
|
|
||||||
@@ -1,123 +1,16 @@
|
|||||||
"""
|
"""
|
||||||
Version information for MarkiTect.
|
Version information for MarkiTect.
|
||||||
|
|
||||||
This module provides version and release information for the MarkiTect package.
|
This module provides version information using setuptools-scm.
|
||||||
Version information is sourced from pyproject.toml and git metadata when available.
|
Version is automatically derived from git tags.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
try:
|
||||||
import subprocess
|
from ._version import version as __version__
|
||||||
from pathlib import Path
|
except ImportError:
|
||||||
from typing import Optional
|
# Fallback when _version.py is not available (e.g., during development without setuptools-scm)
|
||||||
|
__version__ = "unknown"
|
||||||
|
|
||||||
# Base version from pyproject.toml
|
def get_version():
|
||||||
__version__ = "0.5.0"
|
"""Get the current version string."""
|
||||||
|
return __version__
|
||||||
def get_git_commit_hash() -> Optional[str]:
|
|
||||||
"""Get the current git commit hash if available."""
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
['git', 'rev-parse', '--short', 'HEAD'],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
check=True,
|
|
||||||
cwd=Path(__file__).parent.parent
|
|
||||||
)
|
|
||||||
return result.stdout.strip()
|
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_git_branch() -> Optional[str]:
|
|
||||||
"""Get the current git branch if available."""
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
['git', 'branch', '--show-current'],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
check=True,
|
|
||||||
cwd=Path(__file__).parent.parent
|
|
||||||
)
|
|
||||||
return result.stdout.strip()
|
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_git_tag() -> Optional[str]:
|
|
||||||
"""Get the current git tag if available."""
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
['git', 'describe', '--tags', '--exact-match'],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
check=True,
|
|
||||||
cwd=Path(__file__).parent.parent
|
|
||||||
)
|
|
||||||
return result.stdout.strip()
|
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
||||||
return None
|
|
||||||
|
|
||||||
def is_development_version() -> bool:
|
|
||||||
"""Check if this is a development version (has uncommitted changes)."""
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
['git', 'status', '--porcelain'],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
check=True,
|
|
||||||
cwd=Path(__file__).parent.parent
|
|
||||||
)
|
|
||||||
return bool(result.stdout.strip())
|
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_version_info() -> dict:
|
|
||||||
"""Get comprehensive version information."""
|
|
||||||
git_commit = get_git_commit_hash()
|
|
||||||
git_branch = get_git_branch()
|
|
||||||
git_tag = get_git_tag()
|
|
||||||
is_dev = is_development_version()
|
|
||||||
|
|
||||||
# Build version string
|
|
||||||
version_parts = [__version__]
|
|
||||||
|
|
||||||
if git_tag and git_tag != f"v{__version__}":
|
|
||||||
# If we have a different tag, use it
|
|
||||||
version_parts = [git_tag.lstrip('v')]
|
|
||||||
|
|
||||||
if git_commit:
|
|
||||||
if is_dev:
|
|
||||||
version_parts.append(f"dev+{git_commit}")
|
|
||||||
elif not git_tag:
|
|
||||||
version_parts.append(f"+{git_commit}")
|
|
||||||
|
|
||||||
if is_dev and not git_commit:
|
|
||||||
version_parts.append("dev")
|
|
||||||
|
|
||||||
full_version = ".".join(version_parts)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"version": __version__,
|
|
||||||
"full_version": full_version,
|
|
||||||
"git_commit": git_commit,
|
|
||||||
"git_branch": git_branch,
|
|
||||||
"git_tag": git_tag,
|
|
||||||
"is_development": is_dev,
|
|
||||||
"is_git_repo": git_commit is not None
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_release_info() -> dict:
|
|
||||||
"""Get release information."""
|
|
||||||
version_info = get_version_info()
|
|
||||||
|
|
||||||
release_type = "development" if version_info["is_development"] else "release"
|
|
||||||
if version_info["git_tag"]:
|
|
||||||
release_type = "tagged-release"
|
|
||||||
elif version_info["git_commit"] and not version_info["is_development"]:
|
|
||||||
release_type = "commit-build"
|
|
||||||
|
|
||||||
return {
|
|
||||||
"release_type": release_type,
|
|
||||||
"build_from": version_info["git_branch"] or "unknown",
|
|
||||||
"commit": version_info["git_commit"] or "unknown",
|
|
||||||
"clean_build": not version_info["is_development"],
|
|
||||||
**version_info
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,23 @@
|
|||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools>=61.0"]
|
requires = ["setuptools>=64", "setuptools-scm>=8"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "markitect"
|
name = "markitect"
|
||||||
version = "0.7.0"
|
dynamic = ["version"]
|
||||||
description = "Advanced Markdown engine for structured content"
|
description = "Advanced Markdown engine for structured content"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.8"
|
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", "toml"]
|
dependencies = [
|
||||||
|
"markdown-it-py",
|
||||||
|
"PyYAML",
|
||||||
|
"click>=8.0.0",
|
||||||
|
"tabulate>=0.9.0",
|
||||||
|
"jsonpath-ng>=1.5.0",
|
||||||
|
"aiohttp>=3.8.0",
|
||||||
|
"toml",
|
||||||
|
"release-management @ file:./capabilities/release-management"
|
||||||
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
capabilities = [
|
capabilities = [
|
||||||
@@ -100,3 +109,6 @@ module = [
|
|||||||
"yaml.*"
|
"yaml.*"
|
||||||
]
|
]
|
||||||
ignore_missing_imports = true
|
ignore_missing_imports = true
|
||||||
|
|
||||||
|
[tool.setuptools_scm]
|
||||||
|
write_to = "markitect/_version.py"
|
||||||
|
|||||||
174
scripts/capability_discovery.mk
Normal file
174
scripts/capability_discovery.mk
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
# Capability Discovery System
|
||||||
|
# Automatically discovers and includes capability Makefiles
|
||||||
|
|
||||||
|
# Find all capability directories that contain Makefiles
|
||||||
|
CAPABILITY_DIRS := $(dir $(wildcard capabilities/*/Makefile))
|
||||||
|
CAPABILITY_MAKEFILES := $(foreach dir,$(CAPABILITY_DIRS),$(dir)Makefile)
|
||||||
|
|
||||||
|
# DO NOT include capability Makefiles directly to avoid target conflicts
|
||||||
|
# Instead, use delegation patterns below for accessing capability targets
|
||||||
|
|
||||||
|
# Capability discovery and delegation system
|
||||||
|
.PHONY: capabilities-list
|
||||||
|
capabilities-list: ## List all available capabilities
|
||||||
|
@echo "🧩 Available Capabilities:"
|
||||||
|
@echo "=========================="
|
||||||
|
@for dir in $(CAPABILITY_DIRS); do \
|
||||||
|
if [ -f "$$dir/Makefile" ]; then \
|
||||||
|
echo ""; \
|
||||||
|
capability=$$(basename $$dir); \
|
||||||
|
echo "📦 $$capability"; \
|
||||||
|
echo " Location: $$dir"; \
|
||||||
|
if [ -f "$$dir/README.md" ]; then \
|
||||||
|
desc=$$(head -5 "$$dir/README.md" | grep -E "^[A-Z].*" | head -1 || echo "No description"); \
|
||||||
|
echo " Description: $$desc"; \
|
||||||
|
fi; \
|
||||||
|
echo " Makefile: Available"; \
|
||||||
|
fi; \
|
||||||
|
done
|
||||||
|
@echo ""
|
||||||
|
@echo "Use 'make capabilities-help' to see all capability targets"
|
||||||
|
|
||||||
|
.PHONY: capabilities-help
|
||||||
|
capabilities-help: ## Show help for all capabilities
|
||||||
|
@echo "🧩 All Capability Targets:"
|
||||||
|
@echo "=========================="
|
||||||
|
@for dir in $(CAPABILITY_DIRS); do \
|
||||||
|
if [ -f "$$dir/Makefile" ]; then \
|
||||||
|
echo ""; \
|
||||||
|
capability=$$(basename $$dir); \
|
||||||
|
echo "📦 $$capability capability:"; \
|
||||||
|
echo ""; \
|
||||||
|
$(MAKE) -C "$$dir" help 2>/dev/null | grep -E "^ " || echo " No help available"; \
|
||||||
|
fi; \
|
||||||
|
done
|
||||||
|
|
||||||
|
.PHONY: capabilities-install
|
||||||
|
capabilities-install: ## Install all capabilities
|
||||||
|
@echo "🔧 Installing all capabilities..."
|
||||||
|
@for dir in $(CAPABILITY_DIRS); do \
|
||||||
|
if [ -f "$$dir/pyproject.toml" ]; then \
|
||||||
|
capability=$$(basename $$dir); \
|
||||||
|
echo "Installing $$capability..."; \
|
||||||
|
pip install -e "$$dir" || echo "Failed to install $$capability"; \
|
||||||
|
fi; \
|
||||||
|
done
|
||||||
|
|
||||||
|
.PHONY: capabilities-test
|
||||||
|
capabilities-test: ## Run tests for all capabilities
|
||||||
|
@echo "🧪 Testing all capabilities..."
|
||||||
|
@for dir in $(CAPABILITY_DIRS); do \
|
||||||
|
if [ -f "$$dir/Makefile" ]; then \
|
||||||
|
capability=$$(basename $$dir); \
|
||||||
|
echo ""; \
|
||||||
|
echo "Testing $$capability..."; \
|
||||||
|
$(MAKE) -C "$$dir" test 2>/dev/null || echo "No tests or test failed for $$capability"; \
|
||||||
|
fi; \
|
||||||
|
done
|
||||||
|
|
||||||
|
# Create delegation targets for common patterns
|
||||||
|
# This allows calling capability targets directly from main makefile
|
||||||
|
|
||||||
|
# Release management delegation (special handling since it's commonly used)
|
||||||
|
.PHONY: release-%
|
||||||
|
release-%:
|
||||||
|
@if [ -f "capabilities/release-management/Makefile" ]; then \
|
||||||
|
$(MAKE) -C capabilities/release-management $@; \
|
||||||
|
else \
|
||||||
|
echo "❌ Release management capability not found"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Generic capability delegation pattern
|
||||||
|
# Format: capability-name-target-name
|
||||||
|
define CAPABILITY_DELEGATION
|
||||||
|
.PHONY: $(1)-%
|
||||||
|
$(1)-%:
|
||||||
|
@if [ -f "capabilities/$(1)/Makefile" ]; then \
|
||||||
|
target_name=$$$$(echo "$$@" | sed 's/$(1)-//'); \
|
||||||
|
$(MAKE) -C "capabilities/$(1)" "$$$$target_name" || \
|
||||||
|
$(MAKE) -C "capabilities/$(1)" "$(1)-$$$$target_name" 2>/dev/null || \
|
||||||
|
echo "❌ Target '$$$$target_name' not found in $(1) capability"; \
|
||||||
|
else \
|
||||||
|
echo "❌ Capability '$(1)' not found or has no Makefile"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
endef
|
||||||
|
|
||||||
|
# Generate delegation targets for each capability
|
||||||
|
$(foreach cap,$(patsubst capabilities/%/,%,$(CAPABILITY_DIRS)),$(eval $(call CAPABILITY_DELEGATION,$(cap))))
|
||||||
|
|
||||||
|
# Show available capability targets
|
||||||
|
.PHONY: capabilities-targets
|
||||||
|
capabilities-targets: ## Show all available capability targets
|
||||||
|
@echo "🎯 Available Capability Targets:"
|
||||||
|
@echo "================================"
|
||||||
|
@for dir in $(CAPABILITY_DIRS); do \
|
||||||
|
if [ -f "$$dir/Makefile" ]; then \
|
||||||
|
capability=$$(basename $$dir); \
|
||||||
|
echo ""; \
|
||||||
|
echo "📦 $$capability:"; \
|
||||||
|
$(MAKE) -C "$$dir" -qp 2>/dev/null | \
|
||||||
|
grep -E "^[a-zA-Z][^:]*:.*##" | \
|
||||||
|
sed 's/:.*##//' | \
|
||||||
|
while read target; do \
|
||||||
|
echo " $$capability-$$target"; \
|
||||||
|
echo " $@"; \
|
||||||
|
done || echo " No documented targets"; \
|
||||||
|
fi; \
|
||||||
|
done
|
||||||
|
|
||||||
|
# Capability status check
|
||||||
|
.PHONY: capabilities-status
|
||||||
|
capabilities-status: ## Show status of all capabilities
|
||||||
|
@echo "📊 Capability Status:"
|
||||||
|
@echo "===================="
|
||||||
|
@for dir in $(CAPABILITY_DIRS); do \
|
||||||
|
capability=$$(basename $$dir); \
|
||||||
|
echo ""; \
|
||||||
|
echo "📦 $$capability:"; \
|
||||||
|
if [ -f "$$dir/Makefile" ]; then \
|
||||||
|
echo " ✅ Makefile: Available"; \
|
||||||
|
else \
|
||||||
|
echo " ❌ Makefile: Missing"; \
|
||||||
|
fi; \
|
||||||
|
if [ -f "$$dir/pyproject.toml" ]; then \
|
||||||
|
echo " ✅ Package: Available"; \
|
||||||
|
if pip show "$$(echo $$capability | tr '-' '_')" >/dev/null 2>&1 || \
|
||||||
|
pip show "$$capability" >/dev/null 2>&1; then \
|
||||||
|
echo " ✅ Installed: Yes"; \
|
||||||
|
else \
|
||||||
|
echo " ❌ Installed: No"; \
|
||||||
|
fi; \
|
||||||
|
else \
|
||||||
|
echo " ❌ Package: Not available"; \
|
||||||
|
fi; \
|
||||||
|
if [ -f "$$dir/README.md" ]; then \
|
||||||
|
echo " ✅ Documentation: Available"; \
|
||||||
|
else \
|
||||||
|
echo " ❌ Documentation: Missing"; \
|
||||||
|
fi; \
|
||||||
|
done
|
||||||
|
|
||||||
|
# Auto-complete help integration
|
||||||
|
.PHONY: help-capabilities
|
||||||
|
help-capabilities: ## Show capability system help
|
||||||
|
@echo ""
|
||||||
|
@echo "🧩 Capability System:"
|
||||||
|
@echo " capabilities-list List all available capabilities"
|
||||||
|
@echo " capabilities-help Show help for all capabilities"
|
||||||
|
@echo " capabilities-status Show status of all capabilities"
|
||||||
|
@echo " capabilities-install Install all capabilities"
|
||||||
|
@echo " capabilities-test Test all capabilities"
|
||||||
|
@echo " capabilities-targets Show all available capability targets"
|
||||||
|
@echo ""
|
||||||
|
@echo "🎯 Capability Delegation:"
|
||||||
|
@echo " Use 'capability-name-target' to call targets from capabilities"
|
||||||
|
@echo " Example: 'make release-management-status' calls status from release-management"
|
||||||
|
@echo " Special: release-* targets are delegated to release-management capability"
|
||||||
|
@echo ""
|
||||||
|
|
||||||
|
# Export capability information for use by main Makefile
|
||||||
|
CAPABILITIES_FOUND := $(words $(CAPABILITY_DIRS))
|
||||||
|
export CAPABILITIES_FOUND
|
||||||
|
export CAPABILITY_DIRS
|
||||||
Reference in New Issue
Block a user