name: Basil CI/CD Pipeline on: push: branches: - main - develop tags: - 'v*' pull_request: branches: - main - develop env: NODE_VERSION: '20' HARBOR_REGISTRY: harbor.pkartchner.com HARBOR_PROJECT: basil jobs: # ============================================================================ # STAGE 1: PARALLEL QUALITY CHECKS # ============================================================================ lint: name: Code Linting runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} - name: Install dependencies run: npm ci - name: Run linter run: npm run lint || true continue-on-error: true test-api: name: API Tests runs-on: ubuntu-latest services: postgres: image: postgres:16-alpine env: POSTGRES_USER: basil POSTGRES_PASSWORD: basil POSTGRES_DB: basil_test options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} - name: Install dependencies run: npm ci - name: Build shared package run: cd packages/shared && npm run build - name: Generate Prisma Client run: cd packages/api && npm run prisma:generate - name: Run database migrations run: cd packages/api && npm run prisma:migrate env: DATABASE_URL: postgresql://basil:basil@postgres:5432/basil_test?schema=public - name: Run API tests run: cd packages/api && npm run test env: DATABASE_URL: postgresql://basil:basil@postgres:5432/basil_test?schema=public NODE_ENV: test - name: Upload coverage if: always() uses: actions/upload-artifact@v3 with: name: api-coverage path: packages/api/coverage/ retention-days: 14 test-web: name: Web Tests runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} - name: Install dependencies run: npm ci - name: Build shared package run: cd packages/shared && npm run build - name: Run Web tests run: cd packages/web && npm run test - name: Upload coverage if: always() uses: actions/upload-artifact@v3 with: name: web-coverage path: packages/web/coverage/ retention-days: 14 test-shared: name: Shared Package Tests runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} - name: Install dependencies run: npm ci - name: Run Shared tests run: cd packages/shared && npm run test - name: Upload coverage if: always() uses: actions/upload-artifact@v3 with: name: shared-coverage path: packages/shared/coverage/ retention-days: 14 security-scan: name: Security Scanning runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} - name: Install dependencies run: npm ci - name: NPM Audit run: | echo "Running npm audit..." npm audit --audit-level=high || true cd packages/api && npm audit --audit-level=high || true cd ../web && npm audit --audit-level=high || true continue-on-error: true - name: Secret Scanning run: | echo "Scanning for hardcoded secrets..." if grep -r -E "(password|secret|api[_-]?key|token)\s*=\s*['\"][^'\"]+['\"]" \ --include="*.ts" --include="*.js" \ --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=e2e \ --exclude="*.test.ts" --exclude="*.spec.ts" .; then echo "⚠️ Potential hardcoded secrets found in non-test files!" exit 1 fi echo "✓ No hardcoded secrets detected" - name: Check outdated dependencies run: | echo "Checking for outdated dependencies..." npm outdated || true continue-on-error: true # ============================================================================ # STAGE 2: BUILD VERIFICATION # ============================================================================ build: name: Build All Packages runs-on: ubuntu-latest needs: [lint, test-api, test-web, test-shared, security-scan] steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} - name: Install dependencies run: npm ci - name: Build all packages run: npm run build - name: Upload build artifacts uses: actions/upload-artifact@v3 with: name: build-artifacts path: | packages/api/dist/ packages/web/dist/ packages/shared/dist/ retention-days: 7 # ============================================================================ # STAGE 3: E2E TESTING # ============================================================================ e2e-tests: name: E2E Tests runs-on: ubuntu-latest needs: build timeout-minutes: 30 if: false # Temporarily disabled - E2E tests need fixing services: postgres: image: postgres:16-alpine env: POSTGRES_USER: basil POSTGRES_PASSWORD: basil POSTGRES_DB: basil options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} - name: Install dependencies run: npm ci - name: Install Playwright browsers run: npx playwright install --with-deps - name: Build application run: npm run build - name: Run database migrations run: cd packages/api && npm run prisma:migrate env: DATABASE_URL: postgresql://basil:basil@postgres:5432/basil?schema=public - name: Run E2E tests run: npm run test:e2e env: DATABASE_URL: postgresql://basil:basil@postgres:5432/basil?schema=public - name: Upload test results if: always() uses: actions/upload-artifact@v3 with: name: playwright-report path: playwright-report/ retention-days: 14 # ============================================================================ # STAGE 4: DOCKER BUILD & DEPLOYMENT (main branch only) # ============================================================================ docker-build-and-push: name: Build & Push Docker Images runs-on: ubuntu-latest needs: build # Changed from e2e-tests since E2E is temporarily disabled if: github.event_name == 'push' && github.ref == 'refs/heads/main' outputs: image_tag: ${{ steps.meta.outputs.tag }} steps: - name: Checkout code uses: actions/checkout@v4 - name: Install Docker CLI run: | mkdir -p /tmp/docker-cli curl -fsSL "https://download.docker.com/linux/static/stable/x86_64/docker-27.4.1.tgz" | tar -xz -C /tmp/docker-cli --strip-components=1 export PATH="/tmp/docker-cli:$PATH" echo "/tmp/docker-cli" >> $GITHUB_PATH docker --version - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to Harbor uses: docker/login-action@v3 with: registry: ${{ env.HARBOR_REGISTRY }} username: ${{ secrets.HARBOR_USERNAME }} password: ${{ secrets.HARBOR_PASSWORD }} - name: Extract metadata id: meta run: | SHA_SHORT=$(echo $GITHUB_SHA | cut -c1-7) echo "tag=main-${SHA_SHORT}" >> $GITHUB_OUTPUT echo "date=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT - name: Build and push API image uses: docker/build-push-action@v5 with: context: . file: ./packages/api/Dockerfile push: true tags: | ${{ env.HARBOR_REGISTRY }}/${{ env.HARBOR_PROJECT }}/basil-api:latest ${{ env.HARBOR_REGISTRY }}/${{ env.HARBOR_PROJECT }}/basil-api:${{ steps.meta.outputs.tag }} labels: | org.opencontainers.image.created=${{ steps.meta.outputs.date }} org.opencontainers.image.revision=${{ github.sha }} org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} - name: Build and push Web image uses: docker/build-push-action@v5 with: context: . file: ./packages/web/Dockerfile push: true tags: | ${{ env.HARBOR_REGISTRY }}/${{ env.HARBOR_PROJECT }}/basil-web:latest ${{ env.HARBOR_REGISTRY }}/${{ env.HARBOR_PROJECT }}/basil-web:${{ steps.meta.outputs.tag }} labels: | org.opencontainers.image.created=${{ steps.meta.outputs.date }} org.opencontainers.image.revision=${{ github.sha }} org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} - name: Scan API image for vulnerabilities run: | docker run --rm \ -v /var/run/docker.sock:/var/run/docker.sock \ aquasec/trivy:latest image \ --exit-code 0 \ --severity HIGH,CRITICAL \ --no-progress \ ${{ env.HARBOR_REGISTRY }}/${{ env.HARBOR_PROJECT }}/basil-api:latest || true - name: Scan Web image for vulnerabilities run: | docker run --rm \ -v /var/run/docker.sock:/var/run/docker.sock \ aquasec/trivy:latest image \ --exit-code 0 \ --severity HIGH,CRITICAL \ --no-progress \ ${{ env.HARBOR_REGISTRY }}/${{ env.HARBOR_PROJECT }}/basil-web:latest || true - name: Image build summary run: | echo "### Docker Images Built & Pushed 🐳" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**API Image:**" >> $GITHUB_STEP_SUMMARY echo "- \`${{ env.HARBOR_REGISTRY }}/${{ env.HARBOR_PROJECT }}/basil-api:latest\`" >> $GITHUB_STEP_SUMMARY echo "- \`${{ env.HARBOR_REGISTRY }}/${{ env.HARBOR_PROJECT }}/basil-api:${{ steps.meta.outputs.tag }}\`" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Web Image:**" >> $GITHUB_STEP_SUMMARY echo "- \`${{ env.HARBOR_REGISTRY }}/${{ env.HARBOR_PROJECT }}/basil-web:latest\`" >> $GITHUB_STEP_SUMMARY echo "- \`${{ env.HARBOR_REGISTRY }}/${{ env.HARBOR_PROJECT }}/basil-web:${{ steps.meta.outputs.tag }}\`" >> $GITHUB_STEP_SUMMARY trigger-deployment: name: Trigger Deployment runs-on: ubuntu-latest needs: docker-build-and-push if: success() steps: - name: Trigger webhook run: | # Install required packages apt-get update -qq && apt-get install -y -qq jq iproute2 > /dev/null 2>&1 # Get Docker host IP (gateway of the container network) HOST_IP=$(ip route | grep default | awk '{print $3}') # Fallback to common Docker gateway IPs if not found if [ -z "$HOST_IP" ]; then HOST_IP="172.17.0.1" fi echo "Using host IP: $HOST_IP" # Construct JSON payload with jq to properly escape multiline commit messages PAYLOAD=$(jq -n \ --arg branch "main" \ --arg commit "${{ github.sha }}" \ --arg message "${{ github.event.head_commit.message }}" \ --arg tag "${{ needs.docker-build-and-push.outputs.image_tag }}" \ '{branch: $branch, commit: $commit, message: $message, tag: $tag}') # Trigger webhook on Docker host curl -X POST "http://${HOST_IP}:9000/hooks/basil-deploy" \ -H "Content-Type: application/json" \ -H "X-Webhook-Secret: ${{ secrets.WEBHOOK_SECRET }}" \ -d "$PAYLOAD" || echo "Webhook call failed, but continuing..." - name: Deployment triggered run: | echo "### Deployment Triggered 🚀" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "The deployment webhook has been called." >> $GITHUB_STEP_SUMMARY echo "Check the server logs to verify deployment status:" >> $GITHUB_STEP_SUMMARY echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY echo "tail -f /srv/docker-compose/basil/deploy.log" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Application URL:** https://basil.pkartchner.com" >> $GITHUB_STEP_SUMMARY