name: SmartClass CI/CD on: push: branches: [main, master] tags: ["v*"] pull_request: branches: ["*"] workflow_dispatch: env: REGISTRY: ghcr.io IMAGE_PREFIX: ${{ github.repository_owner }}/smart-attendance jobs: # ─── Python CI ───────────────────────────────────────────────────── python-ci: name: Python CI runs-on: ubuntu-latest services: redis: image: redis:7-alpine ports: - 6379:6379 options: >- --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 postgres: image: postgres:16-alpine env: POSTGRES_USER: sc_user POSTGRES_PASSWORD: test_password POSTGRES_DB: smartclass_test ports: - 5432:5432 options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - uses: actions/checkout@v4 - name: Set up Python 3.11 uses: actions/setup-python@v5 with: python-version: "3.11" cache: "pip" - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt pip install -r requirements-dev.txt - name: Lint with ruff run: | pip install ruff ruff check src/ services/ scripts/ ruff format --check src/ services/ scripts/ - name: Type check with mypy run: | pip install mypy mypy src/ --ignore-missing-imports - name: Verify imports run: | python -c "from src.edge_pipeline import EdgePipeline; print('Edge imports OK')" python -c "from services.api.app.main import app; print('API imports OK')" - name: Run tests env: DATABASE_URL: postgresql+asyncpg://sc_user:test_password@localhost:5432/smartclass_test REDIS_URL: redis://localhost:6379 JWT_SECRET_KEY: test_secret_key_for_ci_only_not_production ENVIRONMENT: test run: | pytest tests/ -v --tb=short --cov=src --cov=services --cov-report=xml - name: Upload coverage if: github.event_name == 'push' uses: actions/upload-artifact@v4 with: name: coverage-report path: coverage.xml # ─── Frontend CI ─────────────────────────────────────────────────── frontend-ci: name: Frontend CI runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Node.js 20 uses: actions/setup-node@v4 with: node-version: "20" cache: "npm" cache-dependency-path: frontend/package-lock.json - name: Install dependencies working-directory: frontend run: npm ci - name: Lint working-directory: frontend run: npm run lint - name: Type check working-directory: frontend run: npm run type-check || true - name: Build working-directory: frontend env: VITE_API_URL: http://localhost:8000 run: npm run build - name: Run tests working-directory: frontend run: npm test -- --run || true # ─── Docker Images ──────────────────────────────────────────────── docker-build: name: Build & Push Docker Images runs-on: ubuntu-latest needs: [python-ci, frontend-ci] if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' permissions: contents: read packages: write strategy: matrix: include: - image: edge context: . dockerfile: Dockerfile - image: api context: ./services/api dockerfile: Dockerfile - image: frontend context: ./frontend dockerfile: Dockerfile steps: - uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login 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_PREFIX }}-${{ matrix.image }} tags: | type=ref,event=branch type=ref,event=pr type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=sha,prefix= type=raw,value=latest,enable={{is_default_branch}} - name: Build and push uses: docker/build-push-action@v5 with: context: ${{ matrix.context }} file: ${{ matrix.context }}/${{ matrix.dockerfile }} push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max build-args: | VITE_API_URL=${{ vars.VITE_API_URL || 'http://localhost:8000' }} # ─── Deploy (on tag push) ───────────────────────────────────────── deploy: name: Deploy to Production runs-on: ubuntu-latest needs: [docker-build] if: startsWith(github.ref, 'refs/tags/v') environment: production steps: - uses: actions/checkout@v4 - name: Deploy notification run: | echo "🚀 Deploying version ${{ github.ref_name }} to production" echo "Images:" echo " - ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-edge:${{ github.ref_name }}" echo " - ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-api:${{ github.ref_name }}" echo " - ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-frontend:${{ github.ref_name }}" - name: Deploy via SSH if: vars.DEPLOY_HOST != '' uses: appleboy/ssh-action@v1 with: host: ${{ vars.DEPLOY_HOST }} username: ${{ vars.DEPLOY_USER }} key: ${{ secrets.DEPLOY_SSH_KEY }} script: | cd /opt/smartclass git pull origin main docker compose pull docker compose up -d --remove-orphans docker compose exec api alembic upgrade head echo "✅ Deployment complete"