Files
gh-christianlouis-dmarq/.github/workflows/ci.yml
T
2026-05-23 11:45:11 +02:00

370 lines
11 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: CI
on:
push:
branches: [main, develop]
tags: ['v*']
pull_request:
branches: [main, develop]
workflow_dispatch:
inputs:
release_ref:
description: Git ref to build, for example v1.8.4
required: false
type: string
release_tag:
description: Release image tag to publish, for example v1.8.4
required: false
type: string
promote_stable:
description: Also publish the stable image tag
required: false
default: false
type: boolean
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.13
uses: actions/setup-python@v5
with:
python-version: '3.13'
- 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: Auto-format with Black and isort
run: |
black backend/app
isort backend/app
- 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.13
uses: actions/setup-python@v5
with:
python-version: '3.13'
- 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 --cov-fail-under=80
- 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.13
uses: actions/setup-python@v5
with:
python-version: '3.13'
- 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') ||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')) ||
github.event_name == 'workflow_dispatch'
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.release_ref || github.ref }}
- 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: Compute Docker metadata
id: meta
env:
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
EVENT_NAME: ${{ github.event_name }}
REF_NAME: ${{ github.ref_name }}
REF_TYPE: ${{ github.ref_type }}
RELEASE_TAG: ${{ github.event.inputs.release_tag }}
PROMOTE_STABLE: ${{ github.event.inputs.promote_stable }}
run: |
IMAGE="ghcr.io/$(echo "${GITHUB_REPOSITORY}" | tr '[:upper:]' '[:lower:]')"
SHORT_SHA="$(echo "${GITHUB_SHA}" | cut -c1-7)"
TAGS=("${IMAGE}:${SHORT_SHA}")
if [ "${REF_TYPE}" = "branch" ]; then
SAFE_BRANCH="$(echo "${REF_NAME}" | tr '/:@' '---')"
TAGS+=("${IMAGE}:${SAFE_BRANCH}")
if [ "${REF_NAME}" = "${DEFAULT_BRANCH}" ]; then
TAGS+=("${IMAGE}:latest")
fi
fi
if [ "${REF_TYPE}" = "tag" ]; then
TAGS+=("${IMAGE}:${REF_NAME}")
if [[ "${REF_NAME}" =~ ^v[0-9] ]]; then
TAGS+=("${IMAGE}:${REF_NAME#v}")
TAGS+=("${IMAGE}:stable")
fi
fi
if [ "${EVENT_NAME}" = "workflow_dispatch" ] && [ -n "${RELEASE_TAG}" ]; then
TAGS+=("${IMAGE}:${RELEASE_TAG}")
fi
if [ "${PROMOTE_STABLE}" = "true" ]; then
TAGS+=("${IMAGE}:stable")
fi
{
echo "tags<<EOF"
printf '%s\n' "${TAGS[@]}" | sort -u
echo "EOF"
echo "labels<<EOF"
echo "org.opencontainers.image.source=https://github.com/${GITHUB_REPOSITORY}"
echo "org.opencontainers.image.revision=${GITHUB_SHA}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- 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