diff --git a/Makefile b/Makefile index 660cc50a..f20144a4 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,10 @@ # MarkiTect - Advanced Markdown Engine # 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 help: @@ -26,8 +29,7 @@ help: @echo "" @echo "Development:" @echo " test - Run core tests (excluding capability-specific tests)" - @echo " test-capabilities - Run all capability-specific tests" - @echo " test-capability-* - Run specific capability tests (content, utils, finance, etc.)" + @echo " test-capabilities - Run all capability tests (delegated to capabilities)" @echo " test-status - Show test status summary without re-running" @echo " test-new - Create new test file template" @echo " test-coverage - Analyze test coverage" @@ -36,16 +38,15 @@ help: @echo " lint - Run code linting" @echo " format - Format code" @echo "" - @echo "Release Management (setuptools-scm):" - @echo " release-status - Show current release status" - @echo " release-validate - Validate repository for release" - @echo " release-build - Build release packages (version auto-detected)" - @echo " release-tag VERSION=x.y.z - Create release git tag" - @echo " release-publish VERSION=x.y.z - Complete release workflow (tag + build)" - @echo " release-publish-gitea VERSION=x.y.z - Release + upload to Gitea registry" - @echo " release-upload-gitea - Upload existing packages to Gitea registry" - @echo " release-registry - Show Gitea package registry information" - @echo " release-dry-run VERSION=x.y.z - Test release workflow" + @echo "Capabilities & Extensions:" + @echo " capabilities-list List all available capabilities" + @echo " capabilities-help Show help for all capabilities" + @echo " capabilities-status Show capability status" + @echo "" + @echo "Release Management (via capability):" + @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 "Chaos Engineering:" @echo " chaos-validate - Run architectural independence validation" @@ -384,32 +385,12 @@ test: $(VENV)/bin/activate fi # 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" -test-capability-content: $(VENV)/bin/activate - @echo "๐Ÿงช Running markitect-content capability tests..." - @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 +# Legacy test-capability-* targets are now handled by capability delegation +# Use 'make capability-name-test' instead (e.g., 'make markitect-content-test') # TDD8 Workflow Optimized Test Targets (Issue #57) @@ -502,62 +483,9 @@ package: $(VENV)/bin/activate @echo "โœ… Packages built successfully:" @ls -lah dist/ 2>/dev/null || echo " No packages found" -# Release management (setuptools-scm) -release-status: - @echo "๐Ÿ” Checking release status (setuptools-scm)..." - $(VENV_PYTHON) release.py status - -release-validate: - @echo "โœ… Validating release readiness..." - $(VENV_PYTHON) release.py validate - -release-build: - @echo "๐Ÿ“ฆ Building release packages (version auto-detected by setuptools-scm)..." - $(VENV_PYTHON) release.py build - -release-tag: - @echo "๐Ÿท๏ธ Creating release git tag..." - @if [ -z "$(VERSION)" ]; then \ - echo "โŒ Usage: make release-tag VERSION=1.0.0"; \ - echo "โ„น๏ธ This creates a git tag that setuptools-scm will use for versioning"; \ - exit 1; \ - fi - $(VENV_PYTHON) release.py tag --version $(VERSION) - -release-publish: - @echo "๐Ÿ“ข Publishing complete release (setuptools-scm workflow)..." - @if [ -z "$(VERSION)" ]; then \ - echo "โŒ Usage: make release-publish VERSION=1.0.0"; \ - echo "โ„น๏ธ This creates git tag + builds packages automatically"; \ - exit 1; \ - fi - $(VENV_PYTHON) release.py publish --version $(VERSION) - -release-dry-run: - @echo "๐Ÿงช Dry run release workflow..." - @if [ -z "$(VERSION)" ]; then \ - echo "โŒ Usage: make release-dry-run VERSION=1.0.0"; \ - echo "โ„น๏ธ This tests the tag + build workflow without making changes"; \ - exit 1; \ - fi - $(VENV_PYTHON) release.py publish --version $(VERSION) --dry-run - -release-publish-gitea: - @echo "๐Ÿš€ Publishing complete release with Gitea upload..." - @if [ -z "$(VERSION)" ]; then \ - echo "โŒ Usage: make release-publish-gitea VERSION=1.0.0"; \ - echo "โ„น๏ธ This creates git tag + builds packages + uploads to Gitea"; \ - exit 1; \ - fi - $(VENV_PYTHON) release.py publish --version $(VERSION) --to-gitea - -release-upload-gitea: - @echo "๐Ÿ“ก Uploading packages to Gitea registry..." - $(VENV_PYTHON) release.py upload - -release-registry: - @echo "๐Ÿ“ฆ Gitea package registry information..." - $(VENV_PYTHON) release.py registry +# Release management targets are provided by capabilities/release-management/Makefile +# All capability targets are automatically discovered and available via delegation +# Run 'make capabilities-help' to see all available capability commands # Chaos Engineering targets chaos-validate: diff --git a/agents/agent-capability-manager.md b/agents/agent-capability-manager.md new file mode 100644 index 00000000..c0a2a0e0 --- /dev/null +++ b/agents/agent-capability-manager.md @@ -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. \ No newline at end of file diff --git a/capabilities/markitect-content/Makefile b/capabilities/markitect-content/Makefile new file mode 100644 index 00000000..aabda792 --- /dev/null +++ b/capabilities/markitect-content/Makefile @@ -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/^ / /' \ No newline at end of file diff --git a/capabilities/markitect-utils/Makefile b/capabilities/markitect-utils/Makefile new file mode 100644 index 00000000..98a1b6b8 --- /dev/null +++ b/capabilities/markitect-utils/Makefile @@ -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.txt\"):', safe_filename('file.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/^ / /' \ No newline at end of file diff --git a/capabilities/release-management/MIGRATION_PLAN.md b/capabilities/release-management/MIGRATION_PLAN.md new file mode 100644 index 00000000..b45a9c23 --- /dev/null +++ b/capabilities/release-management/MIGRATION_PLAN.md @@ -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. \ No newline at end of file diff --git a/capabilities/release-management/Makefile b/capabilities/release-management/Makefile new file mode 100644 index 00000000..fa7fcd1c --- /dev/null +++ b/capabilities/release-management/Makefile @@ -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/^ / /' \ No newline at end of file diff --git a/capabilities/release-management/README.md b/capabilities/release-management/README.md new file mode 100644 index 00000000..d4ccdd05 --- /dev/null +++ b/capabilities/release-management/README.md @@ -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 \ No newline at end of file diff --git a/PACKAGE_PUBLISHING.md b/capabilities/release-management/docs/package_publishing.md similarity index 100% rename from PACKAGE_PUBLISHING.md rename to capabilities/release-management/docs/package_publishing.md diff --git a/VERSION_MANAGEMENT.md b/capabilities/release-management/docs/version_management.md similarity index 100% rename from VERSION_MANAGEMENT.md rename to capabilities/release-management/docs/version_management.md diff --git a/capabilities/release-management/release.mk b/capabilities/release-management/release.mk new file mode 100644 index 00000000..c262e8ab --- /dev/null +++ b/capabilities/release-management/release.mk @@ -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 \ No newline at end of file diff --git a/capabilities/release-management/src/release_management/__init__.py b/capabilities/release-management/src/release_management/__init__.py new file mode 100644 index 00000000..8899de55 --- /dev/null +++ b/capabilities/release-management/src/release_management/__init__.py @@ -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" \ No newline at end of file diff --git a/capabilities/release-management/src/release_management/cli/__init__.py b/capabilities/release-management/src/release_management/cli/__init__.py new file mode 100644 index 00000000..1eff15d9 --- /dev/null +++ b/capabilities/release-management/src/release_management/cli/__init__.py @@ -0,0 +1,9 @@ +""" +Command-line interface for release management. + +This module provides CLI commands for release operations. +""" + +from .main import main + +__all__ = ["main"] \ No newline at end of file diff --git a/capabilities/release-management/src/release_management/cli/main.py b/capabilities/release-management/src/release_management/cli/main.py new file mode 100644 index 00000000..2fda88e0 --- /dev/null +++ b/capabilities/release-management/src/release_management/cli/main.py @@ -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() \ No newline at end of file diff --git a/capabilities/release-management/src/release_management/core/__init__.py b/capabilities/release-management/src/release_management/core/__init__.py new file mode 100644 index 00000000..7b2e20c3 --- /dev/null +++ b/capabilities/release-management/src/release_management/core/__init__.py @@ -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"] \ No newline at end of file diff --git a/capabilities/release-management/src/release_management/core/builder.py b/capabilities/release-management/src/release_management/core/builder.py new file mode 100644 index 00000000..352df297 --- /dev/null +++ b/capabilities/release-management/src/release_management/core/builder.py @@ -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 + ) \ No newline at end of file diff --git a/capabilities/release-management/src/release_management/core/manager.py b/capabilities/release-management/src/release_management/core/manager.py new file mode 100644 index 00000000..2c07aa30 --- /dev/null +++ b/capabilities/release-management/src/release_management/core/manager.py @@ -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() \ No newline at end of file diff --git a/capabilities/release-management/src/release_management/core/publisher.py b/capabilities/release-management/src/release_management/core/publisher.py new file mode 100644 index 00000000..f13feafb --- /dev/null +++ b/capabilities/release-management/src/release_management/core/publisher.py @@ -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 \ No newline at end of file diff --git a/capabilities/release-management/src/release_management/git/__init__.py b/capabilities/release-management/src/release_management/git/__init__.py new file mode 100644 index 00000000..df314397 --- /dev/null +++ b/capabilities/release-management/src/release_management/git/__init__.py @@ -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"] \ No newline at end of file diff --git a/capabilities/release-management/src/release_management/git/manager.py b/capabilities/release-management/src/release_management/git/manager.py new file mode 100644 index 00000000..59fa162e --- /dev/null +++ b/capabilities/release-management/src/release_management/git/manager.py @@ -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 + ) \ No newline at end of file diff --git a/capabilities/release-management/src/release_management/registries/__init__.py b/capabilities/release-management/src/release_management/registries/__init__.py new file mode 100644 index 00000000..38765e3a --- /dev/null +++ b/capabilities/release-management/src/release_management/registries/__init__.py @@ -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", +] \ No newline at end of file diff --git a/capabilities/release-management/src/release_management/registries/base.py b/capabilities/release-management/src/release_management/registries/base.py new file mode 100644 index 00000000..72b944c1 --- /dev/null +++ b/capabilities/release-management/src/release_management/registries/base.py @@ -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 \ No newline at end of file diff --git a/capabilities/release-management/src/release_management/registries/factory.py b/capabilities/release-management/src/release_management/registries/factory.py new file mode 100644 index 00000000..596c80ea --- /dev/null +++ b/capabilities/release-management/src/release_management/registries/factory.py @@ -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()) \ No newline at end of file diff --git a/capabilities/release-management/src/release_management/registries/gitea/__init__.py b/capabilities/release-management/src/release_management/registries/gitea/__init__.py new file mode 100644 index 00000000..8414c11c --- /dev/null +++ b/capabilities/release-management/src/release_management/registries/gitea/__init__.py @@ -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"] \ No newline at end of file diff --git a/gitea/config.py b/capabilities/release-management/src/release_management/registries/gitea/config.py similarity index 99% rename from gitea/config.py rename to capabilities/release-management/src/release_management/registries/gitea/config.py index 23697984..b11444df 100644 --- a/gitea/config.py +++ b/capabilities/release-management/src/release_management/registries/gitea/config.py @@ -172,4 +172,4 @@ class GiteaConfig: def requires_auth(self, operation: str = "read") -> bool: """Check if operation requires authentication.""" write_operations = {"create", "update", "delete", "write"} - return operation in write_operations and not self.auth_token + return operation in write_operations and not self.auth_token \ No newline at end of file diff --git a/capabilities/release-management/src/release_management/registries/gitea/exceptions.py b/capabilities/release-management/src/release_management/registries/gitea/exceptions.py new file mode 100644 index 00000000..25bf556e --- /dev/null +++ b/capabilities/release-management/src/release_management/registries/gitea/exceptions.py @@ -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 \ No newline at end of file diff --git a/gitea/package_registry.py b/capabilities/release-management/src/release_management/registries/gitea/registry.py similarity index 51% rename from gitea/package_registry.py rename to capabilities/release-management/src/release_management/registries/gitea/registry.py index 3c79f0ff..d3c842f5 100644 --- a/gitea/package_registry.py +++ b/capabilities/release-management/src/release_management/registries/gitea/registry.py @@ -6,15 +6,15 @@ Gitea supports multiple package registries including PyPI-compatible registries. """ import os -import subprocess -import tempfile from pathlib import Path -from typing import Optional, List, Dict +from typing import Optional, List, Dict, Any + +from ..base import RegistryInterface from .config import GiteaConfig from .exceptions import GiteaError -class GiteaPackageRegistry: +class GiteaRegistry(RegistryInterface): """Client for publishing packages to Gitea package registry.""" def __init__(self, config: Optional[GiteaConfig] = None): @@ -50,7 +50,7 @@ class GiteaPackageRegistry: except Exception: return False - def list_packages(self) -> List[Dict]: + def list_packages(self) -> List[Dict[str, Any]]: """List all packages for this repository owner. Returns: @@ -68,7 +68,7 @@ class GiteaPackageRegistry: except Exception as e: raise GiteaError(f"Failed to list packages: {e}") - def get_package_info(self, package_name: str) -> Optional[Dict]: + def get_package_info(self, package_name: str) -> Optional[Dict[str, Any]]: """Get information about a specific package. Args: @@ -86,13 +86,38 @@ class GiteaPackageRegistry: except Exception: return None - def upload_package(self, - wheel_path: Path, - sdist_path: Optional[Path] = None, - dry_run: bool = False) -> bool: - """Upload package files to Gitea registry. + 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 @@ -101,7 +126,7 @@ class GiteaPackageRegistry: 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.") + 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}") @@ -114,61 +139,27 @@ class GiteaPackageRegistry: files_to_upload.append(sdist_path) if dry_run: - print(f"[DRY RUN] Would upload to: {self.pypi_registry_url}") + 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_file(file_path): + if not self._upload_release_asset(release_id, file_path): success = False return success - 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 - - # Upload using multipart form data (PyPI-compatible) - with open(file_path, 'rb') as f: - files = { - 'content': (file_path.name, f, 'application/octet-stream') - } - headers = { - 'Authorization': f'token {self.config.auth_token}' - } - - upload_url = f"{self.pypi_registry_url}/simple/" - response = requests.post( - upload_url, - headers=headers, - files=files, - 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} {response.text}") - return False - - except Exception as e: - print(f"โŒ Upload failed for {file_path.name}: {e}") - return False - - def delete_package_version(self, package_name: str, version: str, dry_run: bool = False) -> bool: + def delete_package_version(self, package_name: str, version: str, + dry_run: bool = False) -> bool: """Delete a specific version of a package. Args: @@ -205,7 +196,7 @@ class GiteaPackageRegistry: print(f"โŒ Delete failed: {e}") return False - def get_registry_info(self) -> Dict: + def get_registry_info(self) -> Dict[str, Any]: """Get information about the package registry configuration. Returns: @@ -221,51 +212,144 @@ class GiteaPackageRegistry: "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. -def configure_pip_for_gitea(config: Optional[GiteaConfig] = None, - pip_conf_path: Optional[Path] = None) -> Path: - """Configure pip to use Gitea package registry as additional index. + Args: + file_path: Path to file to upload - Args: - config: Gitea configuration - pip_conf_path: Custom path for pip.conf file + Returns: + True if upload successful, False otherwise + """ + try: + import requests - Returns: - Path to created/updated pip.conf file - """ - config = config or GiteaConfig.from_git_repository() + # 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" - if pip_conf_path is None: - # Default pip config location - pip_conf_path = Path.home() / ".pip" / "pip.conf" + with open(file_path, 'rb') as f: + file_content = f.read() - pip_conf_path.parent.mkdir(parents=True, exist_ok=True) + headers = { + 'Authorization': f'token {self.config.auth_token}', + 'Content-Type': 'application/octet-stream' + } - registry = GiteaPackageRegistry(config) - gitea_index = f"{registry.pypi_registry_url}/simple/" + # 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 + ) - # Read existing config or create new - config_content = "" - if pip_conf_path.exists(): - config_content = pip_conf_path.read_text() + 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 - # Add Gitea index if not already present - if "extra-index-url" not in config_content: - if "[global]" not in config_content: - config_content = "[global]\n" + config_content + except Exception as e: + print(f"โŒ Upload failed for {file_path.name}: {e}") + return False - lines = config_content.split('\n') - global_section_idx = next(i for i, line in enumerate(lines) if line.strip() == "[global]") - lines.insert(global_section_idx + 1, f"extra-index-url = {gitea_index}") - config_content = '\n'.join(lines) - elif gitea_index not in config_content: - # Add to existing extra-index-url - lines = config_content.split('\n') - for i, line in enumerate(lines): - if line.startswith("extra-index-url"): - lines[i] = f"{line} {gitea_index}" - break - config_content = '\n'.join(lines) + def _create_or_get_release(self, version: str) -> Optional[int]: + """Create a new release or get existing release ID. - pip_conf_path.write_text(config_content) - return pip_conf_path \ No newline at end of file + 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 \ No newline at end of file diff --git a/capabilities/release-management/src/release_management/utils/__init__.py b/capabilities/release-management/src/release_management/utils/__init__.py new file mode 100644 index 00000000..5ab75cad --- /dev/null +++ b/capabilities/release-management/src/release_management/utils/__init__.py @@ -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"] \ No newline at end of file diff --git a/capabilities/release-management/src/release_management/utils/validation.py b/capabilities/release-management/src/release_management/utils/validation.py new file mode 100644 index 00000000..077d8236 --- /dev/null +++ b/capabilities/release-management/src/release_management/utils/validation.py @@ -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 \ No newline at end of file diff --git a/capabilities/release-management/src/release_management/utils/version.py b/capabilities/release-management/src/release_management/utils/version.py new file mode 100644 index 00000000..b1a53429 --- /dev/null +++ b/capabilities/release-management/src/release_management/utils/version.py @@ -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 \ No newline at end of file diff --git a/gitea/__init__.py b/gitea/__init__.py deleted file mode 100644 index 5eeb3d2b..00000000 --- a/gitea/__init__.py +++ /dev/null @@ -1,35 +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 -from .package_registry import GiteaPackageRegistry - -__all__ = [ - 'GiteaClient', - 'Issue', 'Milestone', 'Label', 'ProjectState', 'Priority', - 'GiteaConfig', - 'GiteaError', 'GiteaAuthError', 'GiteaNotFoundError', - 'GiteaPackageRegistry' -] \ No newline at end of file diff --git a/gitea/api_client.py b/gitea/api_client.py deleted file mode 100644 index aea4197b..00000000 --- a/gitea/api_client.py +++ /dev/null @@ -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 [] \ No newline at end of file diff --git a/gitea/client.py b/gitea/client.py deleted file mode 100644 index f62848ad..00000000 --- a/gitea/client.py +++ /dev/null @@ -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() \ No newline at end of file diff --git a/gitea/exceptions.py b/gitea/exceptions.py deleted file mode 100644 index b43aa667..00000000 --- a/gitea/exceptions.py +++ /dev/null @@ -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 \ No newline at end of file diff --git a/gitea/http_client.py b/gitea/http_client.py deleted file mode 100644 index 4015501c..00000000 --- a/gitea/http_client.py +++ /dev/null @@ -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") \ No newline at end of file diff --git a/gitea/models.py b/gitea/models.py deleted file mode 100644 index 0c290c65..00000000 --- a/gitea/models.py +++ /dev/null @@ -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 = "" \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index c62e990c..0805dfcb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,16 @@ dynamic = ["version"] description = "Advanced Markdown engine for structured content" readme = "README.md" requires-python = ">=3.8" -dependencies = ["markdown-it-py", "PyYAML", "click>=8.0.0", "tabulate>=0.9.0", "jsonpath-ng>=1.5.0", "aiohttp>=3.8.0", "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] capabilities = [ diff --git a/release.py b/release.py deleted file mode 100644 index aa16fba2..00000000 --- a/release.py +++ /dev/null @@ -1,377 +0,0 @@ -#!/usr/bin/env python3 -""" -MarkiTect Release Management Tool (setuptools-scm version) - -This simplified script works with setuptools-scm for automatic version management. -Versions are automatically derived from git tags - no manual version bumping needed. - -Usage: - python release.py [command] [options] - -Commands: - status Show current release status - validate Validate current state for release - tag Create git tag for version (e.g., v0.8.0) - build Build release packages - publish Complete release workflow (tag + build + distribute) - upload Upload packages to Gitea registry - registry Show Gitea package registry information - -Options: - --version VERSION Git tag version (e.g., 0.8.0, 1.0.0-rc1) - --dry-run Show what would be done without making changes - --force Force operation even with warnings - --to-gitea Upload to Gitea package registry -""" - -import subprocess -import argparse -import sys -from pathlib import Path -from datetime import datetime -from typing import Dict, List, Optional, Tuple - -try: - from gitea.package_registry import GiteaPackageRegistry - GITEA_AVAILABLE = True -except ImportError: - GITEA_AVAILABLE = False - - -class SimpleReleaseManager: - """Simplified release manager using setuptools-scm.""" - - def __init__(self, dry_run=False, force=False): - self.dry_run = dry_run - self.force = force - self.project_root = Path(__file__).parent.absolute() - - def run_command(self, cmd: List[str], capture=True, check=True, skip_dry_run=False) -> subprocess.CompletedProcess: - """Run a command with optional dry-run support.""" - if self.dry_run and not skip_dry_run: - print(f"[DRY RUN] Would run: {' '.join(cmd)}") - return subprocess.CompletedProcess(cmd, 0, "", "") - return subprocess.run(cmd, capture_output=capture, text=True, check=check, cwd=self.project_root) - - def get_current_version_from_scm(self) -> str: - """Get current version using setuptools-scm.""" - try: - result = self.run_command(['python', '-m', 'setuptools_scm'], skip_dry_run=True) - return result.stdout.strip() - except subprocess.CalledProcessError: - return "unknown" - - def get_git_status(self) -> Dict[str, any]: - """Get current git repository status.""" - try: - # Get current branch - branch_result = self.run_command(['git', 'branch', '--show-current'], skip_dry_run=True) - current_branch = branch_result.stdout.strip() - - # Check for uncommitted changes - status_result = self.run_command(['git', 'status', '--porcelain'], skip_dry_run=True) - has_changes = bool(status_result.stdout.strip()) - - # Get latest commit - commit_result = self.run_command(['git', 'rev-parse', '--short', 'HEAD'], skip_dry_run=True) - latest_commit = commit_result.stdout.strip() - - # Get latest tag - try: - tag_result = self.run_command(['git', 'describe', '--tags', '--abbrev=0'], skip_dry_run=True) - latest_tag = tag_result.stdout.strip() - except subprocess.CalledProcessError: - latest_tag = None - - return { - 'is_repo': True, - 'branch': current_branch, - 'has_changes': has_changes, - 'latest_commit': latest_commit, - 'latest_tag': latest_tag - } - except subprocess.CalledProcessError: - return {'is_repo': False} - - def validate_release_state(self) -> Tuple[bool, List[str]]: - """Validate that the repository is ready for release.""" - issues = [] - git_status = self.get_git_status() - - if not git_status['is_repo']: - issues.append("Not in a git repository") - else: - if git_status['has_changes'] and not self.force: - issues.append("Repository has uncommitted changes") - if git_status['branch'] != 'main' and not self.force: - issues.append(f"Not on main branch (currently on {git_status['branch']})") - - return len(issues) == 0, issues - - def create_git_tag(self, version: str, message: str = None): - """Create and push git tag.""" - if not version.startswith('v'): - tag_name = f"v{version}" - else: - tag_name = version - - tag_message = message or f"Release {version}" - print(f"๐Ÿท๏ธ Creating git tag {tag_name}") - - # Create annotated tag - self.run_command(['git', 'tag', '-a', tag_name, '-m', tag_message]) - print(f"โœ… Tag {tag_name} created") - - # Optionally push tag (can be done manually) - try: - print(f"๐Ÿ“ค Pushing tag to origin...") - self.run_command(['git', 'push', 'origin', tag_name]) - print(f"โœ… Tag pushed to origin") - except subprocess.CalledProcessError as e: - print(f"โš ๏ธ Could not push tag to origin: {e}") - print("You can push it manually with: git push origin " + tag_name) - - def build_packages(self): - """Build release packages using setuptools-scm.""" - print(f"๐Ÿ“ฆ Building packages (version will be auto-determined by setuptools-scm)") - - # Clean previous builds - for pattern in ['build', 'dist', '*.egg-info']: - try: - self.run_command(['rm', '-rf', pattern]) - except subprocess.CalledProcessError: - pass - - # Build source distribution and wheel - print("Building packages...") - self.run_command(['python', '-m', 'build'], capture=False) - print("โœ… Packages built successfully") - - def show_status(self): - """Show current release status.""" - print("๐Ÿ” MarkiTect Release Status (setuptools-scm)") - print("=" * 60) - - # Get version from setuptools-scm - scm_version = self.get_current_version_from_scm() - print(f"Current Version (setuptools-scm): {scm_version}") - - git_status = self.get_git_status() - if git_status['is_repo']: - print(f"Git Branch: {git_status['branch']}") - print(f"Latest Commit: {git_status['latest_commit']}") - print(f"Latest Tag: {git_status['latest_tag'] or 'None'}") - print(f"Uncommitted Changes: {'Yes' if git_status['has_changes'] else 'No'}") - else: - print("Git Repository: Not available") - - # Check build tools - print("\nBuild Tools:") - try: - self.run_command(['python', '-m', 'build', '--help'], skip_dry_run=True) - print("โœ… build module available") - except (subprocess.CalledProcessError, FileNotFoundError): - print("โŒ build module not available (pip install build)") - - try: - self.run_command(['python', '-m', 'setuptools_scm'], skip_dry_run=True) - print("โœ… setuptools-scm available") - except (subprocess.CalledProcessError, FileNotFoundError): - print("โŒ setuptools-scm not available") - - # Check existing packages - dist_dir = self.project_root / "dist" - if dist_dir.exists(): - packages = list(dist_dir.glob("*")) - print(f"\nExisting Packages: {len(packages)} files in dist/") - for pkg in packages[-5:]: # Show last 5 - print(f" - {pkg.name}") - else: - print("\nExisting Packages: None") - - def publish_release(self, version: str): - """Complete release workflow.""" - 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) - self.create_git_tag(version) - - # Build packages (setuptools-scm will use the tag for version) - self.build_packages() - - print(f"โœ… Release {version} completed!") - print("๐Ÿ“ฆ Packages available in dist/") - print(f"๐Ÿท๏ธ Git tag v{version} created") - return True - - def upload_to_gitea(self, dry_run: bool = False) -> bool: - """Upload packages to Gitea package registry.""" - if not GITEA_AVAILABLE: - print("โŒ Gitea package registry not available (missing gitea module)") - return False - - try: - registry = GiteaPackageRegistry() - print(f"๐Ÿ“ก Uploading to Gitea registry: {registry.pypi_registry_url}") - - # Find built packages - dist_dir = self.project_root / "dist" - if not dist_dir.exists(): - print("โŒ No dist/ directory found. Run 'build' command first.") - return False - - wheel_files = list(dist_dir.glob("*.whl")) - sdist_files = list(dist_dir.glob("*.tar.gz")) - - if not wheel_files and not sdist_files: - print("โŒ No package files found in dist/") - return False - - # Upload each package - success = True - for wheel_file in wheel_files: - # Find matching sdist - sdist_file = None - for sdist in sdist_files: - if wheel_file.stem.split('-')[0] == sdist.stem.split('-')[0]: - sdist_file = sdist - break - - if not registry.upload_package(wheel_file, sdist_file, dry_run=dry_run): - success = False - - # Upload any remaining sdists - uploaded_sdists = [] - for wheel_file in wheel_files: - for sdist in sdist_files: - if wheel_file.stem.split('-')[0] == sdist.stem.split('-')[0]: - uploaded_sdists.append(sdist) - - for sdist_file in sdist_files: - if sdist_file not in uploaded_sdists: - if not registry.upload_package(sdist_file, dry_run=dry_run): - success = False - - return success - - except Exception as e: - print(f"โŒ Upload to Gitea failed: {e}") - return False - - def show_gitea_registry_info(self): - """Show Gitea package registry information.""" - if not GITEA_AVAILABLE: - print("โŒ Gitea package registry not available (missing gitea module)") - return - - try: - registry = GiteaPackageRegistry() - info = registry.get_registry_info() - - print("๐Ÿ“ฆ Gitea Package Registry Information") - print("=" * 50) - print(f"Gitea URL: {info['gitea_url']}") - print(f"Repository: {info['repo_owner']}/{info['repo_name']}") - print(f"PyPI Registry URL: {info['pypi_registry_url']}") - print(f"Package List URL: {info['package_list_url']}") - print(f"Authentication Configured: {'โœ…' if info['auth_configured'] else 'โŒ'}") - print(f"Authentication Valid: {'โœ…' if info['auth_valid'] else 'โŒ' if info['auth_configured'] else 'N/A'}") - - if info['auth_configured']: - try: - packages = registry.list_packages() - print(f"\nExisting Packages: {len(packages)}") - for package in packages[:5]: # Show first 5 - print(f" - {package.get('name', 'unknown')} (type: {package.get('type', 'unknown')})") - except Exception as e: - print(f"\nError listing packages: {e}") - else: - print("\nโ„น๏ธ Set GITEA_API_TOKEN environment variable for package management") - - except Exception as e: - print(f"โŒ Error getting registry info: {e}") - - def publish_with_gitea(self, version: str, dry_run: bool = False) -> bool: - """Complete release workflow including Gitea upload.""" - if not self.publish_release(version): - return False - - if not self.upload_to_gitea(dry_run=dry_run): - print("โš ๏ธ Release completed but Gitea upload failed") - return False - - print("๐ŸŽ‰ Complete release with Gitea upload successful!") - return True - - -def main(): - parser = argparse.ArgumentParser( - description="MarkiTect Release Management Tool (setuptools-scm)", - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=__doc__.split('\n\n')[1] - ) - - parser.add_argument('command', choices=['status', 'validate', 'tag', 'build', 'publish', 'upload', 'registry'], - help='Release command to execute') - parser.add_argument('--version', type=str, help='Target version for git tag (e.g., 0.8.0)') - parser.add_argument('--dry-run', action='store_true', help='Show what would be done') - parser.add_argument('--force', action='store_true', help='Force operation despite warnings') - parser.add_argument('--to-gitea', action='store_true', help='Include Gitea package registry upload') - - args = parser.parse_args() - manager = SimpleReleaseManager(dry_run=args.dry_run, force=args.force) - - try: - if args.command == 'status': - manager.show_status() - - elif args.command == 'validate': - is_valid, issues = manager.validate_release_state() - if is_valid: - print("โœ… Repository is ready for release") - else: - print("โŒ Release validation failed:") - for issue in issues: - print(f" - {issue}") - sys.exit(1) - - elif args.command == 'tag': - if not args.version: - print("โŒ --version is required for tag command") - sys.exit(1) - manager.create_git_tag(args.version) - - elif args.command == 'build': - manager.build_packages() - - elif args.command == 'publish': - if not args.version: - print("โŒ --version is required for publish command") - sys.exit(1) - - if args.to_gitea: - manager.publish_with_gitea(args.version, args.dry_run) - else: - manager.publish_release(args.version) - - elif args.command == 'upload': - manager.upload_to_gitea(args.dry_run) - - elif args.command == 'registry': - manager.show_gitea_registry_info() - - except Exception as e: - print(f"โŒ Error: {e}") - sys.exit(1) - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/scripts/capability_discovery.mk b/scripts/capability_discovery.mk new file mode 100644 index 00000000..288cc1f7 --- /dev/null +++ b/scripts/capability_discovery.mk @@ -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 \ No newline at end of file