| 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: |
| 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: |
| 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-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: |
| 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" |
| |