Files
ObsiGate/.gitea/workflows/ci.yml
T
bruno 9037f2dd69
CI / security (pull_request) Failing after 2s
CI / lint (pull_request) Successful in 1m35s
CI / test (pull_request) Successful in 3m47s
CI / build (pull_request) Successful in 15s
CI / e2e (pull_request) Failing after 30m52s
fix(ci): e2e joint l'app par DNS de réseau (DinD), plus de port publié ni curls sans timeout
Le step 'Start ObsiGate' pendait : boucle de health curls sans --max-time
vers l'IP passerelle:2029 (injoignable/port fuité par un run antérieur) =
150 s de blocage par tentative. L'app est désormais rattachée au réseau du
job et jointe par nom (obsigate-e2e:8080), avec timeout partout et dump
des logs container à l'échec.
2026-11-03 08:44:08 -05:00

335 lines
15 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.
# ObsiGate CI/CD Pipeline
# Runs on every push and pull request to main
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
# ── Lint ──────────────────────────────────────────────────────────
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
run: |
pip install ruff mypy
pip install -r backend/requirements.txt
- name: Ruff (linter)
run: ruff check backend/
- name: Mypy (type checker)
run: mypy backend/ --ignore-missing-imports
- name: Frontend validation
run: node tests/frontend/validate-imports.mjs
- name: Frontend unit tests
run: |
node tests/frontend/unit.test.mjs
node tests/frontend/navfacets.test.mjs
node tests/frontend/desktop-roots.test.mjs
node tests/frontend/image-viewer.test.mjs
node tests/frontend/pdf-viewer.test.mjs
node tests/frontend/forge-completion.test.mjs
node tests/frontend/config-mobile.test.mjs
node tests/frontend/settings-order-avatar.test.mjs
node tests/frontend/mobile-toolbar.test.mjs
node tests/frontend/pretty.test.mjs
node tests/frontend/media-viewer.test.mjs
node tests/frontend/mfa-settings.test.mjs
- name: Frontend JSDOM tests (PaneManager + Excalidraw + Plugins + AI + SW + Collab + Mobile + Semantic + Desktop + Inline edition + Upload + XLSX)
run: |
cd tests/frontend
if [ -d node_modules ]; then
node pane-manager.test.mjs
node excalidraw-viewer.test.mjs
node plugins.test.mjs
node ai.test.mjs
node ai-sidebar.test.mjs
node sidebar-filters.test.mjs
node search-facets.test.mjs
node sw.test.mjs
node collab.test.mjs
node mobile-editor.test.mjs
node semantic-search.test.mjs
node desktop.test.mjs
node toolbar-order.test.mjs
node editor-inline.test.mjs
node ai-quick-actions.test.mjs
node upload.test.mjs
node config-ai-keys.test.mjs
node xlsx-viewer.test.mjs
node filechat.test.mjs
else
echo "tests/frontend/node_modules missing - installing jsdom"
npm install --no-audit --no-fund --silent
node pane-manager.test.mjs
node excalidraw-viewer.test.mjs
node plugins.test.mjs
node ai.test.mjs
node ai-sidebar.test.mjs
node sidebar-filters.test.mjs
node search-facets.test.mjs
node sw.test.mjs
node collab.test.mjs
node mobile-editor.test.mjs
node semantic-search.test.mjs
node desktop.test.mjs
node toolbar-order.test.mjs
node editor-inline.test.mjs
node ai-quick-actions.test.mjs
node upload.test.mjs
node config-ai-keys.test.mjs
node xlsx-viewer.test.mjs
node filechat.test.mjs
fi
# ── Tests ─────────────────────────────────────────────────────────
test:
needs: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
run: |
pip install pytest pytest-cov pytest-asyncio httpx
pip install -r backend/requirements.txt
pip install -r backend/requirements-test.txt || echo "test deps install failed (non-blocking — PDF tests will skip)"
- name: Run tests
run: pytest tests/ --cov=backend --cov-report=xml --cov-report=term -q
- name: Upload coverage artifact
uses: actions/upload-artifact@v3
with:
name: coverage-report
path: coverage.xml
retention-days: 30
# ── Security scan ─────────────────────────────────────────────────
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
# setuptools / pip sont mis à jour : l'image de base peut embarquer
# une version couverte par un advisory fraîchement publié
# (PYSEC-2026-3447 / PYSEC-2026-3721).
# NOTE runner Gitea Act (BUG-083) : aucun `#` dans le `run:`.
run: |
pip install -U pip setuptools
pip install bandit pip-audit
pip install -r backend/requirements.txt
- name: Bandit (SAST, bloquant — #87)
# B105 est exclu (aligné avec [tool.bandit] de pyproject.toml :
# faux positifs systématiques sur les noms de variables) ; les rares
# vrais positifs restants portent un `# nosec` justifié inline.
run: bandit -r backend/ --skip B101,B105,B110,B310
- name: Semgrep (SAST local) — DÉSACTIVÉ (BUG-091)
# Les règles locales (semgrep-rules/, 8 règles) ne sont plus exécutées
# en CI : semgrep-core est un exécutable natif que le runner actuel ne
# peut pas lancer (exit 127, sans message exploitable) — les releases
# récentes exigent un CPU x86-64-v2, et la dernière version compatible
# (1.157.0, core statique vérifié en baseline v1) échoue aussi. Les
# règles restent applicables en local : `semgrep --config semgrep-rules/
# backend/`. À réactiver dès que le runner dispose d'un CPU x86-64-v2
# (ou d'une image de runner plus récente). Bandit et pip-audit, eux,
# restent bloquants dans ce job.
# NOTE runner Gitea Act (BUG-083) : aucun `#` dans le `run:`.
continue-on-error: true
run: |
echo "::warning::SAST semgrep non exécutée (runner incompatible — BUG-091). Bandit et pip-audit restent bloquants."
- name: Pip-audit (bloquant — #87)
# Bloquant depuis T6 (#87) : dépendances qualifiées (mistune 3.3.3,
# python-multipart 0.0.31, weasyprint 70, mcp 1.28.1, fastapi 0.141.1
# + starlette 1.7.0, setuptools 84 — suite complète verte + 0 vuln).
# Seule exception documentée : PYSEC-2026-1325 (ecdsa, Minerva) —
# aucun correctif upstream ET ObsiGate ne signe/vérifie qu'en HS256
# (backend/auth/jwt_handler.py), les chemins ECDSA P-256 ne
# s'exécutent jamais. Les advisories pyjwt (PYSEC-2026-178 puis
# CVE-2026-102274) sont corrigées par le plancher pyjwt>=2.14.0 de
# backend/requirements.txt (BUG-091, BUG-095).
# PYSEC-2026-3910 / PYSEC-2026-3911 (pypdf, DoS de ressources sur
# l'extraction de texte et la lecture d'outlines — donc atteignables
# via backend/pdf_reader.py) sont corrigés par le plancher
# pypdf>=6.16.1 (BUG-093), relevé à pypdf>=6.21.2 par #224 (11 CVE
# sur la 6.19.0 de la toolcache, corrigées au plus tard en 6.21.2).
# #224 ajoute les planchers brotli>=1.2.1 (PYSEC-2026-4322,
# GHSA-9v8h-qqgm-4m35), filelock>=4.2.1 (CVE-2026-112940,
# CVE-2026-112913, CVE-2026-112907) et opentelemetry-api>=1.45.1
# (CVE-2026-114603) : paquets transitifs ou préinstallés dans la
# toolcache, audités mais jamais alignés sans plancher explicite.
# CVE-2026-97687 / CVE-2026-97688 / CVE-2026-97689 (urllib3 2.7.0)
# corrigés par le plancher urllib3>=2.8.0.
# CVE-2026-104874 (multidict 6.7.x) corrigé par le plancher
# multidict>=6.9.1 (transitive aiohttp/yarl ; 6.7.x est dans la
# toolcache de l'image du runner — même piège « already satisfied »).
# CVE-2026-85394 (python-jose ≤3.5.0, forgery HS256 par clé publique
# DER passée comme secret HMAC) : AUCUN correctif upstream (projet
# sans release depuis 2025). Non atteignable dans ObsiGate :
# jwt.decode passe toujours algorithms=["HS256"] et un secret
# symétrique serveur (backend/auth/jwt_handler.py,
# backend/mcp/confirmations.py) — jamais une clé publique comme clé.
# Ces planchers doivent rester *au-dessus* des versions préinstallées
# dans la toolcache de l'image du runner : en dessous, pip répond
# « already satisfied » et n'aligne jamais (c'est exactement ce qui a
# fait échouer ce job). Le garde-fou tests/test_ci_workflow.py::
# TestDependencySecurityFloors verrouille ces planchers.
# NOTE runner Gitea Act (BUG-083) : aucun `#` dans le `run:`.
run: pip-audit --ignore-vuln PYSEC-2026-1325 --ignore-vuln CVE-2026-85394
# ── Docker build ──────────────────────────────────────────────────
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Version livrée
# VERSION (racine du dépôt) est copié dans l'image par le Dockerfile :
# plus aucun numéro généré ni codé en dur dans le pipeline.
run: echo "Version livree = $(cat VERSION)"
- name: Configure DNS (workaround flaky 127.0.0.11 resolver)
# GitHub Actions runners occasionally fail to resolve auth.docker.io via
# the embedded Docker DNS (127.0.0.11:53 → "server misbehaving").
# Force the daemon to use public DNS as a fallback.
run: |
sudo mkdir -p /etc/docker
if ! grep -q "dns" /etc/docker/daemon.json 2>/dev/null; then
echo '{"dns": ["8.8.8.8", "1.1.1.1", "9.9.9.9"]}' | sudo tee /etc/docker/daemon.json
sudo systemctl restart docker || sudo service docker restart || true
sleep 3
fi
- name: Build Docker image (retry on transient DNS/network errors)
run: |
for attempt in 1 2 3; do
echo "=== docker build attempt $attempt/3 ==="
if docker build -t obsigate:ci . ; then
echo "✓ Docker build succeeded"
exit 0
fi
echo "✗ Build failed (attempt $attempt)"
if [ $attempt -lt 3 ]; then
sleep $((attempt * 10))
fi
done
echo "✗ Docker build failed after 3 attempts"
exit 1
- name: Verify image
run: docker images obsigate:ci
# ── E2E Tests (Playwright) ─────────────────────────────────────────
e2e:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install Playwright
run: |
npm ci
npx playwright install --with-deps chromium
- name: Npm audit (bloquant — #87, 0 dépendance prod hors Playwright)
run: npm audit --omit=dev
- name: Start ObsiGate
run: |
docker rm -f obsigate-e2e 2>/dev/null || true
# DinD robuste : PAS de port publié (les ports 2029 fuités par des
# runs antérieurs font échouer « docker start », et l'IP passerelle
# du job n'est pas toujours joignable → boucle de health curls sans
# timeout qui pendaient 150 s chacun). À la place, l'app est
# rattachée au réseau du job et jointable par nom DNS embarqué.
JOBNET=$(docker inspect -f '{{range $k, $v := .NetworkSettings.Networks}}{{$k}} {{end}}' "$(hostname)" 2>/dev/null | awk '{print $1}')
docker create --name obsigate-e2e \
-v $(pwd)/test_vault:/vaults/TestVault \
-v $(pwd)/test_dir:/vaults/TestDir \
-e VAULT_1_NAME=TestVault \
-e VAULT_1_PATH=/vaults/TestVault \
-e DIR_1_NAME=TestDir \
-e DIR_1_PATH=/vaults/TestDir \
-e OBSIGATE_AUTH_ENABLED=false \
-e OBSIGATE_ALLOW_INSECURE=true \
obsigate:ci
# Docker-in-docker : le bind mount $(pwd)/... pointe sur un chemin
# du job container, inexistant sur l'hôte → montage vide. Les -v
# créent quand même /vaults/* dans le container ; on y copie les
# fixtures avant le démarrage (l'indexeur scanne au démarrage).
docker cp test_vault/. obsigate-e2e:/vaults/TestVault
docker cp test_dir/. obsigate-e2e:/vaults/TestDir
if [ -n "$JOBNET" ]; then docker network connect "$JOBNET" obsigate-e2e || true; fi
docker start obsigate-e2e
if [ -n "$JOBNET" ]; then
APP_URL="http://obsigate-e2e:8080"
else
echo "Réseau du job introuvable (JOBNET vide) — impossible de localiser l'app sans port publié"
exit 1
fi
echo "APP_URL: $APP_URL"
# --max-time : ne JAMAIS pendre sans timeout (bug d'origine : curls
# sans timeout = 150 s × 31 tentatives, job « bloqué »).
for i in $(seq 1 40); do
if curl -sf --max-time 3 "$APP_URL/api/health"; then echo "Health OK"; break; fi
sleep 1
done
curl -sf --max-time 5 "$APP_URL/api/health" >/dev/null || { echo "App not reachable at $APP_URL"; docker logs --tail 40 obsigate-e2e; exit 1; }
# Les fixtures sont copiées via `docker cp` en root → rendre le vault
# inscriptible par l'utilisateur non-root de l'app (UID 1000), sinon
# toute création/édition de fichier renvoie 403 « Vault is read-only ».
docker exec -u 0 obsigate-e2e chmod -R a+rwX /vaults/TestVault /vaults/TestDir || true
- name: Run E2E tests
run: |
BASE_URL="http://obsigate-e2e:8080"
docker exec obsigate-e2e true >/dev/null 2>&1 || { echo "obsigate-e2e n'est pas en marche"; exit 1; }
echo "Using BASE_URL=$BASE_URL"
BASE_URL="$BASE_URL" npx playwright test --project=chromium-desktop --reporter=list
- name: Upload test results
if: always()
uses: actions/upload-artifact@v3
with:
name: playwright-report
path: playwright-report/
retention-days: 7
- name: Cleanup
if: always()
run: docker rm -f obsigate-e2e