Développeur

Tester vos factures dans GitLab CI et GitHub Actions

Un job de votre pipeline envoie vos factures d'essai au validateur à chaque modification de votre code. Il devient rouge quand l'une d'elles empire, pas pour un échec déjà connu. Votre projet peut être écrit dans n'importe quel langage : voici les jobs prêts à copier, et la façon de s'en servir au quotidien.

Ce que le job attrape

Le job installe la commande valide-facture, puis valide chaque fichier .pdf et .xml du dossier tests/factures : format du fichier, règles de la norme EN 16931 et contrôles de la réforme française. Il compare ensuite chaque résultat à un fichier de référence, versionné dans votre dépôt, qui photographie les résultats que vous avez acceptés. Son code de sortie dit quoi faire dans la demande de fusion :

  • 0 : aucune facture n'a empiré par rapport à la référence. Une facture déjà non conforme, connue de la référence, ne rend pas le job rouge.
  • 1 : une facture que la référence ne connaît pas encore est en échec. Corrigez la facture, ou le code qui la produit.
  • 2 : une régression. Une facture a empiré, ou a disparu du dossier. Lisez la cause probable écrite sous la ligne du fichier : votre code a changé, ou ce sont les règles du validateur.
  • 3 : le test n'a pas pu se faire (clé absente ou refusée, quota atteint, service indisponible). Ce code ne dit rien de vos factures : relancez le job une fois la cause levée.

Un job dans un autre langage que votre projet

Chaque job d'un pipeline choisit son image Docker. Celui de cette page tourne sur une image qui contient PHP et Composer, et il y installe la commande à chaque exécution. PHP tourne dans le job, pas dans votre projet : votre dépôt n'a besoin ni de composer.json, ni de PHP sur vos postes, et le reste de votre pipeline ne change pas.

Le job lit les factures du dossier tests/factures : versionnées dans votre dépôt, ou produites par un job précédent de votre pipeline, qui les lui transmet.

Sans PHP du tout, le script de la page Valider une facture avec curl donne les codes 0, 1 et 3 dans un job, mais sans fichier de référence ni rapport JUnit.

Le jeu de factures d'essai

  • Déterministe. Produisez chaque facture avec une horloge figée, des numéros fixes et aucun identifiant aléatoire. La référence garde l'empreinte de chaque fichier : un fichier regénéré à l'identique garde la sienne, et devant une régression la commande peut dire si la cause est votre code ou les règles du validateur.
  • Petit et représentatif. Un fichier par type de document et par particularité de votre facturation (avoir, remise, exonération, autre devise) suffit. Chaque fichier coûte une validation à chaque exécution.
  • Jamais une facture réelle d'un client. Elle porte des données personnelles et commerciales, et elle resterait dans l'historique de votre dépôt.

La référence

Produisez-la une première fois depuis votre poste, à la racine du dépôt. Docker suffit, sans PHP installé ; la commande s'écrit pour un shell POSIX (Linux, macOS, ou WSL sous Windows). Exportez d'abord votre clé API dans VALIDE_FACTURE_API_KEY, par exemple avec read -rs VALIDE_FACTURE_API_KEY && export VALIDE_FACTURE_API_KEY :

docker run --rm --user "$(id -u):$(id -g)" -e VALIDE_FACTURE_API_KEY -v "$PWD":/projet -w /projet composer:2 sh -c '
composer global config allow-plugins.php-http/discovery true &&
composer global require logalysm/valide-facture:^1.0 --no-interaction --no-progress &&
"$(composer global config bin-dir --absolute)/valide-facture" baseline update tests/factures'
git add valide-facture.baseline.json

-e VALIDE_FACTURE_API_KEY, sans valeur, transmet la clé de votre shell au conteneur sans l'écrire dans la commande ni dans votre historique. --user fait écrire valide-facture.baseline.json à votre nom : sans lui, sur Linux, le fichier appartient à root. Commitez la référence avec les factures qu'elle décrit.

Relancez la même commande après chaque changement de résultat que vous acceptez : une facture corrigée, que le job signale « amélioré », ou un cas ajouté au jeu d'essai. Commitez la nouvelle référence avec le changement qui l'explique. Ne la regénérez pas pour faire passer une régression : elle en effacerait la seule trace.

Ajouté à la commande check du job, --strict fait échouer toute différence avec la référence, y compris une amélioration ou un fichier nouveau. Il convient quand la référence doit toujours décrire exactement le jeu d'essai.

GitLab CI

Ajoutez ce job à votre .gitlab-ci.yml :

valide-facture:
    stage: test
    image: composer:2
    script:
        - composer global config allow-plugins.php-http/discovery true
        - composer global require logalysm/valide-facture:^1.0 --no-interaction --no-progress
        - export PATH="$(composer global config bin-dir --absolute):$PATH"
        # Les factures de tests/factures sont produites par votre code, ou versionnées.
        - valide-facture check --baseline valide-facture.baseline.json tests/factures --junit rapport-junit.xml
    artifacts:
        when: always
        reports:
            junit: rapport-junit.xml

