--- name: CI on: push: branches: [main, qa, dev] pull_request: branches: [main, qa, dev] jobs: lint-and-test: runs-on: ubuntu-latest container: image: python:3.11-bullseye services: postgres: image: postgres:15 env: POSTGRES_USER: poteuser POSTGRES_PASSWORD: ${{ secrets.DB_PASSWORD || 'testpass123' }} POSTGRES_DB: potedb_test options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - name: Install Node.js for checkout action run: | apt-get update && apt-get install -y curl git curl -fsSL https://deb.nodesource.com/setup_20.x | bash - apt-get install -y nodejs - name: Check out code uses: actions/checkout@v4 - name: Install system dependencies run: | apt-get update apt-get install -y postgresql-client - name: Install Python dependencies run: | pip install --upgrade pip pip install -e ".[dev]" - name: Run linters run: | echo "Running ruff..." ruff check src/ tests/ || true echo "Running black check..." black --check src/ tests/ || true echo "Running mypy..." mypy src/ --install-types --non-interactive || true - name: Run tests with coverage env: DATABASE_URL: postgresql://poteuser:${{ secrets.DB_PASSWORD || 'testpass123' }}@postgres:5432/potedb_test SMTP_HOST: ${{ secrets.SMTP_HOST || 'localhost' }} SMTP_PORT: 587 SMTP_USER: ${{ secrets.SMTP_USER || 'test@example.com' }} SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD || 'dummy' }} FROM_EMAIL: ${{ secrets.FROM_EMAIL || 'test@example.com' }} run: | pytest tests/ -v --cov=src/pote --cov-report=term --cov-report=xml - name: Test scripts env: DATABASE_URL: postgresql://poteuser:${{ secrets.DB_PASSWORD || 'testpass123' }}@postgres:5432/potedb_test run: | echo "Testing database migrations..." alembic upgrade head echo "Testing price loader..." python scripts/fetch_sample_prices.py || true secret-scanning: runs-on: ubuntu-latest container: image: zricethezav/gitleaks:latest steps: - name: Install Node.js for checkout action run: | apk add --no-cache nodejs npm curl git - name: Check out code uses: actions/checkout@v4 with: fetch-depth: 0 - name: Scan for secrets run: | echo "๐Ÿ” Scanning for exposed secrets..." gitleaks detect --source . --no-banner --redact --exit-code 0 || true continue-on-error: true security-scan: runs-on: ubuntu-latest container: image: python:3.11-bullseye steps: - name: Install Node.js for checkout action run: | apt-get update && apt-get install -y curl git curl -fsSL https://deb.nodesource.com/setup_20.x | bash - apt-get install -y nodejs - name: Check out code uses: actions/checkout@v4 - name: Install dependencies run: | pip install --upgrade pip pip install safety bandit - name: Run safety check run: | pip install -e . echo "๐Ÿ” Checking for known vulnerabilities in dependencies..." safety check --json || true continue-on-error: true - name: Run bandit security scan run: | echo "๐Ÿ” Running static security analysis..." bandit -r src/ -f json -o bandit-report.json || true bandit -r src/ -f screen continue-on-error: true dependency-scan: runs-on: ubuntu-latest container: image: aquasec/trivy:latest steps: - name: Install Node.js for checkout action run: | apk add --no-cache nodejs npm curl git - name: Check out code uses: actions/checkout@v4 - name: Scan dependencies run: | echo "๐Ÿ” Scanning dependencies for vulnerabilities..." trivy fs --scanners vuln --exit-code 0 . sast-scan: runs-on: ubuntu-latest container: image: ubuntu:22.04 steps: - name: Install Node.js for checkout action run: | apt-get update && apt-get install -y curl git curl -fsSL https://deb.nodesource.com/setup_20.x | bash - apt-get install -y nodejs - name: Check out code uses: actions/checkout@v4 - name: Install Semgrep run: | apt-get update && apt-get install -y python3 python3-pip pip3 install semgrep - name: Run Semgrep scan run: | echo "๐Ÿ” Running SAST analysis with Semgrep..." semgrep --config=auto --error || true continue-on-error: true container-scan: runs-on: ubuntu-latest container: image: ubuntu:22.04 steps: - name: Install Node.js for checkout action run: | apt-get update && apt-get install -y curl git curl -fsSL https://deb.nodesource.com/setup_20.x | bash - apt-get install -y nodejs - name: Check out code uses: actions/checkout@v4 - name: Install Trivy run: | set -e apt-get update && apt-get install -y wget curl tar # Use a fixed, known-good Trivy version TRIVY_VERSION="0.58.2" TRIVY_URL="https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz" echo "Installing Trivy version: ${TRIVY_VERSION}" if ! wget --progress=bar:force "${TRIVY_URL}" -O /tmp/trivy.tar.gz 2>&1; then echo "โŒ Failed to download Trivy" exit 1 fi if [ ! -f /tmp/trivy.tar.gz ] || [ ! -s /tmp/trivy.tar.gz ]; then echo "โŒ Downloaded Trivy archive is missing or empty" exit 1 fi echo "Extracting Trivy..." if ! tar -xzf /tmp/trivy.tar.gz -C /tmp/ trivy; then echo "โŒ Failed to extract Trivy" exit 1 fi mv /tmp/trivy /usr/local/bin/trivy chmod +x /usr/local/bin/trivy trivy --version - name: Scan Dockerfile run: | if [ -f "Dockerfile" ]; then echo "๐Ÿ” Scanning Dockerfile for vulnerabilities..." trivy config Dockerfile || true else echo "No Dockerfile found, skipping scan" fi continue-on-error: true - name: Scan filesystem run: | echo "๐Ÿ” Scanning filesystem for vulnerabilities..." trivy fs --scanners vuln --severity HIGH,CRITICAL --format table . || true continue-on-error: true sonar-analysis: if: github.event_name == 'pull_request' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/qa' runs-on: ubuntu-latest container: image: ubuntu:22.04 env: SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} steps: - name: Install Node.js for checkout action run: | export DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -y curl curl -fsSL https://deb.nodesource.com/setup_20.x | bash - apt-get install -y nodejs - name: Check out code uses: actions/checkout@v4 - name: Install Java and SonarScanner run: | set -e export DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -y wget curl unzip openjdk-21-jre # Use a known working version to avoid download issues SONAR_SCANNER_VERSION="5.0.1.3006" SCANNER_URL="https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-${SONAR_SCANNER_VERSION}-linux.zip" echo "Installing SonarScanner version: ${SONAR_SCANNER_VERSION}" echo "Downloading from: ${SCANNER_URL}" # Download with verbose error output if ! wget --progress=bar:force "${SCANNER_URL}" -O /tmp/sonar-scanner.zip 2>&1; then echo "โŒ Failed to download SonarScanner" echo "Checking if file was partially downloaded:" ls -lh /tmp/sonar-scanner.zip 2>/dev/null || echo "No file found" exit 1 fi # Verify download if [ ! -f /tmp/sonar-scanner.zip ] || [ ! -s /tmp/sonar-scanner.zip ]; then echo "โŒ Downloaded file is missing or empty" exit 1 fi echo "Download complete. File size: $(du -h /tmp/sonar-scanner.zip | cut -f1)" echo "Extracting SonarScanner..." if ! unzip -q /tmp/sonar-scanner.zip -d /tmp; then echo "โŒ Failed to extract SonarScanner" echo "Archive info:" file /tmp/sonar-scanner.zip || true unzip -l /tmp/sonar-scanner.zip 2>&1 | head -20 || true exit 1 fi # Find the extracted directory (handle both naming conventions) EXTRACTED_DIR="" if [ -d "/tmp/sonar-scanner-${SONAR_SCANNER_VERSION}-linux" ]; then EXTRACTED_DIR="/tmp/sonar-scanner-${SONAR_SCANNER_VERSION}-linux" elif [ -d "/tmp/sonar-scanner-cli-${SONAR_SCANNER_VERSION}-linux" ]; then EXTRACTED_DIR="/tmp/sonar-scanner-cli-${SONAR_SCANNER_VERSION}-linux" else # Try to find any sonar-scanner directory EXTRACTED_DIR=$(find /tmp -maxdepth 1 -type d -name "*sonar-scanner*" | head -1) fi if [ -z "$EXTRACTED_DIR" ] || [ ! -d "$EXTRACTED_DIR" ]; then echo "โŒ SonarScanner directory not found after extraction" echo "Contents of /tmp:" ls -la /tmp/ | grep -E "(sonar|zip)" || ls -la /tmp/ | head -20 exit 1 fi echo "Found extracted directory: ${EXTRACTED_DIR}" mv "${EXTRACTED_DIR}" /opt/sonar-scanner # Create symlink if [ -f /opt/sonar-scanner/bin/sonar-scanner ]; then ln -sf /opt/sonar-scanner/bin/sonar-scanner /usr/local/bin/sonar-scanner chmod +x /opt/sonar-scanner/bin/sonar-scanner chmod +x /usr/local/bin/sonar-scanner else echo "โŒ sonar-scanner binary not found in /opt/sonar-scanner/bin/" echo "Contents of /opt/sonar-scanner/bin/:" ls -la /opt/sonar-scanner/bin/ || true exit 1 fi echo "Verifying installation..." if ! sonar-scanner --version; then echo "โŒ SonarScanner verification failed" echo "PATH: $PATH" which sonar-scanner || echo "sonar-scanner not in PATH" exit 1 fi echo "โœ“ SonarScanner installed successfully" - name: Verify SonarQube connection run: | echo "Checking SonarQube connectivity..." if [ -z "$SONAR_HOST_URL" ] || [ -z "$SONAR_TOKEN" ]; then echo "โš ๏ธ Skipping SonarQube analysis: SONAR_HOST_URL or SONAR_TOKEN secrets are not set." exit 0 fi echo "โœ“ Secrets are configured" echo "SonarQube URL: ${SONAR_HOST_URL}" echo "Testing connectivity to SonarQube server..." if curl -f -s -o /dev/null -w "%{http_code}" "${SONAR_HOST_URL}/api/system/status" | grep -q "200"; then echo "โœ“ SonarQube server is reachable" else echo "โš ๏ธ Warning: Could not verify SonarQube server connectivity (continuing anyway)" fi - name: Install Python 3.11 and dependencies run: | export DEBIAN_FRONTEND=noninteractive export TZ=UTC ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone apt-get update apt-get install -y software-properties-common add-apt-repository -y ppa:deadsnakes/ppa apt-get update apt-get install -y python3.11 python3.11-dev python3.11-venv python3.11-distutils # Install pip for Python 3.11 curl -sS https://bootstrap.pypa.io/get-pip.py | python3.11 # Ensure python3 and pip3 point to 3.11 update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 1 || true # Use python3.11 -m pip for pip operations python3.11 -m pip install --upgrade pip python3 --version python3.11 -m pip --version - name: Generate coverage report run: | echo "Generating coverage report for SonarQube..." python3.11 -m pip install -e ".[dev]" python3.11 -m pytest tests/ --cov=src/pote --cov-report=xml --cov-report=term || true - name: Run SonarScanner run: | echo "Starting SonarQube analysis..." if [ -z "$SONAR_HOST_URL" ] || [ -z "$SONAR_TOKEN" ]; then echo "Skipping SonarQube analysis: secrets not set." exit 0 fi if ! sonar-scanner \ -Dsonar.projectKey=pote \ -Dsonar.sources=src \ -Dsonar.tests=tests \ -Dsonar.python.coverage.reportPaths=coverage.xml \ -Dsonar.host.url=${SONAR_HOST_URL} \ -Dsonar.token=${SONAR_TOKEN} \ -Dsonar.scm.disabled=true \ -Dsonar.python.version=3.11 \ -X; then echo "" echo "โŒ SonarScanner analysis failed!" echo "" echo "Common issues:" echo " 1. Project 'pote' doesn't exist in SonarQube" echo " โ†’ Create it manually in SonarQube UI" echo " 2. Token doesn't have permission to analyze/create project" echo " โ†’ Ensure token has 'Execute Analysis' permission" echo " 3. Token doesn't have 'Create Projects' permission (if project doesn't exist)" echo " โ†’ Grant this permission in SonarQube user settings" echo "" echo "Check SonarQube logs for more details." # Do not fail CI on Sonar auth/project setup issues. exit 0 fi continue-on-error: true docker-build-test: runs-on: ubuntu-latest steps: - name: Check out code uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build Docker image uses: docker/build-push-action@v5 with: context: . push: false load: true tags: pote:test cache-from: type=gha cache-to: type=gha,mode=max - name: Test Docker image run: | docker run --rm pote:test python -c "import pote; print('โœ… POTE import successful')" workflow-summary: runs-on: ubuntu-latest needs: [lint-and-test, secret-scanning, security-scan, dependency-scan, sast-scan, container-scan, sonar-analysis, docker-build-test] if: always() steps: - name: Generate workflow summary run: | echo "## ๐Ÿ” CI Workflow Summary" >> $GITHUB_STEP_SUMMARY || true echo "" >> $GITHUB_STEP_SUMMARY || true echo "### Job Results" >> $GITHUB_STEP_SUMMARY || true echo "" >> $GITHUB_STEP_SUMMARY || true echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY || true echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY || true echo "| ๐Ÿงช Lint & Test | ${{ needs.lint-and-test.result }} |" >> $GITHUB_STEP_SUMMARY || true echo "| ๐Ÿ” Secret Scanning | ${{ needs.secret-scanning.result }} |" >> $GITHUB_STEP_SUMMARY || true echo "| ๐Ÿ”’ Security Scan | ${{ needs.security-scan.result }} |" >> $GITHUB_STEP_SUMMARY || true echo "| ๐Ÿ“ฆ Dependency Scan | ${{ needs.dependency-scan.result }} |" >> $GITHUB_STEP_SUMMARY || true echo "| ๐Ÿ” SAST Scan | ${{ needs.sast-scan.result }} |" >> $GITHUB_STEP_SUMMARY || true echo "| ๐Ÿณ Container Scan | ${{ needs.container-scan.result }} |" >> $GITHUB_STEP_SUMMARY || true echo "| ๐Ÿ” SonarQube Analysis | ${{ needs.sonar-analysis.result }} |" >> $GITHUB_STEP_SUMMARY || true echo "| ๐Ÿ‹ Docker Build | ${{ needs.docker-build-test.result }} |" >> $GITHUB_STEP_SUMMARY || true echo "" >> $GITHUB_STEP_SUMMARY || true echo "### ๐Ÿ“Š Summary" >> $GITHUB_STEP_SUMMARY || true echo "" >> $GITHUB_STEP_SUMMARY || true echo "All security and validation checks have completed." >> $GITHUB_STEP_SUMMARY || true echo "" >> $GITHUB_STEP_SUMMARY || true echo "**Security Layers:**" >> $GITHUB_STEP_SUMMARY || true echo "- โœ… Secret scanning (Gitleaks)" >> $GITHUB_STEP_SUMMARY || true echo "- โœ… Dependency vulnerabilities (Safety + Trivy)" >> $GITHUB_STEP_SUMMARY || true echo "- โœ… Static security analysis (Bandit)" >> $GITHUB_STEP_SUMMARY || true echo "- โœ… SAST scanning (Semgrep)" >> $GITHUB_STEP_SUMMARY || true echo "- โœ… Container scanning (Trivy)" >> $GITHUB_STEP_SUMMARY || true echo "- โœ… Code quality analysis (SonarQube)" >> $GITHUB_STEP_SUMMARY || true continue-on-error: true