# ============================================================================= # GitHub Actions - CI/CD Pipeline para App Padel # Fase 7.4 - Go Live y Soporte # ============================================================================= # # Este workflow automatiza: # - Tests # - Build # - Deploy a Staging # - Deploy a ProducciΓ³n (manual) # # Requiere los siguientes secrets en GitHub: # - SSH_PRIVATE_KEY: Clave SSH para acceso al servidor # - SERVER_HOST: IP o dominio del servidor # - SERVER_USER: Usuario SSH # - ENV_PRODUCTION: Variables de entorno de producciΓ³n (base64) # - ENV_STAGING: Variables de entorno de staging (base64) # ============================================================================= name: CI/CD Pipeline on: push: branches: [main, develop] tags: ['v*'] pull_request: branches: [main, develop] env: NODE_VERSION: '20' REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: # =========================================================================== # Job 1: Test # =========================================================================== test: name: πŸ§ͺ Tests runs-on: ubuntu-latest defaults: run: working-directory: ./backend 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' cache-dependency-path: './backend/package-lock.json' - name: πŸ“¦ Install dependencies run: npm ci - name: πŸ”§ Generate Prisma Client run: npx prisma generate - name: 🧹 Run linter run: npm run lint continue-on-error: true - name: πŸ§ͺ Run tests run: npm test continue-on-error: true env: NODE_ENV: test DATABASE_URL: file:./test.db JWT_SECRET: test-secret-key - name: πŸ“Š Upload coverage uses: codecov/codecov-action@v3 if: github.ref == 'refs/heads/main' with: directory: ./backend/coverage flags: backend name: backend-coverage # =========================================================================== # Job 2: Build # =========================================================================== build: name: πŸ—οΈ Build runs-on: ubuntu-latest needs: test if: github.event_name == 'push' outputs: image_tag: ${{ steps.meta.outputs.tags }} version: ${{ steps.version.outputs.version }} steps: - name: πŸ“₯ Checkout code uses: actions/checkout@v4 - name: πŸ—οΈ Setup 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_NAME }} tags: | type=ref,event=branch type=ref,event=pr type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=sha,prefix={{branch}}- type=raw,value=latest,enable={{is_default_branch}} - name: πŸ”’ Generate version id: version run: | VERSION=$(echo ${{ github.sha }} | cut -c1-7) if [[ "${{ github.ref }}" == refs/tags/v* ]]; then VERSION=${GITHUB_REF#refs/tags/v} fi echo "version=$VERSION" >> $GITHUB_OUTPUT echo "Version: $VERSION" - name: 🐳 Build and push Docker image uses: docker/build-push-action@v5 with: context: ./backend file: ./backend/Dockerfile.prod push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max build-args: | VERSION=${{ steps.version.outputs.version }} BUILD_DATE=${{ github.event.head_commit.timestamp }} # =========================================================================== # Job 3: Deploy to Staging # =========================================================================== deploy-staging: name: πŸš€ Deploy to Staging runs-on: ubuntu-latest needs: [test, build] if: github.ref == 'refs/heads/develop' environment: name: staging url: https://staging.tudominio.com steps: - name: πŸ“₯ Checkout code uses: actions/checkout@v4 - name: πŸ” Setup SSH uses: webfactory/ssh-agent@v0.8.0 with: ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} - name: πŸš€ Deploy to Staging env: SERVER_HOST: ${{ secrets.SERVER_HOST }} SERVER_USER: ${{ secrets.SERVER_USER }} run: | # AΓ±adir host a known_hosts mkdir -p ~/.ssh ssh-keyscan -H $SERVER_HOST >> ~/.ssh/known_hosts # Crear directorio de deploy si no existe ssh $SERVER_USER@$SERVER_HOST "mkdir -p ~/padel-staging" # Copiar docker-compose y archivos necesarios scp docker-compose.prod.yml $SERVER_USER@$SERVER_HOST:~/padel-staging/ scp -r nginx $SERVER_USER@$SERVER_HOST:~/padel-staging/ 2>/dev/null || true # Crear archivo .env desde secret echo "${{ secrets.ENV_STAGING }}" | base64 -d | ssh $SERVER_USER@$SERVER_HOST "cat > ~/padel-staging/.env" # Deploy ssh $SERVER_USER@$SERVER_HOST << 'EOF' cd ~/padel-staging # Login a GitHub Container Registry echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin # Pull latest image docker-compose -f docker-compose.prod.yml pull app # Run pre-deploy checks echo "Running pre-deploy checks..." # Deploy docker-compose -f docker-compose.prod.yml up -d # Run migrations docker-compose -f docker-compose.prod.yml exec -T app npx prisma migrate deploy # Cleanup docker system prune -f # Health check sleep 10 curl -f http://localhost:3000/api/v1/health || exit 1 echo "Deploy to staging completed!" EOF - name: πŸ”” Notify Slack if: always() uses: 8398a7/action-slack@v3 with: status: ${{ job.status }} channel: '#deploys' webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }} env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} # =========================================================================== # Job 4: Deploy to Production # =========================================================================== deploy-production: name: πŸš€ Deploy to Production runs-on: ubuntu-latest needs: [test, build] if: github.ref == 'refs/heads/main' environment: name: production url: https://api.tudominio.com steps: - name: πŸ“₯ Checkout code uses: actions/checkout@v4 - name: ⏸️ Wait for approval uses: trstringer/manual-approval@v1 with: secret: ${{ github.TOKEN }} approvers: tech-lead,devops minimum-approvals: 1 issue-title: "Deploy to Production" issue-body: "Please approve the deployment to production" timeout-minutes: 60 continue-on-error: true - name: πŸ” Setup SSH uses: webfactory/ssh-agent@v0.8.0 with: ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} - name: πŸ’Ύ Backup Database env: SERVER_HOST: ${{ secrets.SERVER_HOST }} SERVER_USER: ${{ secrets.SERVER_USER }} run: | ssh-keyscan -H $SERVER_HOST >> ~/.ssh/known_hosts ssh $SERVER_USER@$SERVER_HOST << 'EOF' echo "Creating pre-deploy backup..." cd ~/padel-prod docker-compose -f docker-compose.prod.yml exec -T postgres pg_dump -U padeluser padeldb > backup-pre-deploy-$(date +%Y%m%d-%H%M%S).sql EOF - name: πŸš€ Deploy to Production env: SERVER_HOST: ${{ secrets.SERVER_HOST }} SERVER_USER: ${{ secrets.SERVER_USER }} run: | ssh-keyscan -H $SERVER_HOST >> ~/.ssh/known_hosts # Crear directorio de deploy si no existe ssh $SERVER_USER@$SERVER_HOST "mkdir -p ~/padel-prod" # Copiar archivos scp docker-compose.prod.yml $SERVER_USER@$SERVER_HOST:~/padel-prod/ scp -r nginx $SERVER_USER@$SERVER_HOST:~/padel-prod/ 2>/dev/null || true # Crear archivo .env echo "${{ secrets.ENV_PRODUCTION }}" | base64 -d | ssh $SERVER_USER@$SERVER_HOST "cat > ~/padel-prod/.env" # Deploy ssh $SERVER_USER@$SERVER_HOST << 'EOF' cd ~/padel-prod # Login a GitHub Container Registry echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin # Pull latest image docker-compose -f docker-compose.prod.yml pull app # Pre-deploy checks echo "Running pre-deploy checks..." # Deploy with zero-downtime (blue-green o rolling) docker-compose -f docker-compose.prod.yml up -d --no-deps --scale app=2 app sleep 30 # Health check en ambas instancias curl -f http://localhost:3000/api/v1/health || exit 1 # Scale down a 1 instancia docker-compose -f docker-compose.prod.yml up -d --no-deps --scale app=1 app # Run migrations docker-compose -f docker-compose.prod.yml exec -T app npx prisma migrate deploy # Cleanup docker system prune -f echo "Deploy to production completed!" EOF - name: βœ… Verify Deployment env: SERVER_HOST: ${{ secrets.SERVER_HOST }} run: | sleep 10 curl -f https://$SERVER_HOST/api/v1/health || exit 1 echo "Health check passed!" - name: πŸ”” Notify Slack if: always() uses: 8398a7/action-slack@v3 with: status: ${{ job.status }} channel: '#deploys' fields: repo,message,commit,author,action,eventName,ref,workflow env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} # =========================================================================== # Job 5: Release # =========================================================================== release: name: 🏷️ Create Release runs-on: ubuntu-latest needs: deploy-production if: github.ref == 'refs/heads/main' && contains(github.event.head_commit.message, '[release]') steps: - name: πŸ“₯ Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 - name: 🏷️ Create Tag id: tag run: | VERSION=$(cat package.json | grep '"version"' | head -1 | awk -F: '{ print $2 }' | sed 's/[",]//g' | tr -d '[[:space:]]') echo "version=$VERSION" >> $GITHUB_OUTPUT git tag -a "v$VERSION" -m "Release v$VERSION" git push origin "v$VERSION" - name: πŸ“ Create GitHub Release uses: softprops/action-gh-release@v1 with: tag_name: v${{ steps.tag.outputs.version }} name: Release v${{ steps.tag.outputs.version }} body: | ## Changes in this Release - ${{ github.event.head_commit.message }} ## Docker Image ``` docker pull ghcr.io/${{ github.repository }}:v${{ steps.tag.outputs.version }} ``` ## Deployment ```bash docker-compose -f docker-compose.prod.yml pull docker-compose -f docker-compose.prod.yml up -d ``` draft: false prerelease: false env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # =========================================================================== # Job 6: Cleanup # =========================================================================== cleanup: name: 🧹 Cleanup runs-on: ubuntu-latest needs: [deploy-staging, deploy-production] if: always() steps: - name: πŸ—‘οΈ Cleanup old images uses: actions/delete-package-versions@v4 with: package-name: ${{ github.event.repository.name }} package-type: 'container' min-versions-to-keep: 10 delete-only-untagged-versions: true continue-on-error: true