diff --git a/.gitea/workflows/ci-cd.yml b/.gitea/workflows/ci-cd.yml new file mode 100644 index 0000000..2a4b40c --- /dev/null +++ b/.gitea/workflows/ci-cd.yml @@ -0,0 +1,126 @@ +name: CI/CD Pipeline + +on: + push: + branches: [ main, production ] + pull_request: + branches: [ main, production ] + +env: + NODE_VERSION: '20' + DOCKER_IMAGE: portfolio-app + CONTAINER_NAME: portfolio-app + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run linting + run: npm run lint + + - name: Run tests + run: npm run test + + - name: Build application + run: npm run build + + security: + runs-on: ubuntu-latest + needs: test + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + scan-type: 'fs' + scan-ref: '.' + format: 'sarif' + output: 'trivy-results.sarif' + + build: + runs-on: ubuntu-latest + needs: [test, security] + if: github.ref == 'refs/heads/production' + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker image + run: | + docker build -t ${{ env.DOCKER_IMAGE }}:latest . + docker tag ${{ env.DOCKER_IMAGE }}:latest ${{ env.DOCKER_IMAGE }}:$(date +%Y%m%d-%H%M%S) + + - name: Save Docker image + run: | + docker save ${{ env.DOCKER_IMAGE }}:latest | gzip > ${{ env.DOCKER_IMAGE }}.tar.gz + + - name: Upload Docker image artifact + uses: actions/upload-artifact@v4 + with: + name: docker-image + path: ${{ env.DOCKER_IMAGE }}.tar.gz + retention-days: 7 + + deploy: + runs-on: ubuntu-latest + needs: build + if: github.ref == 'refs/heads/production' + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download Docker image artifact + uses: actions/download-artifact@v4 + with: + name: docker-image + path: ./ + + - name: Load Docker image + run: | + gunzip -c ${{ env.DOCKER_IMAGE }}.tar.gz | docker load + + - name: Stop existing container + run: | + docker stop ${{ env.CONTAINER_NAME }} || true + docker rm ${{ env.CONTAINER_NAME }} || true + + - name: Start new container + run: | + docker run -d \ + --name ${{ env.CONTAINER_NAME }} \ + --restart unless-stopped \ + -p 3000:3000 \ + -e NODE_ENV=production \ + ${{ env.DOCKER_IMAGE }}:latest + + - name: Wait for container to be ready + run: | + sleep 10 + timeout 60 bash -c 'until curl -f http://localhost:3000/api/health; do sleep 2; done' + + - name: Health check + run: | + curl -f http://localhost:3000/api/health + echo "βœ… Deployment successful!" + + - name: Cleanup old images + run: | + docker image prune -f + docker system prune -f diff --git a/.gitea/workflows/quick-deploy.yml b/.gitea/workflows/quick-deploy.yml new file mode 100644 index 0000000..5cc75a4 --- /dev/null +++ b/.gitea/workflows/quick-deploy.yml @@ -0,0 +1,54 @@ +name: Quick Deploy + +on: + push: + branches: [ main ] + workflow_dispatch: + +env: + NODE_VERSION: '20' + DOCKER_IMAGE: portfolio-app + CONTAINER_NAME: portfolio-app + +jobs: + quick-deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build application + run: npm run build + + - name: Build Docker image + run: | + docker build -t ${{ env.DOCKER_IMAGE }}:latest . + + - name: Stop existing container + run: | + docker stop ${{ env.CONTAINER_NAME }} || true + docker rm ${{ env.CONTAINER_NAME }} || true + + - name: Start new container + run: | + docker run -d \ + --name ${{ env.CONTAINER_NAME }} \ + --restart unless-stopped \ + -p 3000:3000 \ + -e NODE_ENV=production \ + ${{ env.DOCKER_IMAGE }}:latest + + - name: Health check + run: | + sleep 10 + curl -f http://localhost:3000/api/health + echo "βœ… Quick deployment successful!" diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index cd415ae..4459e7a 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,6 +1,6 @@ services: portfolio: - image: ghcr.io/denshooter/my_portfolio:production + image: portfolio-app:latest container_name: portfolio-app restart: unless-stopped ports: diff --git a/package.json b/package.json index c7886ee..cde5bb2 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,8 @@ "deploy": "./scripts/deploy.sh", "auto-deploy": "./scripts/auto-deploy.sh", "quick-deploy": "./scripts/quick-deploy.sh", + "gitea-deploy": "./scripts/gitea-deploy.sh", + "setup-gitea-runner": "./scripts/setup-gitea-runner.sh", "monitor": "./scripts/monitor.sh", "health": "curl -f http://localhost:3000/api/health" }, diff --git a/scripts/gitea-deploy.sh b/scripts/gitea-deploy.sh new file mode 100755 index 0000000..c187cec --- /dev/null +++ b/scripts/gitea-deploy.sh @@ -0,0 +1,207 @@ +#!/bin/bash + +# Gitea-specific deployment script +# Optimiert fΓΌr lokalen Gitea Runner + +set -e + +# Configuration +PROJECT_NAME="portfolio" +CONTAINER_NAME="portfolio-app" +IMAGE_NAME="portfolio-app" +PORT=3000 +BACKUP_PORT=3001 +LOG_FILE="./logs/gitea-deploy.log" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Logging function +log() { + echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1" | tee -a "$LOG_FILE" +} + +error() { + echo -e "${RED}[ERROR]${NC} $1" | tee -a "$LOG_FILE" +} + +success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" | tee -a "$LOG_FILE" +} + +warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" | tee -a "$LOG_FILE" +} + +# Check if running as root +if [[ $EUID -eq 0 ]]; then + error "This script should not be run as root" + exit 1 +fi + +# Check if Docker is running +if ! docker info > /dev/null 2>&1; then + error "Docker is not running. Please start Docker and try again." + exit 1 +fi + +# Check if we're in the right directory +if [ ! -f "package.json" ] || [ ! -f "Dockerfile" ]; then + error "Please run this script from the project root directory" + exit 1 +fi + +log "πŸš€ Starting Gitea deployment for $PROJECT_NAME" + +# Step 1: Code Quality Checks +log "πŸ“‹ Step 1: Running code quality checks..." + +# Run linting +log "πŸ” Running ESLint..." +npm run lint || { + error "ESLint failed. Please fix the issues before deploying." + exit 1 +} + +# Run tests +log "πŸ§ͺ Running tests..." +npm run test || { + error "Tests failed. Please fix the issues before deploying." + exit 1 +} + +success "βœ… Code quality checks passed" + +# Step 2: Build Application +log "πŸ”¨ Step 2: Building application..." + +# Build Next.js application +log "πŸ“¦ Building Next.js application..." +npm run build || { + error "Build failed" + exit 1 +} + +success "βœ… Application built successfully" + +# Step 3: Docker Operations +log "🐳 Step 3: Docker operations..." + +# Build Docker image +log "πŸ—οΈ Building Docker image..." +docker build -t "$IMAGE_NAME:latest" . || { + error "Docker build failed" + exit 1 +} + +# Tag with timestamp +TIMESTAMP=$(date +%Y%m%d-%H%M%S) +docker tag "$IMAGE_NAME:latest" "$IMAGE_NAME:$TIMESTAMP" + +success "βœ… Docker image built successfully" + +# Step 4: Deployment +log "πŸš€ Step 4: Deploying application..." + +# Check if container is running +if [ "$(docker inspect -f '{{.State.Running}}' "$CONTAINER_NAME" 2>/dev/null)" = "true" ]; then + log "πŸ“¦ Stopping existing container..." + docker stop "$CONTAINER_NAME" || true + docker rm "$CONTAINER_NAME" || true +fi + +# Check if port is available +if lsof -Pi :$PORT -sTCP:LISTEN -t >/dev/null ; then + warning "Port $PORT is in use. Trying backup port $BACKUP_PORT" + DEPLOY_PORT=$BACKUP_PORT +else + DEPLOY_PORT=$PORT +fi + +# Start new container +log "πŸš€ Starting new container on port $DEPLOY_PORT..." +docker run -d \ + --name "$CONTAINER_NAME" \ + --restart unless-stopped \ + -p "$DEPLOY_PORT:3000" \ + -e NODE_ENV=production \ + "$IMAGE_NAME:latest" || { + error "Failed to start container" + exit 1 +} + +# Wait for container to be ready +log "⏳ Waiting for container to be ready..." +sleep 10 + +# Health check +log "πŸ₯ Performing health check..." +HEALTH_CHECK_TIMEOUT=60 +HEALTH_CHECK_INTERVAL=2 +ELAPSED=0 + +while [ $ELAPSED -lt $HEALTH_CHECK_TIMEOUT ]; do + if curl -f "http://localhost:$DEPLOY_PORT/api/health" > /dev/null 2>&1; then + success "βœ… Application is healthy!" + break + fi + + sleep $HEALTH_CHECK_INTERVAL + ELAPSED=$((ELAPSED + HEALTH_CHECK_INTERVAL)) + echo -n "." +done + +if [ $ELAPSED -ge $HEALTH_CHECK_TIMEOUT ]; then + error "Health check timeout. Application may not be running properly." + log "Container logs:" + docker logs "$CONTAINER_NAME" --tail=50 + exit 1 +fi + +# Step 5: Verification +log "βœ… Step 5: Verifying deployment..." + +# Test main page +if curl -f "http://localhost:$DEPLOY_PORT/" > /dev/null 2>&1; then + success "βœ… Main page is accessible" +else + error "❌ Main page is not accessible" + exit 1 +fi + +# Show container status +log "πŸ“Š Container status:" +docker ps --filter "name=$CONTAINER_NAME" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" + +# Show resource usage +log "πŸ“ˆ Resource usage:" +docker stats --no-stream --format "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}" "$CONTAINER_NAME" + +# Step 6: Cleanup +log "🧹 Step 6: Cleaning up old images..." + +# Remove old images (keep last 3 versions) +docker images "$IMAGE_NAME" --format "table {{.Tag}}\t{{.ID}}" | tail -n +2 | head -n -3 | awk '{print $2}' | xargs -r docker rmi || { + warning "No old images to remove" +} + +# Clean up unused Docker resources +docker system prune -f --volumes || { + warning "Failed to clean up Docker resources" +} + +# Final success message +success "πŸŽ‰ Gitea deployment completed successfully!" +log "🌐 Application is available at: http://localhost:$DEPLOY_PORT" +log "πŸ₯ Health check endpoint: http://localhost:$DEPLOY_PORT/api/health" +log "πŸ“Š Container name: $CONTAINER_NAME" +log "πŸ“ Logs: docker logs $CONTAINER_NAME" + +# Update deployment log +echo "$(date): Gitea deployment successful - Port: $DEPLOY_PORT - Image: $IMAGE_NAME:$TIMESTAMP" >> "$LOG_FILE" + +exit 0 diff --git a/scripts/setup-gitea-runner.sh b/scripts/setup-gitea-runner.sh new file mode 100755 index 0000000..37148e0 --- /dev/null +++ b/scripts/setup-gitea-runner.sh @@ -0,0 +1,192 @@ +#!/bin/bash + +# Gitea Runner Setup Script +# Installiert und konfiguriert einen lokalen Gitea Runner + +set -e + +# Configuration +GITEA_URL="${GITEA_URL:-http://localhost:3000}" +RUNNER_NAME="${RUNNER_NAME:-portfolio-runner}" +RUNNER_LABELS="${RUNNER_LABELS:-ubuntu-latest,self-hosted,portfolio}" +RUNNER_WORK_DIR="${RUNNER_WORK_DIR:-/tmp/gitea-runner}" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Logging function +log() { + echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1" +} + +error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +# Check if running as root +if [[ $EUID -eq 0 ]]; then + error "This script should not be run as root" + exit 1 +fi + +log "πŸš€ Setting up Gitea Runner for Portfolio" + +# Check if Gitea URL is accessible +log "πŸ” Checking Gitea server accessibility..." +if ! curl -f "$GITEA_URL" > /dev/null 2>&1; then + error "Cannot access Gitea server at $GITEA_URL" + error "Please make sure Gitea is running and accessible" + exit 1 +fi +success "βœ… Gitea server is accessible" + +# Create runner directory +log "πŸ“ Creating runner directory..." +mkdir -p "$RUNNER_WORK_DIR" +cd "$RUNNER_WORK_DIR" + +# Download Gitea Runner +log "πŸ“₯ Downloading Gitea Runner..." +RUNNER_VERSION="latest" +RUNNER_ARCH="linux-amd64" + +# Get latest version +if [ "$RUNNER_VERSION" = "latest" ]; then + RUNNER_VERSION=$(curl -s https://api.github.com/repos/woodpecker-ci/woodpecker/releases/latest | grep -o '"tag_name": "[^"]*' | grep -o '[^"]*$') +fi + +RUNNER_URL="https://github.com/woodpecker-ci/woodpecker/releases/download/${RUNNER_VERSION}/woodpecker-agent_${RUNNER_VERSION}_${RUNNER_ARCH}.tar.gz" + +log "Downloading from: $RUNNER_URL" +curl -L -o woodpecker-agent.tar.gz "$RUNNER_URL" + +# Extract runner +log "πŸ“¦ Extracting Gitea Runner..." +tar -xzf woodpecker-agent.tar.gz +chmod +x woodpecker-agent + +success "βœ… Gitea Runner downloaded and extracted" + +# Create systemd service +log "βš™οΈ Creating systemd service..." +sudo tee /etc/systemd/system/gitea-runner.service > /dev/null < "$RUNNER_WORK_DIR/start-runner.sh" << 'EOF' +#!/bin/bash +echo "Starting Gitea Runner..." +sudo systemctl start gitea-runner +sudo systemctl status gitea-runner +EOF + +# Stop script +cat > "$RUNNER_WORK_DIR/stop-runner.sh" << 'EOF' +#!/bin/bash +echo "Stopping Gitea Runner..." +sudo systemctl stop gitea-runner +EOF + +# Status script +cat > "$RUNNER_WORK_DIR/status-runner.sh" << 'EOF' +#!/bin/bash +echo "Gitea Runner Status:" +sudo systemctl status gitea-runner +echo "" +echo "Logs (last 20 lines):" +sudo journalctl -u gitea-runner -n 20 --no-pager +EOF + +# Logs script +cat > "$RUNNER_WORK_DIR/logs-runner.sh" << 'EOF' +#!/bin/bash +echo "Gitea Runner Logs:" +sudo journalctl -u gitea-runner -f +EOF + +chmod +x "$RUNNER_WORK_DIR"/*.sh + +success "βœ… Helper scripts created" + +# Create environment file +cat > "$RUNNER_WORK_DIR/.env" << EOF +# Gitea Runner Configuration +GITEA_URL=$GITEA_URL +RUNNER_NAME=$RUNNER_NAME +RUNNER_LABELS=$RUNNER_LABELS +RUNNER_WORK_DIR=$RUNNER_WORK_DIR +EOF + +log "πŸ“‹ Setup Summary:" +echo " β€’ Runner Directory: $RUNNER_WORK_DIR" +echo " β€’ Gitea URL: $GITEA_URL" +echo " β€’ Runner Name: $RUNNER_NAME" +echo " β€’ Labels: $RUNNER_LABELS" +echo " β€’ Helper Scripts: $RUNNER_WORK_DIR/*.sh" +echo "" + +log "🎯 Next Steps:" +echo "1. Register the runner in Gitea web interface" +echo "2. Enable and start the service" +echo "3. Test with a workflow run" +echo "" + +success "πŸŽ‰ Gitea Runner setup completed!" +log "πŸ“ All files are in: $RUNNER_WORK_DIR"