Table of Contents
- Project Overview and Architecture
- Technology Stack Analysis
- Project Structure and Organization
- Phase 1: Development Environment Setup
- Phase 2: Application Development
- Phase 3: Containerization with Docker
- Phase 4: Version Control and Git Workflow
- Phase 5: CI/CD Pipeline Implementation
- Phase 6: Infrastructure as Code with Terraform
- Phase 7: Kubernetes Deployment
- Phase 8: Monitoring and Observability
- Phase 9: Security Implementation
- Phase 10: Documentation and Presentation
- Publishing Strategy for LinkedIn and GitHub
Project Overview and Architecture
What We’re Building
You’re creating a production-grade DevOps pipeline for a Flask web application that demonstrates enterprise-level practices. This project will showcase your ability to implement modern software delivery methodologies, infrastructure automation, and operational excellence.
Why This Project Matters
In today’s software industry, DevOps practices are essential for delivering reliable, scalable applications. This project demonstrates your understanding of the complete software delivery lifecycle, from code development to production deployment and monitoring.
Architecture Overview
Developer → Git → GitHub → GitHub Actions → Docker → Kubernetes → GCP → Monitoring
↓ ↓ ↓ ↓ ↓ ↓ ↓
Local IDE → Version → CI/CD Pipeline → Container → Orchestration → Cloud → Observability
Technology Stack Analysis
Core Technologies (Enhanced from Your List)
Application Layer
- Python 3.11+: Modern Python version with performance improvements
- Flask 2.3+: Lightweight web framework with extensive ecosystem
- Gunicorn: WSGI HTTP Server for production deployment
- Flask-CORS: Cross-Origin Resource Sharing support
- Flask-Limiter: Rate limiting for API protection
Development and Testing
- pytest: Industry-standard testing framework
- pytest-cov: Code coverage reporting
- black: Code formatting for consistency
- flake8: Linting for code quality
- mypy: Static type checking
- pre-commit: Git hooks for quality gates
Containerization
- Docker: Container platform
- Docker Compose: Multi-container orchestration
- Distroless Images: Minimal, secure base images
Version Control and Collaboration
- Git: Distributed version control
- Git Flow: Branching strategy
- Conventional Commits: Standardized commit messages
- Semantic Versioning: Version management strategy
CI/CD Pipeline
- GitHub Actions: Automation platform
- GitHub Container Registry: Private image registry
- Dependabot: Automated dependency updates
- CodeQL: Security analysis
Infrastructure and Orchestration
- Terraform: Infrastructure as Code
- Google Cloud Platform: Cloud provider
- Google Kubernetes Engine (GKE): Managed Kubernetes
- Helm: Kubernetes package manager
- Kustomize: Kubernetes configuration management
Monitoring and Observability
- Prometheus: Metrics collection
- Grafana: Visualization and dashboards
- Loki: Log aggregation
- Jaeger: Distributed tracing
- AlertManager: Alert routing and management
Security
- Trivy: Vulnerability scanning
- Google Secret Manager: Secrets management
- Network Policies: Kubernetes security
- RBAC: Role-based access control
Project Structure and Organization
Directory Structure
flask-devops-project/
├── .github/
│ ├── workflows/
│ │ ├── ci.yml
│ │ ├── cd-dev.yml
│ │ ├── cd-staging.yml
│ │ └── cd-prod.yml
│ ├── ISSUE_TEMPLATE/
│ └── PULL_REQUEST_TEMPLATE.md
├── app/
│ ├── __init__.py
│ ├── main.py
│ ├── models/
│ ├── routes/
│ ├── utils/
│ └── templates/
├── tests/
│ ├── unit/
│ ├── integration/
│ └── e2e/
├── docker/
│ ├── Dockerfile
│ ├── Dockerfile.dev
│ └── docker-compose.yml
├── k8s/
│ ├── base/
│ ├── overlays/
│ │ ├── dev/
│ │ ├── staging/
│ │ └── prod/
│ └── helm-chart/
├── terraform/
│ ├── modules/
│ ├── environments/
│ │ ├── dev/
│ │ ├── staging/
│ │ └── prod/
│ └── global/
├── monitoring/
│ ├── prometheus/
│ ├── grafana/
│ └── loki/
├── docs/
│ ├── architecture/
│ ├── deployment/
│ └── api/
├── scripts/
│ ├── setup.sh
│ ├── deploy.sh
│ └── test.sh
├── .env.example
├── .gitignore
├── .pre-commit-config.yaml
├── requirements.txt
├── requirements-dev.txt
├── Pipfile
├── README.md
├── CHANGELOG.md
├── CONTRIBUTING.md
└── LICENSE
Phase 1: Development Environment Setup
Purpose and Objectives
Setting up a consistent, reproducible development environment is crucial for maintaining code quality and ensuring that all team members work with the same tools and configurations. This phase establishes the foundation for all subsequent development work.
Technologies and Tools
- Python 3.11+: Latest stable version with performance improvements
- Poetry/Pipenv: Dependency management and virtual environments
- Pre-commit hooks: Automated code quality checks
- IDE configuration: VS Code or PyCharm setup
Industry Best Practices
Modern development environments should be containerized and version-controlled to ensure consistency across different machines and team members. The principle of “infrastructure as code” applies to development environments as well.
Step-by-Step Implementation
Task 1.1: System Prerequisites
Create a file named docs/setup/system-requirements.md:
# System Requirements
## Required Software
- Python 3.11+
- Docker Desktop
- Git
- Google Cloud SDK
- Terraform
- kubectl
- Helm
## Installation Commands
### macOS (using Homebrew)
brew install python@3.11 docker git google-cloud-sdk terraform kubectl helm
### Ubuntu/Debian
# Python 3.11
sudo apt update
sudo apt install python3.11 python3.11-venv python3.11-pip
# Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
# Additional tools
# Follow official installation guides for each tool
Task 1.2: Python Environment Setup
Create scripts/setup-dev.sh:
#!/bin/bash
# Development environment setup script
set -e
echo "Setting up Flask DevOps Project development environment..."
# Create virtual environment
python3.11 -m venv venv
source venv/bin/activate
# Upgrade pip
pip install --upgrade pip
# Install Poetry for dependency management
pip install poetry
# Install dependencies
poetry install --with dev
# Install pre-commit hooks
pre-commit install
# Create environment files
cp .env.example .env.dev
cp .env.example .env.test
echo "Development environment setup complete!"
echo "Activate with: source venv/bin/activate"
Task 1.3: Pre-commit Configuration
Create .pre-commit-config.yaml:
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- id: check-merge-conflict
- repo: https://github.com/psf/black
rev: 23.3.0
hooks:
- id: black
language_version: python3.11
- repo: https://github.com/pycqa/flake8
rev: 6.0.0
hooks:
- id: flake8
additional_dependencies: [flake8-docstrings]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.3.0
hooks:
- id: mypy
additional_dependencies: [types-all]
- repo: https://github.com/pycqa/isort
rev: 5.12.0
hooks:
- id: isort
args: ["--profile", "black"]
Phase 2: Application Development
Purpose and Objectives
This phase focuses on creating a well-structured Flask application that demonstrates modern Python development practices. The application will serve as the foundation for showcasing DevOps practices throughout the project.
Application Architecture
We’ll build a RESTful API with the following features:
- Health check endpoints
- User management
- Task management
- Metrics exposure for monitoring
- Comprehensive logging
Industry Best Practices
- Separation of Concerns: Clear separation between routes, business logic, and data models
- Configuration Management: Environment-based configuration
- Error Handling: Comprehensive error handling and logging
- API Design: RESTful principles and OpenAPI documentation
- Testing: Unit, integration, and end-to-end tests
Step-by-Step Implementation
Task 2.1: Application Structure Setup
Create the main application files:
app/__init__.py:
"""Flask application factory pattern implementation."""
from flask import Flask
from flask_cors import CORS
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
import logging
import os
from typing import Optional
def create_app(config_name: Optional[str] = None) -> Flask:
"""
Application factory pattern for creating Flask app instances.
Args:
config_name: Configuration environment name
Returns:
Configured Flask application instance
"""
app = Flask(__name__)
# Load configuration
config_name = config_name or os.getenv('FLASK_ENV', 'development')
app.config.from_object(f'app.config.{config_name.title()}Config')
# Initialize extensions
CORS(app)
# Rate limiting
limiter = Limiter(
app,
key_func=get_remote_address,
default_limits=["1000 per hour"]
)
# Configure logging
configure_logging(app)
# Register blueprints
from app.routes.health import health_bp
from app.routes.api import api_bp
app.register_blueprint(health_bp)
app.register_blueprint(api_bp, url_prefix='/api/v1')
return app
def configure_logging(app: Flask) -> None:
"""Configure application logging."""
if not app.debug and not app.testing:
if not os.path.exists('logs'):
os.mkdir('logs')
file_handler = logging.FileHandler('logs/app.log')
file_handler.setFormatter(logging.Formatter(
'%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
))
file_handler.setLevel(logging.INFO)
app.logger.addHandler(file_handler)
app.logger.setLevel(logging.INFO)
app.logger.info('Flask DevOps application startup')
Task 2.2: Configuration Management
Create app/config.py:
"""Application configuration management."""
import os
from typing import Type
class Config:
"""Base configuration class."""
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production'
# Database configuration
DATABASE_URL = os.environ.get('DATABASE_URL') or 'sqlite:///app.db'
# Redis configuration for caching
REDIS_URL = os.environ.get('REDIS_URL') or 'redis://localhost:6379/0'
# Logging configuration
LOG_LEVEL = os.environ.get('LOG_LEVEL') or 'INFO'
# Metrics configuration
METRICS_ENABLED = os.environ.get('METRICS_ENABLED', 'true').lower() == 'true'
class DevelopmentConfig(Config):
"""Development environment configuration."""
DEBUG = True
TESTING = False
class TestingConfig(Config):
"""Testing environment configuration."""
DEBUG = False
TESTING = True
DATABASE_URL = 'sqlite:///:memory:'
class StagingConfig(Config):
"""Staging environment configuration."""
DEBUG = False
TESTING = False
class ProductionConfig(Config):
"""Production environment configuration."""
DEBUG = False
TESTING = False
config = {
'development': DevelopmentConfig,
'testing': TestingConfig,
'staging': StagingConfig,
'production': ProductionConfig,
'default': DevelopmentConfig
}
def get_config(config_name: str) -> Type[Config]:
"""Get configuration class by name."""
return config.get(config_name, config['default'])
Task 2.3: Health Check Endpoints
Create app/routes/health.py:
"""Health check endpoints for monitoring and load balancing."""
from flask import Blueprint, jsonify, current_app
from datetime import datetime
import psutil
import os
health_bp = Blueprint('health', __name__)
@health_bp.route('/health')
def health_check():
"""
Basic health check endpoint.
Returns:
JSON response indicating service health status
"""
return jsonify({
'status': 'healthy',
'timestamp': datetime.utcnow().isoformat(),
'service': 'flask-devops-app',
'version': os.environ.get('APP_VERSION', '1.0.0')
})
@health_bp.route('/health/ready')
def readiness_check():
"""
Kubernetes readiness probe endpoint.
Checks if the application is ready to receive traffic.
"""
try:
# Add checks for database connectivity, external services, etc.
checks = {
'database': check_database_connection(),
'external_apis': check_external_dependencies()
}
all_ready = all(checks.values())
status_code = 200 if all_ready else 503
return jsonify({
'status': 'ready' if all_ready else 'not_ready',
'checks': checks,
'timestamp': datetime.utcnow().isoformat()
}), status_code
except Exception as e:
current_app.logger.error(f"Readiness check failed: {str(e)}")
return jsonify({
'status': 'not_ready',
'error': str(e),
'timestamp': datetime.utcnow().isoformat()
}), 503
@health_bp.route('/health/live')
def liveness_check():
"""
Kubernetes liveness probe endpoint.
Checks if the application is running and should be restarted if not.
"""
try:
# Basic liveness checks
memory_usage = psutil.virtual_memory().percent
cpu_usage = psutil.cpu_percent(interval=1)
# Consider unhealthy if memory usage > 90% or CPU > 95%
is_healthy = memory_usage < 90 and cpu_usage < 95
return jsonify({
'status': 'alive' if is_healthy else 'unhealthy',
'metrics': {
'memory_usage_percent': memory_usage,
'cpu_usage_percent': cpu_usage
},
'timestamp': datetime.utcnow().isoformat()
}), 200 if is_healthy else 503
except Exception as e:
current_app.logger.error(f"Liveness check failed: {str(e)}")
return jsonify({
'status': 'unhealthy',
'error': str(e),
'timestamp': datetime.utcnow().isoformat()
}), 503
def check_database_connection() -> bool:
"""Check database connectivity."""
# Implement actual database connection check
return True
def check_external_dependencies() -> bool:
"""Check external service dependencies."""
# Implement checks for external APIs, services, etc.
return True
Phase 3: Containerization with Docker
Purpose and Objectives
Containerization ensures consistent deployment across different environments and simplifies the deployment process. This phase focuses on creating optimized, secure Docker images following industry best practices.
Docker Best Practices
- Multi-stage builds: Reduce image size and improve security
- Minimal base images: Use distroless or Alpine images
- Security scanning: Implement vulnerability scanning
- Layer optimization: Minimize layers and leverage caching
- Non-root execution: Run containers as non-root users
Step-by-Step Implementation
Task 3.1: Production Dockerfile
Create docker/Dockerfile:
# Multi-stage build for optimized production image
FROM python:3.11-slim as builder
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
curl \
&& rm -rf /var/lib/apt/lists/*
# Create and activate virtual environment
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# Copy requirements and install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Production stage
FROM python:3.11-slim as production
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PATH="/opt/venv/bin:$PATH" \
FLASK_ENV=production
# Install runtime dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get clean
# Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser
# Copy virtual environment from builder stage
COPY --from=builder /opt/venv /opt/venv
# Set working directory
WORKDIR /app
# Copy application code
COPY app/ ./app/
COPY wsgi.py ./
# Create necessary directories and set permissions
RUN mkdir -p /app/logs && \
chown -R appuser:appuser /app
# Switch to non-root user
USER appuser
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
# Expose port
EXPOSE 8000
# Use Gunicorn for production
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "4", "--timeout", "120", "wsgi:app"]
Task 3.2: Development Dockerfile
Create docker/Dockerfile.dev:
# Development Dockerfile with hot reloading
FROM python:3.11-slim
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
FLASK_ENV=development \
FLASK_DEBUG=1
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
curl \
git \
&& rm -rf /var/lib/apt/lists/*
# Set working directory
WORKDIR /app
# Copy requirements and install dependencies
COPY requirements-dev.txt .
RUN pip install --no-cache-dir -r requirements-dev.txt
# Copy application code (will be overridden by volume in docker-compose)
COPY . .
# Expose port
EXPOSE 5000
# Use Flask development server
CMD ["flask", "run", "--host=0.0.0.0", "--port=5000", "--reload"]
Task 3.3: Docker Compose Configuration
Create docker/docker-compose.yml:
version: '3.8'
services:
app:
build:
context: ..
dockerfile: docker/Dockerfile.dev
ports:
- "5000:5000"
volumes:
- ../app:/app/app
- ../tests:/app/tests
environment:
- FLASK_ENV=development
- FLASK_DEBUG=1
- DATABASE_URL=sqlite:///app.db
- REDIS_URL=redis://redis:6379/0
depends_on:
- redis
networks:
- app-network
redis:
image: redis:7-alpine
ports:
- "6379:6379"
networks:
- app-network
prometheus:
image: prom/prometheus:latest
ports:
- "9090:9090"
volumes:
- ../monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
networks:
- app-network
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
volumes:
- grafana-storage:/var/lib/grafana
networks:
- app-network
networks:
app-network:
driver: bridge
volumes:
grafana-storage:
Task 3.4: Docker Ignore File
Create .dockerignore:
# Git
.git
.gitignore
# CI/CD
.github/
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual environments
venv/
env/
ENV/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Logs
*.log
logs/
# Environment files
.env*
!.env.example
# Testing
.coverage
.pytest_cache/
htmlcov/
# Documentation
docs/_build/
# Terraform
*.tfstate
*.tfstate.*
.terraform/
# Kubernetes
*.tmp
Phase 4: Version Control and Git Workflow
Purpose and Objectives
Implementing a robust version control strategy is essential for managing code changes, enabling collaboration, and maintaining release quality. This phase establishes Git workflows that align with industry standards.
Git Workflow Strategy
We’ll implement Git Flow, which provides a robust branching model suitable for projects with scheduled releases and multiple environments.
Branching Strategy
- main: Production-ready code
- develop: Integration branch for features
- feature/*: Individual feature development
- release/*: Release preparation
- hotfix/*: Emergency fixes to production
Versioning Strategy
We’ll use Semantic Versioning (SemVer) with the format MAJOR.MINOR.PATCH:
- MAJOR: Breaking changes
- MINOR: New features (backward compatible)
- PATCH: Bug fixes (backward compatible)
Step-by-Step Implementation
Task 4.1: Git Configuration
Create .gitignore:
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Terraform
*.tfstate
*.tfstate.*
.terraform/
.terraform.lock.hcl
# Kubernetes
*.tmp
# Logs
logs/
*.log
# Environment files
.env.*
!.env.example
Task 4.2: Conventional Commits Configuration
Create .gitmessage:
# <type>[optional scope]: <description>
#
# [optional body]
#
# [optional footer(s)]
#
# Type must be one of the following:
# feat: A new feature
# fix: A bug fix
# docs: Documentation only changes
# style: Changes that do not affect the meaning of the code
# refactor: A code change that neither fixes a bug nor adds a feature
# perf: A code change that improves performance
# test: Adding missing tests or correcting existing tests
# build: Changes that affect the build system or external dependencies
# ci: Changes to our CI configuration files and scripts
# chore: Other changes that don't modify src or test files
# revert: Reverts a previous commit
#
# Examples:
# feat(auth): add OAuth2 integration
# fix(api): resolve timeout issue in user endpoint
# docs: update deployment guide
# style: fix code formatting
# refactor(database): optimize query performance
# test(integration): add user registration tests
# ci: update GitHub Actions workflow
Task 4.3: Git Hooks Setup
Create scripts/git-hooks/commit-msg:
#!/bin/bash
# Conventional commits validation
commit_regex='^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?: .{1,50}'
if ! grep -qE "$commit_regex" "$1"; then
echo "Invalid commit message format!"
echo ""
echo "Valid format: <type>[optional scope]: <description>"
echo ""
echo "Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert"
echo ""
echo "Examples:"
echo " feat(auth): add OAuth2 integration"
echo " fix(api): resolve timeout issue"
echo " docs: update deployment guide"
echo ""
exit 1
fi
Task 4.4: Release Management Script
Create scripts/release.sh:
“`bash
!/bin/bash
Automated release script following semantic versioning
set -e
Colors for output
RED=’\033[0;31m’
GREEN=’\033[0;32m’
YELLOW=’\033[1;33m’
NC=’\033[0m’ # No Color
Functions
log_info() {
echo -e “${GREEN}[INFO]${NC} $1”
}
log_warn() {
echo -e “${YELLOW}[WARN]${NC} $1”
}
log_error() {
echo -e “${RED}[ERROR]${NC} $1”
}
Get current version from git tags
get_current_version() {
git describe –tags –abbrev=0 2>/dev/null || echo “v0.0.0”
}
Parse version components
parse_version() {
local version=$1
version=${version#v} # Remove ‘v’ prefix
echo $version | sed -E ‘s/([0-9]+).([0-9]+).([0-9]+)/\1 \2 \3/’
}
Increment version based on type
increment_version() {
local current_version=$1
local increment_type=$2
read -r major minor patch <<< $(parse_version $current_version)
case $increment_type in
major)
major=$((major + 1))
minor=0
patch=0
;;
minor)
minor=$((minor + 1))
patch=0
;;
patch)
patch=$((patch + 1))
;;
*)
log_error "Invalid increment type: $increment_type"
exit 1
;;
esac
echo "v${major}.${minor}.${patch}"
}
Main release function
create_release() {
local increment_type=$1
# Validate git status
if [[ -n $(git status --porcelain) ]]; then
log_error "Working directory is not clean. Please commit or stash changes."
exit 1
fi
# Ensure we're on develop branch
current_branch=$(git rev-parse --abbrev-ref HEAD)
if [[ "$current_branch" != "develop" ]]; then
log_error "Releases must be created from the develop branch"
exit 1
fi
# Get current version and calculate new version
current_version=$(get_current_version)
new_version=$(increment_version $current_version $increment_type)
log_info "Current version: $current_version"
log_info "New version: $new_version"
# Create release branch
release_branch="release/$new_version"
log_info "Creating release branch: $release_branch"
git checkout -b $release_branch
# Update version in files
echo $new_version > VERSION
sed -i.bak "s/VERSION = .*/VERSION = \"${new_version#v}\"/" app/__init__.py
rm -f app/__init__.py.bak