From eb33f470719678ca8b574135da4591860e2b8895 Mon Sep 17 00:00:00 2001 From: donpat1to Date: Tue, 21 Oct 2025 22:20:26 +0200 Subject: [PATCH] added ci --- .github/workflows/docker.yml | 161 ++++++++++++++++++ Dockerfile | 102 +++++++++++ backend/dockerfile | 8 - backend/dockerfilexpython | 61 ------- backend/package.json | 6 +- .../src/models/defaults/employeeDefaults.ts | 2 - backend/src/scripts/verify-python.js | 27 +++ docker-compose.yml | 32 ++-- frontend/dockerfile | 8 - module.config.js | 30 ++++ 10 files changed, 345 insertions(+), 92 deletions(-) create mode 100644 .github/workflows/docker.yml create mode 100644 Dockerfile delete mode 100644 backend/dockerfile delete mode 100644 backend/dockerfilexpython create mode 100644 backend/src/scripts/verify-python.js delete mode 100644 frontend/dockerfile create mode 100644 module.config.js diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..2fa13df --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,161 @@ +name: CI/CD Pipeline + +on: + push: + branches: [ main, master, development ] + pull_request: + branches: [ main, master, development ] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + set-tag: + name: Set Tag Name + runs-on: ubuntu-latest + outputs: + tag_name: ${{ steps.set_tag.outputs.tag_name }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch all history for tags + + - name: Determine next semantic version tag + id: set_tag + run: | + git fetch --tags + + # Find latest tag matching vX.Y.Z + latest_tag=$(git tag --list 'v*.*.*' --sort=-v:refname | head -n 1) + if [[ -z "$latest_tag" ]]; then + major=0 + minor=0 + patch=0 + else + version="${latest_tag#v}" + IFS='.' read -r major minor patch <<< "$version" + fi + + if [[ "${GITHUB_REF}" == "refs/heads/main" || "${GITHUB_REF}" == "refs/heads/master" ]]; then + major=$((major + 1)) + minor=0 + patch=0 + elif [[ "${GITHUB_REF}" == "refs/heads/development" ]]; then + minor=$((minor + 1)) + patch=0 + else + patch=$((patch + 1)) + fi + + new_tag="v${major}.${minor}.${patch}" + echo "tag_name=${new_tag}" >> $GITHUB_OUTPUT + echo "Next version tag: ${new_tag}" + + test: + runs-on: ubuntu-latest + needs: set-tag + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: backend/package-lock.json + + - name: Install backend dependencies + working-directory: ./backend + run: npm ci + + - name: Run TypeScript check + working-directory: ./backend + run: npx tsc --noEmit + + - name: Run backend tests + working-directory: ./backend + run: npm test --if-present + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install Python dependencies + run: pip install ortools + + - name: Test Python integration + run: | + python -c "from ortools.sat.python import cp_model; print('OR-Tools available')" + + - name: Display next version + run: | + echo "Next version will be: ${{ needs.set-tag.outputs.tag_name }}" + + build-and-push: + needs: [set-tag, test] + runs-on: ubuntu-latest + if: github.event_name == 'push' + + permissions: + contents: read + packages: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=semver,pattern={{version}} + type=sha + # Add the dynamically generated semantic version + ${{ needs.set-tag.outputs.tag_name }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Create Git Tag + if: success() + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git tag ${{ needs.set-tag.outputs.tag_name }} + git push origin ${{ needs.set-tag.outputs.tag_name }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Display pushed images + run: | + echo "✅ Docker images pushed successfully!" + echo "📦 Image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" + echo "🏷️ Tags: ${{ steps.meta.outputs.tags }}" + echo "🚀 New version: ${{ needs.set-tag.outputs.tag_name }}" \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b00a9d2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,102 @@ +# Multi-stage build for combined frontend + backend +FROM node:20-alpine AS backend-builder + +WORKDIR /app/backend + +# Install Python and required build tools for OR-Tools +RUN apk add --no-cache \ + python3 \ + py3-pip \ + build-base \ + python3-dev \ + cmake \ + make \ + g++ \ + linux-headers + +# Create symlink so python3 is callable as python +RUN ln -sf /usr/bin/python3 /usr/bin/python + +# Upgrade pip and install Python dependencies +RUN python -m pip install --upgrade pip && \ + pip install ortools + +# Copy backend files +COPY backend/package*.json ./ +COPY backend/tsconfig.json ./ + +# Install backend dependencies +RUN npm ci + +# Copy backend source +COPY backend/src/ ./src/ +COPY backend/python-scripts/ ./python-scripts/ + +# Build backend +RUN npm run build + +# Verify Python and OR-Tools installation +RUN python -c "from ortools.sat.python import cp_model; print('OR-Tools installed successfully')" + +# Frontend build stage +FROM node:20-alpine AS frontend-builder + +WORKDIR /app/frontend + +# Copy frontend files +COPY frontend/package*.json ./ +COPY frontend/tsconfig.json ./ + +# Install frontend dependencies +RUN npm ci + +# Copy frontend source +COPY frontend/src/ ./src/ +COPY frontend/public/ ./public/ + +# Build frontend +RUN npm run build + +# Production stage +FROM node:20-alpine + +WORKDIR /app + +# Install Python and OR-Tools for production +RUN apk add --no-cache \ + python \ + py3-pip \ + && pip3 install ortools + +# Install PM2 for process management +RUN npm install -g pm2 + +# Copy backend built files +COPY --from=backend-builder /app/backend/package*.json ./ +COPY --from=backend-builder /app/backend/dist/ ./dist/ +COPY --from=backend-builder /app/backend/node_modules/ ./node_modules/ +COPY --from=backend-builder /app/backend/python-scripts/ ./python-scripts/ + +# Copy frontend built files +COPY --from=frontend-builder /app/frontend/build/ ./frontend-build/ + +# Copy PM2 configuration +COPY ecosystem.config.js ./ + +# Create a non-root user +RUN addgroup -g 1001 -S nodejs && \ + adduser -S schichtplan -u 1001 && \ + chown -R schichtplan:nodejs /app + +USER schichtplan + +# Verify installations +RUN python --version && \ + python -c "from ortools.sat.python import cp_model; print('OR-Tools verified')" + +EXPOSE 3000 3002 + +HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:3002/api/health || exit 1 + +CMD ["pm2-runtime", "ecosystem.config.js"] \ No newline at end of file diff --git a/backend/dockerfile b/backend/dockerfile deleted file mode 100644 index e644bb6..0000000 --- a/backend/dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -# backend/Dockerfile -FROM node:18-alpine -WORKDIR /app -COPY package*.json ./ -RUN npm ci --only=production -COPY . . -EXPOSE 3002 -CMD ["node", "dist/server.js"] \ No newline at end of file diff --git a/backend/dockerfilexpython b/backend/dockerfilexpython deleted file mode 100644 index d3bd054..0000000 --- a/backend/dockerfilexpython +++ /dev/null @@ -1,61 +0,0 @@ -# Multi-stage Dockerfile for Node.js + Python application -FROM node:20-alpine AS node-base - -# Install Python and build dependencies in Node stage -RUN apk add --no-cache \ - python3 \ - py3-pip \ - build-base \ - python3-dev - -# Install Python dependencies -COPY python-scripts/requirements.txt /tmp/requirements.txt -RUN pip3 install --no-cache-dir -r /tmp/requirements.txt - -# Set working directory -WORKDIR /app - -# Copy package files -COPY package*.json ./ - -# Install Node.js dependencies -RUN npm ci --only=production - -# Build stage -FROM node-base AS builder - -# Install all dependencies (including dev dependencies) -RUN npm ci - -# Copy source code -COPY . . - -# Build the application -RUN npm run build - -# Production stage -FROM node-base AS production - -# Copy built application from builder stage -COPY --from=builder /app/dist ./dist -COPY --from=builder /app/package*.json ./ - -# Copy Python scripts -COPY --from=builder /app/python-scripts ./python-scripts - -# Create non-root user -RUN addgroup -g 1001 -S nodejs && \ - adduser -S nextjs -u 1001 - -# Change to non-root user -USER nextjs - -# Expose port -EXPOSE 3000 - -# Health check -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD node dist/health-check.js - -# Start the application -CMD ["npm", "start"] \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index ea9eb4b..e7e4f55 100644 --- a/backend/package.json +++ b/backend/package.json @@ -6,7 +6,11 @@ "dev": "npm run build && npx tsx src/server.ts", "build": "tsc", "start": "node dist/server.js", - "prestart": "npm run build" + "prestart": "npm run build", + "test": "jest", + "test:python": "python3 -c \"from ortools.sat.python import cp_model; print('OR-Tools OK')\"", + "docker:build": "docker build -t schichtplan-backend .", + "docker:run": "docker run -p 3001:3001 schichtplan-backend" }, "dependencies": { "@types/bcrypt": "^6.0.0", diff --git a/backend/src/models/defaults/employeeDefaults.ts b/backend/src/models/defaults/employeeDefaults.ts index e80e189..27118ff 100644 --- a/backend/src/models/defaults/employeeDefaults.ts +++ b/backend/src/models/defaults/employeeDefaults.ts @@ -102,7 +102,6 @@ export const AVAILABILITY_PREFERENCES = { } as const; // Default availability for new employees (all shifts unavailable as level 3) -// UPDATED: Now uses shiftId instead of timeSlotId + dayOfWeek export function createDefaultAvailabilities(employeeId: string, planId: string, shiftIds: string[]): Omit[] { const availabilities: Omit[] = []; @@ -120,7 +119,6 @@ export function createDefaultAvailabilities(employeeId: string, planId: string, } // Create complete manager availability for all days (default: only Mon-Tue available) -// NOTE: This function might need revision based on new schema requirements export function createManagerDefaultSchedule(managerId: string, planId: string, timeSlotIds: string[]): Omit[] { const assignments: Omit[] = []; diff --git a/backend/src/scripts/verify-python.js b/backend/src/scripts/verify-python.js new file mode 100644 index 0000000..e6048d4 --- /dev/null +++ b/backend/src/scripts/verify-python.js @@ -0,0 +1,27 @@ +import { spawn } from 'child_process'; +import path from 'path'; + +export function runPythonScript(scriptPath, args = []) { + return new Promise((resolve, reject) => { + const pythonProcess = spawn('python3', [scriptPath, ...args]); + + let stdout = ''; + let stderr = ''; + + pythonProcess.stdout.on('data', (data) => { + stdout += data.toString(); + }); + + pythonProcess.stderr.on('data', (data) => { + stderr += data.toString(); + }); + + pythonProcess.on('close', (code) => { + if (code === 0) { + resolve(stdout); + } else { + reject(new Error(`Python script exited with code ${code}: ${stderr}`)); + } + }); + }); +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 9dfddfb..a1f08e8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,18 +1,26 @@ version: '3.8' -services: - frontend: - build: ./frontend - ports: - - "80:80" - depends_on: - - backend - backend: - build: ./backend +services: + schichtplan: + build: + context: . + dockerfile: backend/Dockerfile ports: - "3001:3001" + - "3000:3000" environment: - - DATABASE_URL=file:./dev.db - - JWT_SECRET=your-secret-key + - NODE_ENV=production + - DATABASE_URL=file:./prod.db + - JWT_SECRET=your-production-secret-key-change-this + - PYTHON_PATH=/usr/bin/python3 + volumes: + - app_data:/app/data + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3001/health"] + interval: 30s + timeout: 10s + retries: 3 - # Später: Database service hinzufügen \ No newline at end of file +volumes: + app_data: \ No newline at end of file diff --git a/frontend/dockerfile b/frontend/dockerfile deleted file mode 100644 index 173efe2..0000000 --- a/frontend/dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -# backend/Dockerfile -FROM node:18-alpine -WORKDIR /app -COPY package*.json ./ -RUN npm ci --only=production -COPY . . -EXPOSE 3001 -CMD ["node", "dist/server.js"] \ No newline at end of file diff --git a/module.config.js b/module.config.js new file mode 100644 index 0000000..97bbaa0 --- /dev/null +++ b/module.config.js @@ -0,0 +1,30 @@ +module.exports = { + apps: [ + { + name: 'backend', + script: './dist/server.js', + instances: 1, + exec_mode: 'fork', + env: { + NODE_ENV: 'production', + PORT: 3002 + }, + error_file: './logs/backend-err.log', + out_file: './logs/backend-out.log', + time: true + }, + { + name: 'frontend', + script: 'npx', + args: 'serve -s frontend-build -l 3000', + instances: 1, + exec_mode: 'fork', + env: { + NODE_ENV: 'production' + }, + error_file: './logs/frontend-err.log', + out_file: './logs/frontend-out.log', + time: true + } + ] +}; \ No newline at end of file