Aller au contenu

Pipeline Gitea Actions

Assembler tous les outils de qualité et de sécurité dans un pipeline CI automatise avec Gitea Actions.


Architecture du pipeline

Le pipeline orchestre six jobs qui couvrent l'ensemble de la chaîne qualité. Certains s'exécutent en parallèle pour réduire le temps total, d'autres dépendent du résultat des précédents.

graph TD
    A["Push / PR"] --> B["Job: lint"]
    A --> C["Job: security-deps"]
    A --> D["Job: security-sast"]
    B --> E["Job: sonar"]
    E --> F["Job: security-image"]
    C --> G["Job: quality-gate"]
    D --> G
    F --> G
    G -->|"PASS"| H["Deploy"]
    G -->|"FAIL"| I["Bloque"]

Les jobs lint, security-deps et security-sast démarrent immédiatement à chaque push ou pull request. Le job sonar attend que le lint soit termine (il exploite les rapports générés). Le scan d'image ne s'exécuté que si un Dockerfile a été modifie. Enfin, le quality-gate synthetise tous les résultats et décidé si le code peut être déployé.


Runner self-hosted

Pourquoi un runner auto-heberge

Gitea Actions est compatible avec la syntaxe GitHub Actions, mais dans un contexte enterprise ou air-gapped, un runner self-hosted est indispensable :

  • Accès au réseau interne : le runner peut atteindre SonarQube, les registres prives, les bases de vulnérabilités locales
  • Outils pre-installes : Podman, Trivy, Semgrep et leurs caches sont déjà presents sur la machine
  • Isolation : aucune donnée de code ne quitte le périmètre réseau

Déploiement du runner dans un conteneur Podman

version: "3"
services:
  runner:
    image: gitea/act_runner:latest
    container_name: gitea-runner
    environment:
      GITEA_INSTANCE_URL: "http://gitea:3000"
      GITEA_RUNNER_REGISTRATION_TOKEN: "<TOKEN>"
      GITEA_RUNNER_LABELS: "self-hosted,podman"
    volumes:
      - /run/user/1000/podman/podman.sock:/var/run/docker.sock
      - runner_data:/data
volumes:
  runner_data:

Enregistrement du runner

Une fois le conteneur démarré, enregistrez le runner aupres de l'instance Gitea :

podman exec gitea-runner act_runner register

Le runner apparait alors dans Administration > Runners de votre instance Gitea.

Socket Podman rootless

Le montage /run/user/1000/podman/podman.sock suppose que le socket Podman rootless est actif pour l'utilisateur avec l'UID 1000. Activez-le avec systemctl --user enable --now podman.socket. Si vous utilisez un UID différent, adaptez le chemin en conséquence. En environnement rootful, utilisez /run/podman/podman.sock à la place.


Workflow complet

Le fichier .gitea/workflows/quality.yml contient l'ensemble du pipeline. Chaque job est détaillé ci-dessous.

Job lint

Le premier job exécuté les linters et formatters sur le code source. Il produit des rapports JSON exploitables par les jobs suivants.

name: Quality Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: self-hosted
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"

      - name: Install Node dependencies
        run: npm ci

      - name: ESLint
        run: npx eslint . --format json --output-file eslint-report.json || true

      - name: Prettier check
        run: npx prettier --check "**/*.{js,ts,jsx,tsx,json,md,yaml,yml}"

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install Python linters
        run: pip install ruff black

      - name: Ruff
        run: ruff check . --output-format json --output-file ruff-report.json || true

      - name: Black check
        run: black --check .

      - name: Upload lint reports
        uses: actions/upload-artifact@v4
        with:
          name: lint-reports
          path: |
            eslint-report.json
            ruff-report.json

Ce job installe les écosystèmes Node.js et Python, puis exécuté les linters (ESLint, Ruff) en générant des rapports JSON. Les formatters (Prettier, Black) verifient que le code respecte les conventions de style. Les rapports sont uploades comme artefacts pour exploitation ultérieure.

Job sonar

