name: HR Portal CI/CD on: push: branches: - main # 生產環境 - dev # 測試環境 pull_request: branches: - main - dev env: REGISTRY: git.lab.taipei IMAGE_NAME_BACKEND: porscheworld/hr-portal-backend IMAGE_NAME_FRONTEND: porscheworld/hr-portal-frontend jobs: # ============================================== # 測試階段 - 後端 # ============================================== test-backend: name: Test Backend (FastAPI) runs-on: ubuntu-latest services: postgres: image: postgres:16-alpine env: POSTGRES_DB: hr_portal_test POSTGRES_USER: test_user POSTGRES_PASSWORD: test_password ports: - 5432:5432 options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - name: Checkout Code uses: actions/checkout@v4 - name: Set up Python 3.11 uses: actions/setup-python@v4 with: python-version: '3.11' cache: 'pip' - name: Install Dependencies working-directory: ./backend run: | pip install --upgrade pip pip install -r requirements.txt - name: Run Tests with Coverage working-directory: ./backend env: DATABASE_HOST: localhost DATABASE_PORT: 5432 DATABASE_NAME: hr_portal_test DATABASE_USER: test_user DATABASE_PASSWORD: test_password run: | pytest tests/ --cov=app --cov-report=term-missing --cov-report=xml - name: Upload Coverage to Codecov uses: codecov/codecov-action@v3 with: file: ./backend/coverage.xml flags: backend name: backend-coverage # ============================================== # 測試階段 - 前端 # ============================================== test-frontend: name: Test Frontend (Next.js) runs-on: ubuntu-latest steps: - name: Checkout Code uses: actions/checkout@v4 - name: Set up Node.js 18 uses: actions/setup-node@v4 with: node-version: '18' cache: 'npm' cache-dependency-path: ./frontend/package-lock.json - name: Install Dependencies working-directory: ./frontend run: npm ci - name: Run Lint working-directory: ./frontend run: npm run lint - name: Run Type Check working-directory: ./frontend run: npx tsc --noEmit - name: Build working-directory: ./frontend run: npm run build # ============================================== # 建置與推送映像 - 後端 # ============================================== build-backend: name: Build Backend Image needs: test-backend runs-on: ubuntu-latest if: github.event_name == 'push' steps: - name: Checkout Code uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to Gitea Container Registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ secrets.GITEA_USERNAME }} password: ${{ secrets.GITEA_TOKEN }} - name: Extract metadata id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_BACKEND }} tags: | type=ref,event=branch type=sha,prefix={{branch}}- type=raw,value=latest,enable={{is_default_branch}} - name: Build and Push uses: docker/build-push-action@v5 with: context: ./backend file: ./backend/Dockerfile push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME_BACKEND }}:buildcache cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME_BACKEND }}:buildcache,mode=max # ============================================== # 建置與推送映像 - 前端 # ============================================== build-frontend: name: Build Frontend Image needs: test-frontend runs-on: ubuntu-latest if: github.event_name == 'push' steps: - name: Checkout Code uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to Gitea Container Registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ secrets.GITEA_USERNAME }} password: ${{ secrets.GITEA_TOKEN }} - name: Extract metadata id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_FRONTEND }} tags: | type=ref,event=branch type=sha,prefix={{branch}}- type=raw,value=latest,enable={{is_default_branch}} - name: Build and Push uses: docker/build-push-action@v5 with: context: ./frontend file: ./frontend/Dockerfile push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME_FRONTEND }}:buildcache cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME_FRONTEND }}:buildcache,mode=max # ============================================== # 部署到測試環境 (dev 分支) # ============================================== deploy-testing: name: Deploy to Testing Environment needs: [build-backend, build-frontend] runs-on: ubuntu-latest if: github.ref == 'refs/heads/dev' environment: name: testing url: https://test.hr.ease.taipei steps: - name: Checkout Code uses: actions/checkout@v4 - name: Deploy to Testing Server uses: appleboy/ssh-action@v1.0.0 with: host: ${{ secrets.DEPLOY_HOST }} username: ${{ secrets.DEPLOY_USER }} key: ${{ secrets.DEPLOY_SSH_KEY }} port: 22 script: | cd /opt/deployments/hr-portal-test # 拉取最新映像 docker-compose -f docker-compose.prod.yml pull # 重啟服務 docker-compose -f docker-compose.prod.yml up -d # 清理舊映像 docker image prune -f # 檢查服務狀態 docker-compose -f docker-compose.prod.yml ps # ============================================== # 部署到生產環境 (main 分支) # ============================================== deploy-production: name: Deploy to Production Environment needs: [build-backend, build-frontend] runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' environment: name: production url: https://hr.ease.taipei steps: - name: Checkout Code uses: actions/checkout@v4 - name: Deploy to Production Server uses: appleboy/ssh-action@v1.0.0 with: host: ${{ secrets.DEPLOY_HOST }} username: ${{ secrets.DEPLOY_USER }} key: ${{ secrets.DEPLOY_SSH_KEY }} port: 22 script: | cd /opt/deployments/hr-portal-prod # 拉取最新映像 docker-compose -f docker-compose.prod.yml pull # 執行資料庫備份 docker exec hr-portal-postgres-prod pg_dump -U hr_admin hr_portal | gzip > backup-$(date +%Y%m%d-%H%M%S).sql.gz # 滾動更新 (零停機部署) docker-compose -f docker-compose.prod.yml up -d --no-deps --build backend sleep 10 docker-compose -f docker-compose.prod.yml up -d --no-deps --build frontend # 清理舊映像 docker image prune -f # 檢查服務狀態 docker-compose -f docker-compose.prod.yml ps # 健康檢查 curl -f https://hr-api.ease.taipei/health || exit 1 # ============================================== # 通知 # ============================================== notify: name: Send Notification needs: [deploy-testing, deploy-production] runs-on: ubuntu-latest if: always() steps: - name: Send Notification run: | echo "Deployment completed for branch: ${{ github.ref_name }}" # TODO: 整合 Email 或 Slack 通知