feat: Add comprehensive Gitea CI/CD workflows for multi-arch container builds

- Add build-container.yml: Main build pipeline with multi-arch support
- Add pr-check.yml: Pull request validation with comprehensive testing
- Add release.yml: Automated release pipeline with security scanning
- Add nightly.yml: Daily builds with performance testing
- Add health_check.sh: Container health validation script
- Add setup-ci.sh: Local CI/CD environment setup script
- Add comprehensive CI/CD documentation

Features:
- Multi-architecture builds (linux/amd64, linux/arm64)
- Security scanning with Trivy
- Automated PyPI publishing for releases
- Container registry integration
- Performance testing and validation
- Artifact management and cleanup
- Build caching and optimization

Supports full development workflow from PR to production deployment.
This commit is contained in:
William Valentin
2025-09-15 02:04:07 -07:00
parent 1f0604cba6
commit 25666a76cf
8 changed files with 2123 additions and 0 deletions

306
scripts/health_check.sh Executable file
View File

@@ -0,0 +1,306 @@
#!/bin/bash
# Health check script for UnitForge CI/CD workflows
# Tests basic functionality of the running application
set -e
# Configuration
HOST=${HOST:-localhost}
PORT=${PORT:-8000}
TIMEOUT=${TIMEOUT:-30}
MAX_RETRIES=${MAX_RETRIES:-5}
RETRY_DELAY=${RETRY_DELAY:-2}
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Helper 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"
}
# Check if application is responding
check_health() {
local url="http://${HOST}:${PORT}/health"
local retry_count=0
log_info "Checking health endpoint: $url"
while [ $retry_count -lt "$MAX_RETRIES" ]; do
if curl -s -f --max-time "$TIMEOUT" "$url" > /dev/null 2>&1; then
log_info "Health check passed"
return 0
fi
retry_count=$((retry_count + 1))
log_warn "Health check failed (attempt $retry_count/$MAX_RETRIES)"
if [ $retry_count -lt "$MAX_RETRIES" ]; then
log_info "Retrying in ${RETRY_DELAY} seconds..."
sleep "$RETRY_DELAY"
fi
done
log_error "Health check failed after $MAX_RETRIES attempts"
return 1
}
# Check if main page loads
check_main_page() {
local url="http://${HOST}:${PORT}/"
log_info "Checking main page: $url"
local response
response=$(curl -s -w "%{http_code}" --max-time "$TIMEOUT" "$url")
local http_code="${response: -3}"
if [ "$http_code" = "200" ]; then
log_info "Main page check passed (HTTP $http_code)"
return 0
else
log_error "Main page check failed (HTTP $http_code)"
return 1
fi
}
# Check API endpoints
check_api() {
local base_url="http://${HOST}:${PORT}/api"
log_info "Checking API endpoints"
# Check API health
local api_health_url="${base_url}/health"
if curl -s -f --max-time "$TIMEOUT" "$api_health_url" > /dev/null 2>&1; then
log_info "API health endpoint passed"
else
log_warn "API health endpoint failed or not available"
fi
# Check API version
local api_version_url="${base_url}/version"
if curl -s -f --max-time "$TIMEOUT" "$api_version_url" > /dev/null 2>&1; then
log_info "API version endpoint passed"
else
log_warn "API version endpoint failed or not available"
fi
return 0
}
# Check static assets
check_static_assets() {
log_info "Checking static assets"
local assets=(
"/static/css/style.css"
"/static/js/app.js"
"/static/vendor/bootstrap/css/bootstrap.min.css"
"/static/vendor/fontawesome/css/all.min.css"
)
local failed_assets=0
for asset in "${assets[@]}"; do
local url="http://${HOST}:${PORT}${asset}"
if curl -s -f --max-time "$TIMEOUT" "$url" > /dev/null 2>&1; then
log_info "Asset check passed: $asset"
else
log_warn "Asset check failed: $asset"
failed_assets=$((failed_assets + 1))
fi
done
if [ $failed_assets -eq 0 ]; then
log_info "All static assets available"
return 0
else
log_warn "$failed_assets static assets failed to load"
return 0 # Don't fail health check for missing assets
fi
}
# Performance test
check_performance() {
log_info "Running basic performance test"
local url="http://${HOST}:${PORT}/"
local response_time
# Test response time
local response_time
response_time=$(curl -s -w "%{time_total}" --max-time "$TIMEOUT" -o /dev/null "$url")
if curl -s -w "%{time_total}" --max-time "$TIMEOUT" -o /dev/null "$url" > /dev/null 2>&1; then
log_info "Response time: ${response_time}s"
# Check if response time is reasonable (< 5 seconds)
if (( $(echo "$response_time < 5.0" | bc -l) )); then
log_info "Performance check passed"
return 0
else
log_warn "Performance check warning: slow response time (${response_time}s)"
return 0 # Don't fail health check for slow response
fi
else
log_error "Performance check failed: no response"
return 1
fi
}
# Memory usage check (if running in container)
check_memory() {
if command -v docker > /dev/null 2>&1 && [ -n "$CONTAINER_NAME" ]; then
log_info "Checking container memory usage"
local memory_usage
memory_usage=$(docker stats "$CONTAINER_NAME" --no-stream --format "{{.MemUsage}}" | cut -d'/' -f1)
if [ -n "$memory_usage" ]; then
log_info "Memory usage: $memory_usage"
else
log_warn "Could not determine memory usage"
fi
fi
}
# Wait for application to start
wait_for_startup() {
log_info "Waiting for application to start..."
local startup_timeout=60
local elapsed=0
while [ $elapsed -lt $startup_timeout ]; do
if curl -s --max-time 5 "http://${HOST}:${PORT}/" > /dev/null 2>&1; then
log_info "Application is responding"
return 0
fi
sleep 5
elapsed=$((elapsed + 5))
log_info "Waiting... (${elapsed}s/${startup_timeout}s)"
done
log_error "Application failed to start within ${startup_timeout} seconds"
return 1
}
# Main health check function
run_health_check() {
log_info "Starting UnitForge health check"
log_info "Target: http://${HOST}:${PORT}"
local failed_checks=0
# Wait for startup if needed
if ! curl -s --max-time 5 "http://${HOST}:${PORT}/" > /dev/null 2>&1; then
wait_for_startup || return 1
fi
# Run all checks
check_health || failed_checks=$((failed_checks + 1))
check_main_page || failed_checks=$((failed_checks + 1))
check_api || failed_checks=$((failed_checks + 1))
check_static_assets || true # Don't count static asset failures
check_performance || true # Don't count performance warnings
check_memory || true # Don't count memory check failures
# Summary
if [ $failed_checks -eq 0 ]; then
log_info "✅ All health checks passed"
return 0
else
log_error "$failed_checks health checks failed"
return 1
fi
}
# Usage information
usage() {
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Options:"
echo " -h, --host HOST Target host (default: localhost)"
echo " -p, --port PORT Target port (default: 8000)"
echo " -t, --timeout TIMEOUT Request timeout in seconds (default: 30)"
echo " -r, --retries RETRIES Maximum retry attempts (default: 5)"
echo " -d, --delay DELAY Retry delay in seconds (default: 2)"
echo " -c, --container NAME Container name for memory checks"
echo " --help Show this help message"
echo ""
echo "Environment variables:"
echo " HOST Same as --host"
echo " PORT Same as --port"
echo " TIMEOUT Same as --timeout"
echo " MAX_RETRIES Same as --retries"
echo " RETRY_DELAY Same as --delay"
echo " CONTAINER_NAME Same as --container"
echo ""
echo "Examples:"
echo " $0 # Check localhost:8000"
echo " $0 -h production.example.com -p 80 # Check production server"
echo " $0 -c unitforge-container # Include container memory check"
}
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-h|--host)
HOST="$2"
shift 2
;;
-p|--port)
PORT="$2"
shift 2
;;
-t|--timeout)
TIMEOUT="$2"
shift 2
;;
-r|--retries)
MAX_RETRIES="$2"
shift 2
;;
-d|--delay)
RETRY_DELAY="$2"
shift 2
;;
-c|--container)
CONTAINER_NAME="$2"
shift 2
;;
--help)
usage
exit 0
;;
*)
echo "Unknown option: $1"
usage
exit 1
;;
esac
done
# Check dependencies
if ! command -v curl > /dev/null 2>&1; then
log_error "curl is required but not installed"
exit 1
fi
if ! command -v bc > /dev/null 2>&1; then
log_warn "bc is not installed, performance timing may not work properly"
fi
# Run the health check
run_health_check
exit $?