Le job SonarQube dépend du lint — il s'exécuté uniquement si le lint est termine. Il envoie le code a SonarQube pour l'analyse approfondie, puis vérifié le quality gate via l'API.

  sonar:
    runs-on: self-hosted
    needs: lint
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: SonarQube Scan
        uses: sonarsource/sonarqube-scan-action@v3
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
          SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}

      - name: Quality Gate check
        run: |
          STATUS=$(curl -s -u "${{ secrets.SONAR_TOKEN }}:" \
            "${{ secrets.SONAR_HOST_URL }}/api/qualitygates/project_status?projectKey=${{ github.repository }}" \
            | jq -r '.projectStatus.status')
          echo "Quality Gate status: $STATUS"
          if [ "$STATUS" != "OK" ]; then
            echo "Quality Gate FAILED"
            exit 1
          fi

Le fetch-depth: 0 est nécessaire pour que SonarQube puisse analyser l'historique Git et calculer les métriques de couverture sur le nouveau code. Le quality gate est vérifié via un appel API avec curl et jq.

Job security-deps

Ce job analyse les dépendances applicatives à la recherche de vulnérabilités connues (CVE). Il s'exécuté en parallèle du lint.

  security-deps:
    runs-on: self-hosted
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Cache pip-audit
        uses: actions/cache@v4
        with:
          path: ~/.cache/pip
          key: pip-audit-${{ hashFiles('**/requirements*.txt') }}

      - name: pip-audit
        run: |
          pip install pip-audit
          pip-audit -r requirements.txt --format json --output pip-audit-report.json || true

      - name: npm audit
        run: npm audit --json > npm-audit-report.json || true

      - name: Cache Trivy DB
        uses: actions/cache@v4
        with:
          path: ~/.cache/trivy
          key: trivy-db-${{ github.run_id }}
          restore-keys: trivy-db-

      - name: Trivy filesystem scan
        run: |
          trivy filesystem . \
            --severity HIGH,CRITICAL \
            --format sarif \
            --output trivy-fs-report.sarif

      - name: Upload SCA reports
        uses: actions/upload-artifact@v4
        with:
          name: sca-reports
          path: |
            pip-audit-report.json
            npm-audit-report.json
            trivy-fs-report.sarif

Le job combine trois outils complementaires : pip-audit pour les dépendances Python, npm audit pour les dépendances Node.js, et Trivy en mode filesystem pour une détection croisee. Le || true sur pip-audit et npm audit evite que le job échoué immédiatement — les résultats sont evalues au quality gate.

Job security-sast

L'analyse statique du code source (SAST) avec Semgrep détecté les failles de sécurité dans la logique applicative : injections SQL, XSS, deserialisation non sécurisée, etc.

  security-sast:
    runs-on: self-hosted
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Semgrep SAST scan
        run: |
          pip install semgrep
          semgrep scan \
            --config "p/default" \
            --config "p/owasp-top-ten" \
            --sarif \
            --output semgrep-report.sarif \
            .

      - name: Check critical findings
        run: |
          CRITICAL_COUNT=$(cat semgrep-report.sarif | jq '[.runs[].results[] | select(.level == "error")] | length')
          echo "Critical findings: $CRITICAL_COUNT"
          if [ "$CRITICAL_COUNT" -gt 0 ]; then
            echo "CRITICAL security issues found!"
            exit 1
          fi

      - name: Upload SAST report
        uses: actions/upload-artifact@v4
        with:
          name: sast-report
          path: semgrep-report.sarif

Semgrep utilise les rulesets default et owasp-top-ten pour une couverture large. Le job compte les findings de niveau error (critiques) dans le rapport SARIF et échoué si au moins un est détecté.

Job security-image

Le scan d'image conteneur ne s'exécuté que si un Dockerfile a été modifie dans le commit ou la PR. Il dépend du job sonar pour s'assurer que le code a passe l'analyse qualité avant de construire l'image.

  security-image:
    runs-on: self-hosted
    needs: sonar
    if: contains(github.event.commits.*.modified, 'Dockerfile') || contains(github.event.commits.*.added, 'Dockerfile')
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Build image
        run: podman build -t ${{ github.repository }}:${{ github.sha }} .

      - name: Trivy image scan
        run: |
          trivy image \
            --severity HIGH,CRITICAL \
            --exit-code 1 \
            --format sarif \
            --output trivy-image-report.sarif \
            ${{ github.repository }}:${{ github.sha }}

      - name: Upload image scan report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: image-scan-report
          path: trivy-image-report.sarif

