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 :
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/cacheavec la clé basée sur le hash depackage-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.