445
scripts/setup-ci.sh Executable file
View File

@@ -0,0 +1,445 @@
#!/bin/bash
# CI/CD Setup Script for UnitForge
# Sets up local environment for testing CI/CD workflows
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Helper 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"
}
log_step() {
echo -e "${BLUE}[STEP]${NC} $1"
}
# Check if command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Check Docker and Docker Buildx
check_docker() {
log_step "Checking Docker installation..."
if ! command_exists docker; then
log_error "Docker is not installed"
echo "Please install Docker from: https://docs.docker.com/get-docker/"
return 1
fi
log_info "Docker found: $(docker --version)"
# Check if Docker daemon is running
if ! docker info >/dev/null 2>&1; then
log_error "Docker daemon is not running"
echo "Please start Docker daemon"
return 1
fi
# Check Docker Buildx
if ! docker buildx version >/dev/null 2>&1; then
log_warn "Docker Buildx not found, installing..."
docker buildx install 2>/dev/null || true
fi
log_info "Docker Buildx found: $(docker buildx version)"
return 0
}
# Setup Docker Buildx for multi-arch builds
setup_buildx() {
log_step "Setting up Docker Buildx for multi-arch builds..."
# Create builder if it doesn't exist
if ! docker buildx ls | grep -q "unitforge-builder"; then
log_info "Creating unitforge-builder..."
docker buildx create --name unitforge-builder --use
else
log_info "Using existing unitforge-builder"
docker buildx use unitforge-builder
fi
# Bootstrap the builder
log_info "Bootstrapping builder..."
docker buildx inspect --bootstrap
log_info "Builder setup complete"
return 0
}
# Check container registry access
check_registry() {
log_step "Checking container registry configuration..."
if [ -f ".env" ]; then
log_info "Found .env file"
if grep -q "CONTAINER_REGISTRY_URL" .env; then
local registry_url
registry_url=$(grep '^CONTAINER_REGISTRY_URL=' .env | cut -d'=' -f2)
log_info "Registry URL: $registry_url"
else
log_warn "CONTAINER_REGISTRY_URL not found in .env"
fi
if grep -q "CONTAINER_TAG" .env; then
local container_tag
container_tag=$(grep '^CONTAINER_TAG=' .env | cut -d'=' -f2)
log_info "Container tag: $container_tag"
else
log_warn "CONTAINER_TAG not found in .env"
fi
else
log_warn ".env file not found"
log_info "Creating sample .env file..."
cat > .env << EOF
# Container Registry Configuration
CONTAINER_REGISTRY_URL=gitea-http.taildb3494.ts.net/will/unitforge
CONTAINER_TAG=latest
# Development Configuration
DEBUG=true
LOG_LEVEL=debug
EOF
log_info "Sample .env file created. Please update with your registry details."
fi
return 0
}
# Verify vendor assets
check_vendor_assets() {
log_step "Checking vendor assets..."
local assets=(
"frontend/static/vendor/bootstrap/css/bootstrap.min.css"
"frontend/static/vendor/bootstrap/js/bootstrap.bundle.min.js"
"frontend/static/vendor/fontawesome/css/all.min.css"
"frontend/static/vendor/fontawesome/webfonts/fa-solid-900.woff2"
"frontend/static/img/osi-logo.svg"
)
local missing_assets=0
for asset in "${assets[@]}"; do
if [ ! -f "$asset" ]; then
log_warn "Missing asset: $asset"
missing_assets=$((missing_assets + 1))
else
log_info "Found asset: $asset"
fi
done
if [ $missing_assets -gt 0 ]; then
log_error "$missing_assets vendor assets are missing"
log_info "Please ensure all vendor assets are downloaded and committed"
log_info "Run: make setup-dev to download missing assets"
return 1
fi
log_info "All vendor assets found"
return 0
}
# Test local build
test_local_build() {
log_step "Testing local Docker build..."
if docker build -t unitforge:test . >/dev/null 2>&1; then
log_info "Local Docker build successful"
# Test container startup
log_info "Testing container startup..."
if docker run -d --name unitforge-test -p 8080:8000 unitforge:test >/dev/null 2>&1; then
sleep 5
if curl -s -f http://localhost:8080/ >/dev/null 2>&1; then
log_info "Container startup test successful"
else
log_warn "Container started but not responding on port 8080"
fi
docker stop unitforge-test >/dev/null 2>&1
docker rm unitforge-test >/dev/null 2>&1
else
log_warn "Container startup test failed"
fi
# Clean up test image
docker rmi unitforge:test >/dev/null 2>&1 || true
return 0
else
log_error "Local Docker build failed"
return 1
fi
}
# Test multi-arch build
test_multiarch_build() {
log_step "Testing multi-architecture build..."
if docker buildx build --platform linux/amd64,linux/arm64 -t unitforge:multiarch-test . >/dev/null 2>&1; then
log_info "Multi-architecture build successful"
# Clean up
docker buildx rm --force >/dev/null 2>&1 || true
return 0
else
log_error "Multi-architecture build failed"
return 1
fi
}
# Check development environment
check_dev_environment() {
log_step "Checking development environment..."
# Check uv
if ! command_exists uv; then
log_error "uv is not installed"
echo "Install with: curl -LsSf https://astral.sh/uv/install.sh | sh"
return 1
fi
log_info "uv found: $(uv --version)"
# Check Python
if ! command_exists python3; then
log_error "Python 3 is not installed"
return 1
fi
log_info "Python found: $(python3 --version)"
# Check if virtual environment exists
if [ -d ".venv" ]; then
log_info "Virtual environment exists"
else
log_warn "Virtual environment not found"
log_info "Run: make setup-dev to create it"
fi
return 0
}
# Test CI/CD workflow syntax
check_workflow_syntax() {
log_step "Checking workflow syntax..."
local workflows_dir=".gitea/workflows"
if [ ! -d "$workflows_dir" ]; then
log_error "Workflows directory not found: $workflows_dir"
return 1
fi
local yaml_files=("$workflows_dir"/*.yml)
if [ ${#yaml_files[@]} -eq 0 ]; then
log_warn "No YAML workflow files found"
return 0
fi
local syntax_errors=0
for file in "${yaml_files[@]}"; do
if [ -f "$file" ]; then
log_info "Checking syntax: $(basename "$file")"
# Basic YAML syntax check (if python3 is available)
if command_exists python3; then
if python3 -c "import yaml; yaml.safe_load(open('$file'))" 2>/dev/null; then
log_info "$(basename "$file") syntax OK"
else
log_error "$(basename "$file") has syntax errors"
syntax_errors=$((syntax_errors + 1))
fi
else
log_warn "Python3 not available, skipping YAML syntax check"
fi
fi
done
if [ $syntax_errors -eq 0 ]; then
log_info "All workflow files have valid syntax"
return 0
else
log_error "$syntax_errors workflow files have syntax errors"
return 1
fi
}
# Generate CI/CD documentation
generate_docs() {
log_step "Generating CI/CD documentation..."
local docs_dir="docs/ci-cd"
mkdir -p "$docs_dir"
cat > "$docs_dir/local-testing.md" << 'EOF'
# Local CI/CD Testing
This guide helps you test CI/CD workflows locally before pushing to the repository.
## Prerequisites
- Docker with Buildx support
- uv package manager
- Python 3.8+
## Local Testing Commands
```bash
# Test local build
make docker-build
# Test multi-arch build
make docker-buildx-local
# Test full development workflow
make dev
# Run health checks
./scripts/health_check.sh
```
## Workflow Testing
Use `act` to test GitHub/Gitea workflows locally:
```bash
# Install act
curl https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash
# Test PR workflow
act pull_request -s CONTAINER_REGISTRY_USERNAME=test -s CONTAINER_REGISTRY_PASSWORD=test
# Test release workflow
act push -e tests/fixtures/release-event.json
```
## Troubleshooting
### Build Issues
- Ensure all vendor assets are committed
- Check Docker daemon is running
- Verify buildx is properly configured
### Registry Issues
- Check .env file configuration
- Verify registry credentials
- Test registry connectivity
### Performance Issues
- Use build cache: `--cache-from type=gha`
- Optimize Docker layers
- Use multi-stage builds
```
EOF
log_info "Local testing documentation generated: $docs_dir/local-testing.md"
return 0
}
# Main setup function
main() {
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE} UnitForge CI/CD Setup${NC}"
echo -e "${BLUE}========================================${NC}"
echo ""
local failed_checks=0
# Run all checks
check_docker || failed_checks=$((failed_checks + 1))
setup_buildx || failed_checks=$((failed_checks + 1))
check_registry || true # Don't fail on registry issues
check_vendor_assets || failed_checks=$((failed_checks + 1))
check_dev_environment || failed_checks=$((failed_checks + 1))
check_workflow_syntax || failed_checks=$((failed_checks + 1))
# Optional tests
if [ "$1" = "--test-build" ]; then
test_local_build || failed_checks=$((failed_checks + 1))
test_multiarch_build || failed_checks=$((failed_checks + 1))
fi
# Generate documentation
generate_docs || true
echo ""
echo -e "${BLUE}========================================${NC}"
if [ $failed_checks -eq 0 ]; then
log_info "✅ CI/CD setup completed successfully!"
echo ""
echo "Next steps:"
echo "1. Update .env with your registry details"
echo "2. Test local build: make docker-buildx-local"
echo "3. Run full test suite: make dev"
echo "4. Check workflow syntax: ./scripts/setup-ci.sh"
echo ""
echo "For testing builds:"
echo " ./scripts/setup-ci.sh --test-build"
else
log_error "❌ CI/CD setup completed with $failed_checks issues"
echo ""
echo "Please fix the issues above before proceeding."
exit 1
fi
echo -e "${BLUE}========================================${NC}"
}
# Usage information
usage() {
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Options:"
echo " --test-build Run local and multi-arch build tests"
echo " --help Show this help message"
echo ""
echo "This script sets up your local environment for CI/CD development."
echo "It checks Docker, Buildx, dependencies, and workflow syntax."
}
# Parse command line arguments
case "${1:-}" in
--help)
usage
exit 0
;;
--test-build)
main --test-build
;;
"")
main
;;
*)
echo "Unknown option: $1"
usage
exit 1
;;
esac