# ============================================================================
# release.mk - Version & Publishing Management
# ============================================================================
# Provides: version bumping, build, publish to PyPI, GitHub releases
# Include in main Makefile with: -include .makefiles/release.mk
#
# Extracted from: claude-mpm production Makefile (97 targets)
# Dependencies: common.mk (for colors, VERSION, PYTHON, ENV)
# quality.mk (for pre-publish checks)
# Last updated: 2025-11-21
# ============================================================================
# ============================================================================
# Release Target Declarations
# ============================================================================
.PHONY: release-check release-patch release-minor release-major
.PHONY: release-build release-publish release-verify
.PHONY: release-dry-run release-test-pypi
.PHONY: build-metadata build-info-json
.PHONY: patch minor major
# ============================================================================
# Release Prerequisites Check
# ============================================================================
release-check: ## Check if environment is ready for release
@echo "$(YELLOW)๐ Checking release prerequisites...$(NC)"
@echo "Checking required tools..."
@command -v git >/dev/null 2>&1 || (echo "$(RED)โ git not found$(NC)" && exit 1)
@command -v $(PYTHON) >/dev/null 2>&1 || (echo "$(RED)โ python not found$(NC)" && exit 1)
@command -v gh >/dev/null 2>&1 || (echo "$(RED)โ GitHub CLI not found. Install from: https://cli.github.com/$(NC)" && exit 1)
@echo "$(GREEN)โ All required tools found$(NC)"
@echo "Checking working directory..."
@if [ -n "$$(git status --porcelain)" ]; then \
echo "$(RED)โ Working directory is not clean$(NC)"; \
git status --short; \
exit 1; \
fi
@echo "$(GREEN)โ Working directory is clean$(NC)"
@echo "Checking current branch..."
@BRANCH=$$(git branch --show-current); \
if [ "$$BRANCH" != "main" ]; then \
echo "$(YELLOW)โ Currently on branch '$$BRANCH', not 'main'$(NC)"; \
read -p "Continue anyway? [y/N]: " confirm; \
if [ "$$confirm" != "y" ] && [ "$$confirm" != "Y" ]; then \
echo "$(RED)Aborted$(NC)"; \
exit 1; \
fi; \
else \
echo "$(GREEN)โ On main branch$(NC)"; \
fi
@echo "$(GREEN)โ Release prerequisites check passed$(NC)"
# ============================================================================
# Build Metadata Tracking
# ============================================================================
build-metadata: ## Track build metadata in JSON format
@echo "$(YELLOW)๐ Tracking build metadata...$(NC)"
@mkdir -p $(BUILD_DIR)
@VERSION=$$(cat $(VERSION_FILE) 2>/dev/null || echo "0.0.0"); \
BUILD_NUM=$$(cat $(BUILD_NUMBER_FILE) 2>/dev/null || echo "0"); \
COMMIT=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
SHORT_COMMIT=$$(git rev-parse --short HEAD 2>/dev/null || echo "unknown"); \
BRANCH=$$(git branch --show-current 2>/dev/null || echo "unknown"); \
TIMESTAMP=$$(date -u +%Y-%m-%dT%H:%M:%SZ); \
PYTHON_VER=$$($(PYTHON) --version 2>&1); \
echo "{" > $(BUILD_DIR)/metadata.json; \
echo ' "version": "'$$VERSION'",' >> $(BUILD_DIR)/metadata.json; \
echo ' "build_number": '$$BUILD_NUM',' >> $(BUILD_DIR)/metadata.json; \
echo ' "commit": "'$$COMMIT'",' >> $(BUILD_DIR)/metadata.json; \
echo ' "commit_short": "'$$SHORT_COMMIT'",' >> $(BUILD_DIR)/metadata.json; \
echo ' "branch": "'$$BRANCH'",' >> $(BUILD_DIR)/metadata.json; \
echo ' "timestamp": "'$$TIMESTAMP'",' >> $(BUILD_DIR)/metadata.json; \
echo ' "python_version": "'$$PYTHON_VER'",' >> $(BUILD_DIR)/metadata.json; \
echo ' "environment": "'$${ENV:-development}'"' >> $(BUILD_DIR)/metadata.json; \
echo "}" >> $(BUILD_DIR)/metadata.json
@echo "$(GREEN)โ Build metadata saved to $(BUILD_DIR)/metadata.json$(NC)"
build-info-json: build-metadata ## Display build metadata from JSON
@if [ -f $(BUILD_DIR)/metadata.json ]; then \
cat $(BUILD_DIR)/metadata.json; \
else \
echo "$(YELLOW)No build metadata found. Run 'make build-metadata' first.$(NC)"; \
fi
# ============================================================================
# Version Bumping (requires VERSION file)
# ============================================================================
# NOTE: These targets assume a VERSION file exists.
# Customize the version bumping logic for your project's needs.
# Example: Use `bump2version`, `python-semantic-release`, or custom script.
# ============================================================================
patch: ## Bump patch version (X.Y.Z+1)
@echo "$(YELLOW)๐ง Bumping patch version...$(NC)"
@if [ ! -f "$(VERSION_FILE)" ]; then \
echo "$(RED)โ VERSION file not found$(NC)"; \
exit 1; \
fi
@CURRENT=$$(cat $(VERSION_FILE)); \
NEW=$$($(PYTHON) -c "import semver; print(semver.VersionInfo.parse('$$CURRENT').bump_patch())"); \
echo "$$NEW" > $(VERSION_FILE); \
echo "$(GREEN)โ Version bumped: $$CURRENT โ $$NEW$(NC)"
minor: ## Bump minor version (X.Y+1.0)
@echo "$(YELLOW)โจ Bumping minor version...$(NC)"
@if [ ! -f "$(VERSION_FILE)" ]; then \
echo "$(RED)โ VERSION file not found$(NC)"; \
exit 1; \
fi
@CURRENT=$$(cat $(VERSION_FILE)); \
NEW=$$($(PYTHON) -c "import semver; print(semver.VersionInfo.parse('$$CURRENT').bump_minor())"); \
echo "$$NEW" > $(VERSION_FILE); \
echo "$(GREEN)โ Version bumped: $$CURRENT โ $$NEW$(NC)"
major: ## Bump major version (X+1.0.0)
@echo "$(YELLOW)๐ฅ Bumping major version...$(NC)"
@if [ ! -f "$(VERSION_FILE)" ]; then \
echo "$(RED)โ VERSION file not found$(NC)"; \
exit 1; \
fi
@CURRENT=$$(cat $(VERSION_FILE)); \
NEW=$$($(PYTHON) -c "import semver; print(semver.VersionInfo.parse('$$CURRENT').bump_major())"); \
echo "$$NEW" > $(VERSION_FILE); \
echo "$(GREEN)โ Version bumped: $$CURRENT โ $$NEW$(NC)"
# ============================================================================
# Release Build
# ============================================================================
release-build: pre-publish ## Build Python package for release (runs quality checks first)
@echo "$(YELLOW)๐ฆ Building package...$(NC)"
@$(MAKE) build-metadata
@rm -rf $(DIST_DIR)/ $(BUILD_DIR)/ *.egg-info
@$(PYTHON) -m build $(BUILD_FLAGS)
@if command -v twine >/dev/null 2>&1; then \
twine check $(DIST_DIR)/*; \
echo "$(GREEN)โ Package validation passed$(NC)"; \
else \
echo "$(YELLOW)โ twine not found, skipping package validation$(NC)"; \
fi
@echo "$(GREEN)โ Package built successfully$(NC)"
@ls -la $(DIST_DIR)/
# ============================================================================
# Release Workflow Shortcuts
# ============================================================================
release-patch: release-check patch release-build ## Create a patch release (X.Y.Z+1)
@echo "$(GREEN)โ Patch release prepared$(NC)"
@echo "$(BLUE)Next: Run 'make release-publish' to publish$(NC)"
release-minor: release-check minor release-build ## Create a minor release (X.Y+1.0)
@echo "$(GREEN)โ Minor release prepared$(NC)"
@echo "$(BLUE)Next: Run 'make release-publish' to publish$(NC)"
release-major: release-check major release-build ## Create a major release (X+1.0.0)
@echo "$(GREEN)โ Major release prepared$(NC)"
@echo "$(BLUE)Next: Run 'make release-publish' to publish$(NC)"
# ============================================================================
# Publishing to PyPI
# ============================================================================
release-publish: ## Publish release to PyPI and create GitHub release
@echo "$(YELLOW)๐ Publishing release...$(NC)"
@VERSION=$$(cat $(VERSION_FILE)); \
echo "Publishing version: $$VERSION"; \
read -p "Continue with publishing? [y/N]: " confirm; \
if [ "$$confirm" != "y" ] && [ "$$confirm" != "Y" ]; then \
echo "$(RED)Publishing aborted$(NC)"; \
exit 1; \
fi
@echo "$(YELLOW)๐ค Publishing to PyPI...$(NC)"
@if command -v twine >/dev/null 2>&1; then \
$(PYTHON) -m twine upload $(DIST_DIR)/*; \
echo "$(GREEN)โ Published to PyPI$(NC)"; \
else \
echo "$(RED)โ twine not found. Install with: pip install twine$(NC)"; \
exit 1; \
fi
@echo "$(YELLOW)๐ค Creating GitHub release...$(NC)"
@VERSION=$$(cat $(VERSION_FILE)); \
gh release create "v$$VERSION" \
--title "v$$VERSION" \
--generate-notes \
$(DIST_DIR)/* || echo "$(YELLOW)โ GitHub release creation failed$(NC)"
@echo "$(GREEN)โ GitHub release created$(NC)"
@$(MAKE) release-verify
release-test-pypi: release-build ## Publish to TestPyPI for testing
@echo "$(YELLOW)๐งช Publishing to TestPyPI...$(NC)"
@if command -v twine >/dev/null 2>&1; then \
$(PYTHON) -m twine upload --repository testpypi $(DIST_DIR)/*; \
echo "$(GREEN)โ Published to TestPyPI$(NC)"; \
echo "$(BLUE)Test install: pip install --index-url https://test.pypi.org/simple/ <package-name>$(NC)"; \
else \
echo "$(RED)โ twine not found. Install with: pip install twine$(NC)"; \
exit 1; \
fi
# ============================================================================
# Release Verification
# ============================================================================
release-verify: ## Verify release across all channels
@echo "$(YELLOW)๐ Verifying release...$(NC)"
@VERSION=$$(cat $(VERSION_FILE)); \
echo "Verifying version: $$VERSION"; \
echo ""; \
echo "$(BLUE)๐ฆ PyPI:$(NC) https://pypi.org/project/<package-name>/$$VERSION/"; \
echo "$(BLUE)๐ท๏ธ GitHub:$(NC) https://github.com/<owner>/<repo>/releases/tag/v$$VERSION"; \
echo ""; \
echo "$(GREEN)โ Release verification links generated$(NC)"
@echo "$(BLUE)๐ก Test installation with:$(NC)"
@echo " pip install <package-name>==$$(cat $(VERSION_FILE))"
# ============================================================================
# Dry Run
# ============================================================================
release-dry-run: ## Show what a patch release would do (dry run)
@echo "$(YELLOW)๐ DRY RUN: Patch release preview$(NC)"
@echo "This would:"
@echo " 1. Check prerequisites and working directory"
@echo " 2. Bump patch version"
@echo " 3. Run pre-publish quality checks"
@echo " 4. Build Python package"
@echo " 5. Wait for confirmation to publish"
@echo " 6. Publish to PyPI and create GitHub release"
@echo " 7. Show verification links"
@echo ""
@echo "$(BLUE)Current version:$(NC) $$(cat $(VERSION_FILE) 2>/dev/null || echo 'unknown')"
@if [ -f "$(VERSION_FILE)" ]; then \
NEXT=$$($(PYTHON) -c "import semver; print(semver.VersionInfo.parse('$$(cat $(VERSION_FILE))').bump_patch())" 2>/dev/null || echo "unknown"); \
echo "$(BLUE)Next patch version would be:$(NC) $$NEXT"; \
fi
# ============================================================================
# Usage Examples
# ============================================================================
# Full release workflow (patch):
# make release-patch # Bump version, run checks, build
# make release-publish # Publish to PyPI + GitHub
#
# Full release workflow (minor):
# make release-minor # Bump version, run checks, build
# make release-publish # Publish to PyPI + GitHub
#
# Full release workflow (major):
# make release-major # Bump version, run checks, build
# make release-publish # Publish to PyPI + GitHub
#
# Test release on TestPyPI:
# make release-build
# make release-test-pypi
#
# Preview release:
# make release-dry-run
#
# Verify published release:
# make release-verify
#
# IMPORTANT: Customize package name in release-verify target!
# Replace <package-name>, <owner>, <repo> with actual values.
# ============================================================================