name: CI / CD run-name: "${{ github.event_name == 'pull_request' && format('PR #{0} — {1}', github.event.pull_request.number, github.event.pull_request.title) || format('merge to {0} by @{1}', github.ref_name, github.actor) }}" on: push: branches: [main] pull_request: jobs: # ── Unit Tests ───────────────────────────────────────────────────────────── unit-tests: name: Unit Tests runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Set up .NET uses: actions/setup-dotnet@v4 with: dotnet-version: '10.0.x' - name: Run unit tests run: dotnet test src/backend/tests/unit/Randall.Domain.UnitTests.csproj --logger "console;verbosity=normal" # ── E2E Tests ────────────────────────────────────────────────────────────── e2e: name: E2E Tests runs-on: ubuntu-latest needs: unit-tests steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build images run: docker compose -f cicd/docker/docker-compose.yml build - name: Start services run: docker compose -f cicd/docker/docker-compose.yml up -d env: JWT_KEY: ${{ secrets.JWT_KEY }} - name: Wait for frontend to be ready run: | echo "Waiting for services to become healthy..." timeout 120 bash -c \ 'until curl -sf http://localhost > /dev/null; do echo " still waiting..."; sleep 3; done' echo "Services are ready." - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: '22' cache: 'npm' cache-dependency-path: tests/e2e/package-lock.json - name: Install dependencies working-directory: tests/e2e run: npm ci - name: Install Playwright browsers working-directory: tests/e2e run: npx playwright install --with-deps chromium - name: Run E2E tests working-directory: tests/e2e env: BASE_URL: http://localhost CI: 'true' run: npx playwright test - name: Upload Playwright report uses: actions/upload-artifact@v4 if: always() with: name: playwright-report path: tests/e2e/playwright-report/ retention-days: 30 - name: Dump Docker logs on failure if: failure() run: docker compose -f cicd/docker/docker-compose.yml logs - name: Stop services if: always() run: docker compose -f cicd/docker/docker-compose.yml down -v # ── Deploy ───────────────────────────────────────────────────────────────── deploy: name: Deploy runs-on: ubuntu-latest needs: e2e # Only deploy on merges to main, not on pull requests if: github.ref == 'refs/heads/main' && github.event_name == 'push' steps: - name: Checkout uses: actions/checkout@v4 - name: Configure SSH run: | mkdir -p ~/.ssh echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/deploy_key chmod 600 ~/.ssh/deploy_key ssh-keyscan -H "${{ secrets.DEPLOY_HOST }}" >> ~/.ssh/known_hosts - name: Sync source to VM run: | rsync -az --delete \ -e "ssh -i ~/.ssh/deploy_key" \ --exclude='.git/' \ --exclude='.github/' \ --exclude='tests/' \ --exclude='src/frontend/node_modules/' \ --exclude='src/frontend/dist/' \ --exclude='src/backend/**/bin/' \ --exclude='src/backend/**/obj/' \ --exclude='src/backend/**/*.db' \ --exclude='src/backend/**/*.db-shm' \ --exclude='src/backend/**/*.db-wal' \ ./ ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:${{ secrets.DEPLOY_PATH }}/ - name: Deploy with Docker Compose uses: appleboy/ssh-action@v1.0.3 env: JWT_KEY: ${{ secrets.JWT_KEY }} PORT: ${{ secrets.DEPLOY_PORT || '80' }} with: host: ${{ secrets.DEPLOY_HOST }} username: ${{ secrets.DEPLOY_USER }} key: ${{ secrets.DEPLOY_SSH_KEY }} envs: JWT_KEY,PORT script: | set -e cd "${{ secrets.DEPLOY_PATH }}" echo "Building backend..." DOCKER_BUILDKIT=0 docker compose -f cicd/docker/docker-compose.yml build backend echo "Building frontend..." DOCKER_BUILDKIT=0 docker compose -f cicd/docker/docker-compose.yml build frontend echo "Starting services..." docker compose -f cicd/docker/docker-compose.yml up -d echo "Removing unused images..." docker image prune -f echo "Running containers:" docker compose -f cicd/docker/docker-compose.yml ps