diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d64f6bf --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,305 @@ +name: CI + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + schedule: + # Weekly security scan on Mondays at 00:00 UTC + - cron: '0 0 * * 1' + +env: + K8S_STATE_REPO: christianlouis/k8s-cluster-state + +jobs: + # ── Stage 1: Lint (gates everything else) ──────────────────────────────── + lint: + name: Lint + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Cache pip packages + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('backend/requirements.txt') }} + restore-keys: ${{ runner.os }}-pip- + + - name: Install linting tools + run: | + python -m pip install --upgrade pip + pip install black isort flake8 pylint + cd backend && pip install -r requirements.txt + + - name: Black – format check + run: black --check backend/app + + - name: isort – import order check + run: isort --check-only backend/app + + - name: Flake8 + run: flake8 backend/app + + - name: Pylint + run: pylint backend/app + continue-on-error: true + + # ── Stage 2 (parallel): Test, Security, CodeQL, Dependency Review ──────── + test: + name: Test + runs-on: ubuntu-latest + needs: lint + permissions: + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Cache pip packages + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('backend/requirements.txt') }} + restore-keys: ${{ runner.os }}-pip- + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + cd backend && pip install -r requirements.txt + + - name: Run tests with coverage + run: | + cd backend + pytest --cov=app --cov-report=xml --cov-report=term-missing + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + file: ./backend/coverage.xml + flags: unittests + name: codecov-umbrella + fail_ci_if_error: false + + security: + name: Security Scan + runs-on: ubuntu-latest + needs: lint + permissions: + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Cache pip packages + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('backend/requirements.txt') }} + restore-keys: ${{ runner.os }}-pip- + + - name: Install security tools + run: | + python -m pip install --upgrade pip + pip install bandit pip-audit + cd backend && pip install -r requirements.txt + + - name: Bandit – Python security linter + run: bandit -r backend/app -f json -o bandit-report.json + continue-on-error: true + + - name: Upload Bandit report + uses: actions/upload-artifact@v4 + if: always() + with: + name: bandit-security-report + path: bandit-report.json + + - name: pip-audit – dependency vulnerability check + run: pip-audit --requirement backend/requirements.txt + continue-on-error: true + + codeql: + name: CodeQL Analysis + runs-on: ubuntu-latest + needs: lint + # CodeQL is only meaningful on real pushes/PRs, not the weekly schedule alone + if: github.event_name != 'schedule' + permissions: + actions: read + contents: read + security-events: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: python + queries: security-and-quality + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: /language:python + + dependency-review: + name: Dependency Review + runs-on: ubuntu-latest + needs: lint + if: github.event_name == 'pull_request' + permissions: + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Dependency Review + uses: actions/dependency-review-action@v4 + with: + fail-on-severity: moderate + + # ── Stage 3: Docker Build & Publish (only on main push) ────────────────── + docker: + name: Docker Build & Publish + runs-on: ubuntu-latest + needs: [test, security] + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + permissions: + contents: read + packages: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=ref,event=branch + type=sha,prefix= + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: ./backend + file: ./backend/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + # ── Stage 4: GitOps – update preprod k8s manifest ──────────────────────── + update-k8s-manifest: + name: Update Preprod K8s Manifest + runs-on: ubuntu-latest + needs: [docker] + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + permissions: + contents: read + + steps: + - name: Compute image tag + id: tag + run: | + SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7) + echo "image=ghcr.io/${{ github.repository }}:${SHORT_SHA}" >> "$GITHUB_OUTPUT" + echo "short_sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT" + echo "image_pattern=^ghcr\\.io/${{ github.repository }}:" >> "$GITHUB_OUTPUT" + + - name: Check if GH_PAT is configured and has repo access + id: pat-check + env: + GH_PAT: ${{ secrets.GH_PAT }} + run: | + if [ -z "$GH_PAT" ]; then + echo "::warning::GH_PAT secret is not configured. Skipping k8s manifest update." + echo "available=false" >> "$GITHUB_OUTPUT" + else + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \ + -H "Authorization: Bearer $GH_PAT" \ + "https://api.github.com/repos/${{ env.K8S_STATE_REPO }}") + if [ "$HTTP_CODE" = "200" ]; then + echo "available=true" >> "$GITHUB_OUTPUT" + else + echo "::warning::GH_PAT does not have access to ${{ env.K8S_STATE_REPO }} (HTTP $HTTP_CODE). Skipping k8s manifest update." + echo "available=false" >> "$GITHUB_OUTPUT" + fi + fi + + - name: Checkout k8s-cluster-state + if: steps.pat-check.outputs.available == 'true' + uses: actions/checkout@v4 + with: + repository: ${{ env.K8S_STATE_REPO }} + token: ${{ secrets.GH_PAT }} + path: k8s-cluster-state + ref: main + + - name: Update image tag in preprod manifest + if: steps.pat-check.outputs.available == 'true' + uses: mikefarah/yq@v4.44.6 + env: + IMAGE: ${{ steps.tag.outputs.image }} + IMAGE_PATTERN: ${{ steps.tag.outputs.image_pattern }} + with: + cmd: | + yq -i '(.. | select(tag == "!!str") | select(test(strenv(IMAGE_PATTERN)))) = strenv(IMAGE)' \ + k8s-cluster-state/apps/dmarq/preprod/dmarq-stack.yaml + + - name: Commit and push manifest update + if: steps.pat-check.outputs.available == 'true' + run: | + cd k8s-cluster-state + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add apps/dmarq/preprod/dmarq-stack.yaml + if git diff --staged --quiet; then + echo "No changes to commit" + else + git commit -m "chore(preprod): update dmarq image to ${{ steps.tag.outputs.short_sha }}" + git push + fi diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml deleted file mode 100644 index 42dfc88..0000000 --- a/.github/workflows/pylint.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Pylint - -on: [push] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.10' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install pylint - cd backend && pip install -r requirements.txt - - name: Analysing the code with pylint - run: | - pylint $(git ls-files '*.py') --disable=C0111,R0903 - continue-on-error: true diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml deleted file mode 100644 index 80522dc..0000000 --- a/.github/workflows/security.yml +++ /dev/null @@ -1,99 +0,0 @@ -name: Security Scanning - -on: - push: - branches: [ main, develop ] - pull_request: - branches: [ main, develop ] - schedule: - # Run weekly on Mondays at 00:00 UTC - - cron: '0 0 * * 1' - -jobs: - security-scan: - name: Security Vulnerability Scan - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.10' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install bandit safety detect-secrets - cd backend && pip install -r requirements.txt - - - name: Run Bandit (Python Security Linter) - run: | - bandit -r backend/app -f json -o bandit-report.json || true - bandit -r backend/app - continue-on-error: true - - - name: Run Safety (Dependency Vulnerability Check) - run: | - safety check --json || true - safety check - continue-on-error: true - - - name: Run detect-secrets - run: | - detect-secrets scan --baseline .secrets.baseline || true - continue-on-error: true - - - name: Upload Bandit Report - uses: actions/upload-artifact@v4 - if: always() - with: - name: bandit-security-report - path: bandit-report.json - - codeql-analysis: - name: CodeQL Analysis - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'python' ] - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - queries: security-and-quality - - - name: Autobuild - uses: github/codeql-action/autobuild@v3 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:${{matrix.language}}" - - dependency-review: - name: Dependency Review - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Dependency Review - uses: actions/dependency-review-action@v4 - with: - fail-on-severity: moderate