Merge pull request #21 from dordormin/fix/cors-204-nocontent #65
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: TradeOffStack CI/CD Pipeline | |
| on: | |
| push: | |
| branches: [ "main", "feature/postman-tests" ] | |
| pull_request: | |
| branches: [ "main", "feature/postman-tests" ] | |
| jobs: | |
| build-and-test: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - name: π₯ Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: βοΈ Setup .NET 10 | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: '10.0.x' | |
| - name: π¦ Restore backend dependencies | |
| run: dotnet restore TradeOffStackAPI.sln | |
| - name: π¨ Build API | |
| run: dotnet build TradeOffStackAPI.sln --no-restore --configuration Release | |
| - name: π§ͺ Run C# Backend Tests | |
| run: dotnet test TradeOffStackAPI.sln --no-build --configuration Release --verbosity normal | |
| - name: π Start Services (API & Database) via Docker Compose | |
| run: | | |
| echo "POSTGRES_USER=tradeoff_admin" >> .env | |
| echo "POSTGRES_PASSWORD=Tr@de0ff_Secure!2026_Db#X9" >> .env | |
| echo "POSTGRES_DB=tradeoffstack" >> .env | |
| echo "POSTGRES_PORT=5432" >> .env | |
| echo "JWT_SECRET_KEY=Tr@de0ff_Super_Secret_Key_For_JWT_Auth_2026!" >> .env | |
| docker compose up -d --build | |
| - name: π©Ί Wait for API to be healthy | |
| run: | | |
| echo "Waiting for API to respond at http://localhost:5000/api/equipment..." | |
| for i in {1..30}; do | |
| if curl -s http://localhost:5000/api/equipment > /dev/null; then | |
| echo "API is up and running!" | |
| exit 0 | |
| fi | |
| sleep 2 | |
| done | |
| echo "API failed to start in time." | |
| docker compose logs | |
| exit 1 | |
| - name: π’ Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: 'npm' | |
| cache-dependency-path: frontend/package-lock.json | |
| - name: π¦ Install Frontend Dependencies | |
| run: | | |
| cd frontend | |
| npm ci | |
| - name: π Install Playwright Browsers | |
| run: | | |
| cd frontend | |
| npx playwright install --with-deps chromium | |
| - name: β‘ Start Frontend Vite Server | |
| run: | | |
| cd frontend | |
| npm run dev & | |
| echo "Waiting for Vite frontend to be responsive..." | |
| for i in {1..15}; do | |
| if curl -s http://localhost:5173 > /dev/null; then | |
| echo "Vite is up!" | |
| exit 0 | |
| fi | |
| sleep 1 | |
| done | |
| - name: π§ͺ Run Playwright E2E UI Tests | |
| run: | | |
| cd frontend | |
| node ui_test.js | |
| - name: π§ͺ Run Playwright E2E Global CRUD Lifecycle Tests | |
| run: | | |
| cd frontend | |
| node ui_global_test.js | |
| - name: π€ Upload E2E Screenshots | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: playwright-screenshots | |
| path: frontend/screenshots/ | |
| # ========================================== | |
| # DOCKER PUBLISH TO GHCR (Only on push to main or feature/postman-tests) | |
| # ========================================== | |
| - name: π Log in to GitHub Container Registry | |
| if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/feature/postman-tests') | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: π³ Build and Push Docker Images to GHCR | |
| if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/feature/postman-tests') | |
| run: | | |
| OWNER_LC=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') | |
| echo "Building and Pushing API Image..." | |
| docker build -t ghcr.io/$OWNER_LC/tradeoffstack-api:latest -f Dockerfile . | |
| docker push ghcr.io/$OWNER_LC/tradeoffstack-api:latest | |
| echo "Building and Pushing Frontend Image..." | |
| docker build -t ghcr.io/$OWNER_LC/tradeoffstack-frontend:latest -f frontend/Dockerfile frontend/ | |
| docker push ghcr.io/$OWNER_LC/tradeoffstack-frontend:latest | |
| # ========================================== | |
| # π CONTINUOUS DEPLOYMENT (CD) TO HOSTINGER VPS | |
| # ========================================== | |
| - name: π Diagnose VPS Connectivity | |
| if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/feature/postman-tests') | |
| run: | | |
| echo "Testing connectivity to VPS..." | |
| echo "Target: ${{ secrets.VPS_IP }}:22" | |
| timeout 15 bash -c "cat < /dev/null > /dev/tcp/${{ secrets.VPS_IP }}/22" 2>&1 && echo "β Port 22 is OPEN" || echo "β Port 22 is UNREACHABLE" | |
| - name: π Deploy to VPS (All-in-One SSH) | |
| if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/feature/postman-tests') | |
| uses: appleboy/ssh-action@v1.0.3 | |
| with: | |
| host: ${{ secrets.VPS_IP }} | |
| username: ${{ secrets.VPS_USER }} | |
| key: ${{ secrets.SSH_PRIVATE_KEY }} | |
| timeout: 60s | |
| command_timeout: 10m | |
| script: | | |
| # Create deployment directory | |
| mkdir -p /app/tradeoffstack | |
| cd /app/tradeoffstack | |
| # Write Caddyfile directly (no SCP needed) | |
| cat > Caddyfile << 'CADDY_EOF' | |
| tradeoffstack.dordorapps.tech { | |
| handle /api/* { | |
| reverse_proxy api:8080 | |
| } | |
| handle { | |
| reverse_proxy frontend:80 | |
| } | |
| encode gzip zstd | |
| } | |
| CADDY_EOF | |
| # Write docker-compose.prod.yml directly (no SCP needed) | |
| cat > docker-compose.prod.yml << 'COMPOSE_EOF' | |
| version: '3.8' | |
| services: | |
| db: | |
| image: postgres:17-alpine | |
| container_name: tradeoffstack-db-prod | |
| environment: | |
| POSTGRES_USER: ${POSTGRES_USER:-tradeoff_admin} | |
| POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} | |
| POSTGRES_DB: tradeoffstack | |
| volumes: | |
| - pg_prod_data:/var/lib/postgresql/data | |
| restart: always | |
| healthcheck: | |
| test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-tradeoff_admin} -d tradeoffstack"] | |
| interval: 5s | |
| timeout: 5s | |
| retries: 5 | |
| api: | |
| image: ghcr.io/dordormin/tradeoffstack-api:latest | |
| container_name: tradeoffstack-api-prod | |
| depends_on: | |
| db: | |
| condition: service_healthy | |
| environment: | |
| ConnectionStrings__DefaultConnection: Host=db;Database=tradeoffstack;Username=${POSTGRES_USER:-tradeoff_admin};Password=${POSTGRES_PASSWORD} | |
| JWT_SECRET_KEY: ${JWT_SECRET_KEY} | |
| ASPNETCORE_ENVIRONMENT: Production | |
| restart: always | |
| frontend: | |
| image: ghcr.io/dordormin/tradeoffstack-frontend:latest | |
| container_name: tradeoffstack-frontend-prod | |
| restart: always | |
| caddy: | |
| image: caddy:2-alpine | |
| container_name: tradeoffstack-proxy-prod | |
| ports: | |
| - "80:80" | |
| - "443:443" | |
| volumes: | |
| - ./Caddyfile:/etc/caddy/Caddyfile | |
| - caddy_data:/data | |
| - caddy_config:/config | |
| depends_on: | |
| - api | |
| - frontend | |
| restart: always | |
| volumes: | |
| pg_prod_data: | |
| caddy_data: | |
| caddy_config: | |
| COMPOSE_EOF | |
| # Inject production environment variables | |
| cat > .env << ENV_EOF | |
| POSTGRES_USER=tradeoff_admin | |
| POSTGRES_PASSWORD=${{ secrets.DB_PASSWORD }} | |
| JWT_SECRET_KEY=${{ secrets.JWT_SECRET_KEY }} | |
| ENV_EOF | |
| # Login to GHCR to pull private images | |
| echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.repository_owner }} --password-stdin | |
| # Pull latest images and update containers | |
| docker compose -f docker-compose.prod.yml pull | |
| docker compose -f docker-compose.prod.yml up -d --remove-orphans | |
| # Cleanup unused images to keep disk space free | |
| docker image prune -f | |