La première ligne de script autorise le plugin php-http/discovery, qui installe le client HTTP de la commande. Sans elle, Composer bloque le plugin en mode non interactif, et chaque fichier sort en 3.

Déclarez la clé en variable CI/CD du projet (Settings > CI/CD > Variables), nommée VALIDE_FACTURE_API_KEY, en visibilité Masked. Laissez Protect variable décoché, sauf si le job ne tourne que sur des branches protégées : une variable protégée n'est transmise qu'à leurs pipelines, et ailleurs le job sort en 3, clé absente.

when: always envoie le rapport même quand le job échoue ; par défaut, GitLab ne l'envoie qu'en cas de succès. Le rapport s'affiche dans l'onglet Tests du pipeline et dans la demande de fusion, avec un cas de test par facture.

GitHub Actions

Le même contrôle, dans .github/workflows/valide-facture.yml :

name: valide-facture

on:
    push:
        branches: [main]
    pull_request:

permissions:
    contents: read

jobs:
    valide-facture:
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
            - uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0
              with:
                  php-version: '8.5' # toute version prise en charge par la commande : 8.3 ou plus récente
            - run: |
                  composer global config allow-plugins.php-http/discovery true
                  composer global require logalysm/valide-facture:^1.0 --no-interaction --no-progress
                  composer global config bin-dir --absolute >> "$GITHUB_PATH"
            # Les factures de tests/factures sont produites par votre code, ou versionnées.
            - run: valide-facture check --baseline valide-facture.baseline.json tests/factures --junit rapport-junit.xml
              env:
                  VALIDE_FACTURE_API_KEY: ${{ secrets.VALIDE_FACTURE_API_KEY }}
            - uses: mikepenz/action-junit-report@6ef3fdcfcd5f0b5d14df3e659ef19b8c6820f93d # v6.6.1
              if: ${{ !cancelled() }}
              with:
                  report_paths: rapport-junit.xml
                  annotate_only: true

Chaque étape run est un shell neuf : la troisième étape inscrit le dossier de la commande dans $GITHUB_PATH, que GitHub ajoute au chemin des étapes suivantes.

Déclarez la clé en secret du dépôt (Settings > Secrets and variables > Actions), nommé VALIDE_FACTURE_API_KEY. Seule l'étape qui lance check la reçoit. Sur une pull request ouverte depuis un fork, GitHub ne transmet pas les secrets : le job sort en 3, clé absente. N'utilisez pas pull_request_target pour le contourner : il donnerait le secret au code du fork.

Le workflow tourne sur les push de main et sur les pull requests : une branche qui a une pull request ouverte n'est pas validée deux fois à chaque push, ni sa consommation de quota doublée.

GitHub n'affiche pas de rapport JUnit de lui-même. La dernière étape le lit et l'écrit dans le résumé du job, même quand la commande a échoué : mikepenz/action-junit-report est une action tierce, sous licence Apache-2.0, que vous pouvez remplacer par tout lecteur de JUnit. Avec annotate_only: true, elle ne crée pas de check run, et le droit contents: read lui suffit. Chaque action est épinglée par l'empreinte de son commit : une étiquette peut être déplacée, une empreinte non.

Ce que vous faites tourner dans votre pipeline

^1.0 installe la dernière version 1 de la commande, jamais une version 2, qui pourrait changer les codes de sortie ou le format de la référence. Les dépendances de la commande, elles, sont résolues à chaque exécution : une version publiée entre deux pipelines entre dans votre job sans que vous l'ayez relue. L'image composer:2 suit de même les versions de PHP et de Composer.

Pour figer davantage, installez la commande depuis un composer.json dédié au job, rangé dans un sous-dossier, et versionnez son composer.lock : composer install rejoue alors exactement les mêmes versions. Épinglez l'image par son empreinte de la même façon.

Ce que coûte une exécution

Chaque fichier validé consomme une unité de quota, à chaque exécution du pipeline ; la production de la référence aussi. Dix factures d'essai et vingt pipelines dans le mois consomment deux cents unités. Un refus d'entrée (fichier vide, trop volumineux, ni PDF ni XML) et une indisponibilité du service n'en consomment pas. Une clé refusée ou un quota atteint arrêtent la commande : les fichiers suivants ne sont pas envoyés.

Aller plus loin

La liste complète des refus du service est sur la page des guides du développeur, et chaque code de règle qu'un rapport peut citer a sa page parmi les codes d'erreur du validateur. Pour voir le rapport complet d'une facture avant de l'ajouter au jeu d'essai, déposez-la dans le validateur.

Portée de cette page

Ces jobs testent vos factures d'essai, pas celles que vous émettez. Le verdict porte sur la conformité technique du document au format et aux règles appliquées par le validateur ; il constitue une aide au diagnostic, et ne présume pas de l'acceptation de la facture par la plateforme de votre destinataire.

Développeur