name: Build and deploy on: push: branches: [main, dev] # Les jobs "test-*"/"lint-*"/"sonarqube" tournent sur CHAQUE push (main et # dev) : jusqu'ici aucune étape de CI n'exécutait la suite de tests ni les # outils qualité, rien n'empêchait un commit cassé d'atteindre la # production (voir l'audit qualité de la Phase 0 du plan). "build-and- # push"/"deploy", eux, restent réservés à main (via le filtre "if" sur # gitea.ref) — un push sur dev ne doit jamais redéployer la prod, seulement # faire tourner tests/lint/sonar. # # lint-python/lint-js rejouent EXACTEMENT les hooks pre-commit locaux # (.pre-commit-config.yaml) mais bloquants ici dès le départ (déjà tous # verts en local, voir le commit "Phase 3 : hardening qualite de code") — # les hooks pre-commit ne protègent que la machine du committeur, jamais # un push direct ou une PR mergée depuis ailleurs. djlint EXCLU # volontairement (49 H021 "styles inline" déjà en backlog assumé, voir # CODE_QUALITY.md) — à ajouter ici quand ce lot sera traité. sonarqube, # lui, reste NON-BLOQUANT (continue-on-error) — remis en place le # 18/09/2026 une fois l'instance prod opérationnelle (voir CODE_QUALITY.md # pour l'historique du retrait temporaire), le temps que le rapport de # code smells soit entièrement trié. # Secrets à configurer dans Gitea (Paramètres du dépôt > Actions > Secrets) : # REGISTRY_HOST adresse du registre d'images (ex: gitea.exemple.com) # REGISTRY_IMAGE chemin complet de l'image (ex: gitea.exemple.com/mon-compte/forge-engine) # REGISTRY_USER utilisateur pour "docker login" sur ce registre # REGISTRY_TOKEN jeton d'accès (scope package:write) pour ce registre # DEPLOY_HOST adresse du serveur de production # DEPLOY_USER utilisateur SSH sur ce serveur # DEPLOY_SSH_KEY clé privée SSH (au format PEM) autorisée sur ce serveur # DEPLOY_PATH dossier sur le serveur où vit docker-compose.prod.yml (ex: /home/deploy/forge-engine) # SONAR_TOKEN jeton d'analyse SonarQube (Mon compte > Security > Generate Token # sur sonar.forgebase.fr) — jamais le mot de passe admin. # # Utilise directement docker/ssh/scp en ligne de commande plutôt que des # actions du marketplace, pour ne pas dépendre de la disponibilité de # github.com / du miroir gitea.com depuis le runner self-hosted. jobs: # Deux essais précédents ont échoué : # 1. docker run -v "$PWD":/app ... imbriqué depuis le runner — sur un # runner qui exécute déjà le job dans son propre conteneur (Docker- # outside-of-Docker), "$PWD" ne correspond à aucun chemin que le démon # Docker de l'HÔTE peut monter : /app se retrouvait vide. # 2. jobs.test.container: image: python:3.13-slim — actions/checkout@v4 # (une action Node.js) a alors besoin de Node dans CE conteneur pour # s'exécuter, qui ne l'a pas ("command not found", voir # nektos/act#107) : `container:` remplace tout l'environnement des # steps, pas seulement celui des commandes qu'on y lance soi-même. # Solution : le job tourne sur le runner PAR DÉFAUT (checkout fonctionne, # Node y est déjà disponible), et les tests s'exécutent PENDANT un # `docker build` (Dockerfile passé par stdin, jamais commité) — le # transfert du contexte de build au démon Docker ne dépend JAMAIS d'un # chemin hôte/montage de volume (protocole API, un tar envoyé tel quel), # donc insensible au problème Docker-outside-of-Docker du point 1. test-python: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Tests Python (pytest, exécutés PENDANT le build d'une image jetable) run: | # .dockerignore (à la racine, pensé pour l'image de PROD) exclut # tests/ du contexte de build — COPY . . ne l'aurait donc jamais # inclus, quel que soit le Dockerfile utilisé (.dockerignore # s'applique au contexte entier, pas à un Dockerfile en # particulier). Neutralisé ici SANS RISQUE : ce checkout est # propre à ce job, jetable, jamais repoussé vers le dépôt. mv .dockerignore .dockerignore.disabled-for-ci docker build -f - -t forge-test-python:${{ gitea.sha }} . <<'DOCKERFILE' FROM python:3.13-slim WORKDIR /app COPY requirements.txt requirements-dev.txt ./ RUN pip install --no-cache-dir -r requirements-dev.txt COPY . . RUN python -m pytest tests/ -q DOCKERFILE test-js: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Tests JS (node:test — logique pure de static/js/play/, et l'assistant déclencheurs de static/js/scenes/+static/js/triggers/, voir le plan de modularisation) run: | docker build -f - -t forge-test-js:${{ gitea.sha }} . <<'DOCKERFILE' FROM node:20-slim WORKDIR /app COPY static/js/play/ static/js/play/ COPY static/js/scenes/ static/js/scenes/ COPY static/js/triggers/ static/js/triggers/ RUN node --test static/js/play/__tests__/*.test.js static/js/play/offline/__tests__/*.test.js static/js/scenes/__tests__/*.test.js DOCKERFILE lint-python: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Qualite Python (ruff/mypy/vulture/bandit/import-linter — memes commandes que .pre-commit-config.yaml) run: | # Meme neutralisation de .dockerignore que test-python ci-dessus : # ruff/mypy typent aussi tests/ (voir le plan de typage strict), # exclu par defaut de l'image de PROD. mv .dockerignore .dockerignore.disabled-for-ci docker build -f - -t forge-lint-python:${{ gitea.sha }} . <<'DOCKERFILE' FROM python:3.13-slim WORKDIR /app COPY requirements.txt requirements-dev.txt ./ RUN pip install --no-cache-dir -r requirements-dev.txt COPY . . RUN ruff check . RUN ruff format --check . RUN mypy . RUN vulture RUN bandit -c pyproject.toml -r ai auth core db filters publish routes screens scripts app.py build_css.py RUN lint-imports DOCKERFILE lint-js: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Qualite JS/CSS (eslint/stylelint — memes commandes que .pre-commit-config.yaml, via les scripts npm de package.json) run: | docker build -f - -t forge-lint-js:${{ gitea.sha }} . <<'DOCKERFILE' FROM node:20-slim WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci COPY . . RUN npm run lint:js RUN npm run lint:css DOCKERFILE sonarqube: runs-on: ubuntu-latest # Non-bloquant pendant cette premiere periode (voir le bloc de # commentaires en tete de fichier) — un echec ici n'empeche jamais # build-and-push/deploy, contrairement a lint-python/lint-js. continue-on-error: true steps: - uses: actions/checkout@v4 - name: Analyse SonarQube (rapport seul, non-bloquant) run: | # Meme neutralisation de .dockerignore que test-python : sonar- # project.properties couvre aussi tests/ (sonar.tests). mv .dockerignore .dockerignore.disabled-for-ci docker build -f - -t forge-sonar:${{ gitea.sha }} . <<'DOCKERFILE' FROM sonarsource/sonar-scanner-cli:latest WORKDIR /usr/src COPY . . DOCKERFILE docker run --rm forge-sonar:${{ gitea.sha }} \ -Dsonar.host.url=https://sonar.forgebase.fr \ -Dsonar.token=${{ secrets.SONAR_TOKEN }} build-and-push: needs: [test-python, test-js, lint-python, lint-js] if: gitea.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Connexion au registre d'images run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login "${{ secrets.REGISTRY_HOST }}" -u "${{ secrets.REGISTRY_USER }}" --password-stdin - name: Build de l'image run: | docker build \ -t "${{ secrets.REGISTRY_IMAGE }}:${{ gitea.sha }}" \ -t "${{ secrets.REGISTRY_IMAGE }}:latest" \ . - name: Publication de l'image run: | docker push "${{ secrets.REGISTRY_IMAGE }}:${{ gitea.sha }}" docker push "${{ secrets.REGISTRY_IMAGE }}:latest" deploy: needs: build-and-push if: gitea.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Préparation de la clé SSH run: | mkdir -p ~/.ssh echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/deploy_key chmod 600 ~/.ssh/deploy_key ssh-keyscan -H "${{ secrets.DEPLOY_HOST }}" >> ~/.ssh/known_hosts - name: Envoi du docker-compose.prod.yml sur le serveur run: | scp -i ~/.ssh/deploy_key docker-compose.prod.yml \ "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:${{ secrets.DEPLOY_PATH }}/docker-compose.prod.yml" - name: Pull et redémarrage sur le serveur run: | ssh -i ~/.ssh/deploy_key "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" ' set -e cd "${{ secrets.DEPLOY_PATH }}" echo "REGISTRY_IMAGE=${{ secrets.REGISTRY_IMAGE }}" > .env echo "IMAGE_TAG=${{ gitea.sha }}" >> .env echo "${{ secrets.REGISTRY_TOKEN }}" | docker login "${{ secrets.REGISTRY_HOST }}" -u "${{ secrets.REGISTRY_USER }}" --password-stdin docker compose -f docker-compose.prod.yml pull docker compose -f docker-compose.prod.yml up -d docker image prune -f ' - name: Nettoyage de la cle SSH # if: always() - meme si les etapes precedentes ont echoue, la cle # privee posee ci-dessus (~/.ssh/deploy_key) ne doit jamais rester # sur le disque du job. Le runner semble deja jetable (voir # "docker volume rm .../JOB--..." dans les logs des autres # jobs de ce meme workflow), mais jamais verifie directement pour # CE job (jamais execute sur dev, reserve a main) - ne pas se fier # uniquement a une inference par analogie pour une cle privee. if: always() run: rm -f ~/.ssh/deploy_key