diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml new file mode 100644 index 0000000..7b7aaec --- /dev/null +++ b/.github/workflows/ci-cd.yml @@ -0,0 +1,207 @@ +name: CI/CD Pipeline + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main, develop ] + release: + types: [ published ] + +env: + PYTHON_VERSION: '3.11' + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + lint: + name: Lint Code + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: 'pip' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 black isort mypy + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + + - name: Run Black (code formatting check) + run: black --check --diff . + continue-on-error: true + + - name: Run isort (import sorting check) + run: isort --check-only --diff . + continue-on-error: true + + - name: Run Flake8 (linting) + run: | + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + + - name: Run MyPy (type checking) + run: mypy --install-types --non-interactive --ignore-missing-imports . + continue-on-error: true + + test: + name: Run Tests + runs-on: ubuntu-latest + needs: lint + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: 'pip' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pytest pytest-cov pytest-asyncio httpx + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + + - name: Run tests with coverage + run: | + pytest tests/ -v --cov=. --cov-report=xml --cov-report=html --cov-report=term + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + file: ./coverage.xml + flags: unittests + name: codecov-umbrella + fail_ci_if_error: false + + - name: Archive code coverage results + uses: actions/upload-artifact@v4 + with: + name: code-coverage-report + path: htmlcov/ + + security: + name: Security Scan + runs-on: ubuntu-latest + needs: lint + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install safety bandit + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + + - name: Run Safety (dependency vulnerability check) + run: | + safety check --json || true + + - name: Run Bandit (security linter) + run: | + bandit -r . -f json -o bandit-report.json || true + bandit -r . -f screen + continue-on-error: true + + - name: Upload Bandit report + uses: actions/upload-artifact@v4 + with: + name: bandit-security-report + path: bandit-report.json + + build-docker: + name: Build Docker Image + runs-on: ubuntu-latest + needs: [test, security] + if: github.event_name == 'push' || github.event_name == 'release' + + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to 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: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + deploy-staging: + name: Deploy to Staging + runs-on: ubuntu-latest + needs: build-docker + if: github.event_name == 'push' && github.ref == 'refs/heads/develop' + environment: + name: staging + url: https://staging.geo-analytics-api.example.com + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Deploy to staging + run: | + echo "Deploying to staging environment..." + # Add your deployment commands here + # Example: kubectl set image deployment/geo-analytics-api app=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:develop + + deploy-production: + name: Deploy to Production + runs-on: ubuntu-latest + needs: build-docker + if: github.event_name == 'release' + environment: + name: production + url: https://geo-analytics-api.example.com + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Deploy to production + run: | + echo "Deploying to production environment..." + # Add your deployment commands here + # Example: kubectl set image deployment/geo-analytics-api app=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.release.tag_name }}