L'image est construite avec podman build (compatible OCI). Trivy la scanne avec --exit-code 1 : toute vulnérabilité CRITICAL fait échouer le job. Le rapport SARIF est uploade même en cas d'échec grâce à if: always().

Job quality-gate

Le quality gate est le dernier rempart. Il vérifié que tous les jobs précédents ont réussi avant d'autoriser le déploiement.

  quality-gate:
    runs-on: self-hosted
    needs: [lint, sonar, security-deps, security-sast, security-image]
    if: always()
    steps:
      - name: Check job results
        run: |
          echo "Lint: ${{ needs.lint.result }}"
          echo "Sonar: ${{ needs.sonar.result }}"
          echo "Security deps: ${{ needs.security-deps.result }}"
          echo "Security SAST: ${{ needs.security-sast.result }}"
          echo "Security image: ${{ needs.security-image.result }}"

          if [ "${{ needs.lint.result }}" == "failure" ] || \
             [ "${{ needs.sonar.result }}" == "failure" ] || \
             [ "${{ needs.security-deps.result }}" == "failure" ] || \
             [ "${{ needs.security-sast.result }}" == "failure" ] || \
             [ "${{ needs.security-image.result }}" == "failure" ]; then
            echo "Quality gate FAILED — one or more jobs failed"
            exit 1
          fi

          echo "Quality gate PASSED — all checks successful"

Le if: always() garantit que ce job s'exécuté même si certains jobs ont échoué. Il inspecte le résultat de chaque job et échoué si l'un d'entre eux est en erreur. Le job security-image peut avoir le statut skipped (si aucun Dockerfile n'a change) — ce n'est pas considéré comme un échec.


Optimisation

Cache

Le cache accéléré considérablement les executions suivantes du pipeline. Trois caches sont configures :

  • node_modules : actions/cache avec la clé basée sur le hash de package-lock.json
  • .pip-cache : cache pip pour pip-audit et les linters Python
  • Trivy DB : la base de vulnérabilités Trivy est volumineuse (~40 Mo), la mettre en cache evite de la retelecharger à chaque run
- name: Cache node_modules
  uses: actions/cache@v4
  with:
    path: node_modules
    key: node-${{ hashFiles('package-lock.json') }}

- name: Cache pip
  uses: actions/cache@v4
  with:
    path: ~/.cache/pip
    key: pip-${{ hashFiles('**/requirements*.txt') }}

- name: Cache Trivy DB
  uses: actions/cache@v4
  with:
    path: ~/.cache/trivy
    key: trivy-db-${{ github.run_id }}
    restore-keys: trivy-db-

Parallelisation

Les jobs lint, security-deps et security-sast n'ont aucune dépendance entre eux. Ils démarrent simultanément des le déclenchement du pipeline, ce qui réduit le temps total d'exécution. Seul le job sonar attend la fin du lint (il exploite les rapports générés).

Exécution conditionnelle

Le job security-image utilise une condition if pour ne s'exécuter que lorsqu'un Dockerfile est present dans les fichiers modifies. Cela evite de construire et scanner une image à chaque commit sur du code applicatif uniquement.


Artefacts et rapports

Rapports SARIF et JSON

Chaque job produit des rapports dans des formats exploitables :

Job Format Fichier
lint JSON eslint-report.json, ruff-report.json
security-deps JSON + SARIF pip-audit-report.json, npm-audit-report.json, trivy-fs-report.sarif
security-sast SARIF semgrep-report.sarif
security-image SARIF trivy-image-report.sarif

Ces rapports sont stockes comme artefacts du workflow et restent disponibles pendant 90 jours (configurable). Ils permettent l'investigation post-mortem et la traçabilité des audits de sécurité.

Commentaire automatique sur les PR

Pour enrichir l'expérience développeur, le pipeline peut poster un commentaire automatique sur la pull request via l'API Gitea :

curl -X POST "http://gitea:3000/api/v1/repos/org/repo/pulls/${PR_NUMBER}/reviews" \
  -H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
  -H "Content-Type: application/json" \
  -d '{"body": "Quality report: all checks passed"}'

Aller plus loin

Vous pouvez enrichir le commentaire avec un résumé des rapports : nombre de findings Semgrep, nombre de CVE détectées, statut du quality gate SonarQube. Parsez les rapports JSON/SARIF avec jq et construisez le corps du commentaire dynamiquement.