Technique

Automatiser validation et correction XML Factur-X en CI/CD (GitHub Actions, GitLab)

8 min de lecture Par FacturX API

Intégrer validation EN16931 et correction XML CII Factur-X dans un pipeline CI/CD. Exemples GitHub Actions et GitLab CI avec gestion d'erreurs.

Valider manuellement chaque facture n’est pas tenable en production. Quand votre ERP génère des centaines de documents Factur-X par jour, une erreur de format qui passe inaperçue se propage sur toute une série avant d’être détectée — par le destinataire ou la PDP. Le coût de la correction manuelle a posteriori est sans commune mesure avec celui d’une validation automatisée en amont.

Cet article couvre l’automatisation de la validation et de la correction XML CII dans un pipeline CI/CD. Pour la conversion d’un PDF classique en Factur-X PDF/A-3, voir l’article dédié.

L’automatisation via CI/CD garantit que chaque document généré est conforme avant sa transmission. Si une mise à jour de votre ERP introduit une régression dans le XML CII — un format de date qui change, un namespace oublié, un champ conditionnellement obligatoire qui disparaît — le pipeline bloque la livraison immédiatement. Les erreurs de format réparables sont corrigées automatiquement, les erreurs de fond déclenchent une alerte à l’équipe.

Pour comprendre les deux niveaux de validation (XSD structurel et Schematron sémantique) et la logique des codes d’erreur BR-*, voir Valider EN16931 / Factur-X en pratique : XSD vs Schematron, erreurs BR-*.

Le pipeline type

Le flux de validation automatisé suit une logique en trois étapes : valider, réparer si possible, alerter sinon.

ERP génère XML/PDF → Validation API → Conforme ?

                          Oui ─────────┼──────────→ Transmission PDP

                          Non ─────────┼──→ Erreur réparable ?

                                 Oui ─────────┼──→ Repair API → Re-validation → Conforme ?
                                              │                                  │
                                 Non ─────────┤                     Oui ─────────┼──→ Transmission
                                              │                                  │
                                              └──→ Alerte équipe    Non ─────────┘
                                                   (erreur de fond)

La distinction entre erreur réparable et erreur de fond est centrale. Les erreurs réparables sont des problèmes de format : dates en ISO au lieu de AAAAMMJJ, virgules au lieu de points décimaux, namespaces manquants. Le moteur Repair les corrige automatiquement avec un diff traçable. Les erreurs de fond — montant TTC incohérent, TVA mal calculée, champ métier absent — nécessitent une intervention humaine.

Portée du Repair : l’endpoint Repair corrige le XML CII structuré (dates, décimaux, namespaces, schemeID). Il ne corrige pas le conteneur PDF/A-3 ni les métadonnées XMP.

Pour le catalogue complet des erreurs réparables et le fonctionnement du moteur de correction, voir Corriger automatiquement un XML CII Factur-X invalide.

Script bash de validation

Un script réutilisable qui encapsule la logique de validation et retourne les codes de sortie appropriés pour l’intégration CI/CD.

#!/usr/bin/env bash
# validate-facturx.sh — Valide un fichier Factur-X via l'API
# Usage: ./validate-facturx.sh <fichier.pdf|fichier.xml>
# Codes retour: 0 = conforme, 1 = non conforme, 2 = erreur réseau/API

set -euo pipefail

FILE="${1:?Usage: $0 <fichier>}"
API_URL="${FACTURX_API_URL:-https://api.facturxapi.com/api/v1}"
API_KEY="${FACTURX_API_KEY:?Variable FACTURX_API_KEY requise}"

if [[ ! -f "$FILE" ]]; then
  echo "ERREUR: fichier '$FILE' introuvable" >&2
  exit 2
fi

# Validation
RESPONSE=$(curl -s -w "\n%{http_code}" \
  -X POST "${API_URL}/validate" \
  -H "X-API-Key: ${API_KEY}" \
  -F "file=@${FILE}" \
  --max-time 30)

HTTP_CODE=$(echo "$RESPONSE" | tail -1)
BODY=$(echo "$RESPONSE" | sed '$d')

if [[ "$HTTP_CODE" -lt 200 || "$HTTP_CODE" -ge 300 ]]; then
  echo "ERREUR API: HTTP ${HTTP_CODE}" >&2
  echo "$BODY" >&2
  exit 2
fi

IS_VALID=$(echo "$BODY" | jq -r '.valid')
if [[ "$IS_VALID" == "true" ]]; then
  echo "CONFORME: $(basename "$FILE")"
  exit 0
fi

# Non conforme — afficher les erreurs
ERROR_COUNT=$(echo "$BODY" | jq '.errors | length')
echo "NON CONFORME: $(basename "$FILE") — ${ERROR_COUNT} erreur(s)" >&2
echo "$BODY" | jq -r '.errors[] | "  [\(.severity)] \(.ruleId): \(.message)"' >&2
exit 1

Ce script sépare trois situations distinctes : le document conforme (code 0), le document non conforme (code 1), et l’erreur technique qui empêche la validation (code 2). Cette convention permet au pipeline CI/CD de distinguer un rejet métier d’un problème d’infrastructure.

Script de validation avec réparation automatique

En production, le pipeline complet tente la réparation avant de déclarer un échec. Rappel : l’endpoint Repair corrige le XML CII structuré (dates, décimaux, namespaces, schemeID). Il ne corrige pas le conteneur PDF/A-3 ni les métadonnées XMP.

#!/usr/bin/env bash
# validate-and-repair.sh — Valide, répare si possible, re-valide
# Usage: ./validate-and-repair.sh <fichier.xml>
# Codes retour: 0 = conforme (ou réparé), 1 = non réparable, 2 = erreur technique

set -euo pipefail

FILE="${1:?Usage: $0 <fichier.xml>}"
API_URL="${FACTURX_API_URL:-https://api.facturxapi.com/api/v1}"
API_KEY="${FACTURX_API_KEY:?Variable FACTURX_API_KEY requise}"
OUTPUT_DIR="${OUTPUT_DIR:-.}"

# Appel API avec capture du code HTTP et gestion d'erreurs
api_call() {
  local endpoint="$1"
  local file="$2"
  local response http_code body

  response=$(curl -s -w "\n%{http_code}" \
    -X POST "${API_URL}/${endpoint}" \
    -H "X-API-Key: ${API_KEY}" \
    -F "file=@${file}" \
    --max-time 30) || {
    echo "ERREUR RÉSEAU: impossible de contacter l'API" >&2
    return 2
  }

  http_code=$(echo "$response" | tail -1)
  body=$(echo "$response" | sed '$d')

  if [[ "$http_code" -lt 200 || "$http_code" -ge 300 ]]; then
    echo "ERREUR API (${endpoint}): HTTP ${http_code}" >&2
    echo "$body" >&2
    return 2
  fi

  # Vérifier que la réponse est du JSON valide
  if ! echo "$body" | jq empty 2>/dev/null; then
    echo "ERREUR: réponse API non JSON (${endpoint})" >&2
    return 2
  fi

  echo "$body"
}

# Étape 1 : validation initiale
RESULT=$(api_call "validate" "$FILE") || exit 2
IS_VALID=$(echo "$RESULT" | jq -r '.valid')

if [[ "$IS_VALID" == "true" ]]; then
  echo "CONFORME: $(basename "$FILE")"
  exit 0
fi

# Étape 2 : tentative de réparation
echo "Non conforme — tentative de réparation..." >&2
REPAIR_BODY=$(api_call "repair" "$FILE") || exit 2

# Sauvegarder le fichier réparé
REPAIRED_FILE="${OUTPUT_DIR}/$(basename "${FILE%.xml}")-repaired.xml"
echo "$REPAIR_BODY" | jq -r '.repairedXml' > "$REPAIRED_FILE"

FIXES_COUNT=$(echo "$REPAIR_BODY" | jq '.fixes | length')
echo "Réparation: ${FIXES_COUNT} correction(s) appliquée(s)" >&2

# Étape 3 : re-validation du fichier réparé
REVALIDATION=$(api_call "validate" "$REPAIRED_FILE") || exit 2
IS_VALID_AFTER=$(echo "$REVALIDATION" | jq -r '.valid')

if [[ "$IS_VALID_AFTER" == "true" ]]; then
  echo "CONFORME APRÈS RÉPARATION: $(basename "$FILE")"
  echo "  Fichier réparé: ${REPAIRED_FILE}"
  exit 0
fi

# Erreurs restantes = erreurs de fond, non réparables
REMAINING=$(echo "$REVALIDATION" | jq '.errors | length')
echo "NON RÉPARABLE: ${REMAINING} erreur(s) de fond restante(s)" >&2
echo "$REVALIDATION" | jq -r '.errors[] | "  [\(.severity)] \(.ruleId): \(.message)"' >&2
exit 1

GitHub Actions : workflow complet

Ce workflow se déclenche sur chaque push dans le dossier invoices/, valide tous les fichiers, tente la réparation si nécessaire, et fait échouer le build si des erreurs de fond subsistent.

name: Validate Factur-X invoices

on:
  push:
    paths:
      - 'invoices/**'
  pull_request:
    paths:
      - 'invoices/**'

env:
  API_URL: https://api.facturxapi.com/api/v1

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install jq
        run: sudo apt-get install -y jq

      - name: Validate all invoices
        env:
          FACTURX_API_KEY: ${{ secrets.FACTURX_API_KEY }}
        run: |
          FAILED=0
          REPAIRED=0
          PASSED=0

          for file in invoices/*.{pdf,xml}; do
            [ -f "$file" ] || continue
            echo "--- Validation: $(basename "$file") ---"

            # Validation initiale
            RESULT=$(curl -s -X POST "${API_URL}/validate" \
              -H "X-API-Key: ${FACTURX_API_KEY}" \
              -F "file=@${file}" \
              --max-time 30)

            IS_VALID=$(echo "$RESULT" | jq -r '.valid')

            if [ "$IS_VALID" = "true" ]; then
              echo "✅ Conforme"
              PASSED=$((PASSED + 1))
              continue
            fi

            # Tentative de réparation (XML uniquement)
            if [[ "$file" == *.xml ]]; then
              echo "⚠️ Non conforme — tentative de réparation..."
              REPAIR=$(curl -s -X POST "${API_URL}/repair" \
                -H "X-API-Key: ${FACTURX_API_KEY}" \
                -F "file=@${file}" \
                --max-time 30)

              FIXES=$(echo "$REPAIR" | jq '.fixes | length')
              echo "  ${FIXES} correction(s) appliquée(s)"

              VALID_AFTER=$(echo "$REPAIR" | jq -r '.validation.valid // false')
              if [ "$VALID_AFTER" = "true" ]; then
                echo "✅ Conforme après réparation"
                REPAIRED=$((REPAIRED + 1))
                continue
              fi
            fi

            # Échec définitif
            echo "❌ Erreurs non réparables :"
            echo "$RESULT" | jq -r '.errors[] | "  [\(.severity)] \(.ruleId): \(.message)"'
            FAILED=$((FAILED + 1))
          done

          echo ""
          echo "=== Résumé ==="
          echo "Conformes: ${PASSED}"
          echo "Réparés:   ${REPAIRED}"
          echo "Échoués:   ${FAILED}"

          if [ "$FAILED" -gt 0 ]; then
            echo "::error::${FAILED} facture(s) non conforme(s)"
            exit 1
          fi

      - name: Notify on failure
        if: failure()
        uses: slackapi/[email protected]
        with:
          payload: |
            {
              "text": "🚨 Validation Factur-X échouée sur ${{ github.ref_name }}",
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "*Validation Factur-X échouée*\nBranche: `${{ github.ref_name }}`\nCommit: `${{ github.sha }}`\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Voir le rapport>"
                  }
                }
              ]
            }
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

Le secret FACTURX_API_KEY doit être configuré dans Settings > Secrets and variables > Actions du repository. Ne jamais commiter la clé API en clair dans le workflow.

GitLab CI : équivalent

validate-facturx:
  stage: test
  image: alpine:3.20
  before_script:
    - apk add --no-cache curl jq bash
  variables:
    API_URL: "https://api.facturxapi.com/api/v1"
  script:
    - |
      FAILED=0
      for file in invoices/*.pdf invoices/*.xml; do
        [ -f "$file" ] || continue
        RESULT=$(curl -s -X POST "${API_URL}/validate" \
          -H "X-API-Key: ${FACTURX_API_KEY}" \
          -F "file=@${file}" \
          --max-time 30)

        IS_VALID=$(echo "$RESULT" | jq -r '.valid')
        if [ "$IS_VALID" != "true" ]; then
          echo "ÉCHEC: $(basename "$file")"
          echo "$RESULT" | jq -r '.errors[] | "  [\(.severity)] \(.ruleId): \(.message)"'
          FAILED=$((FAILED + 1))
        else
          echo "OK: $(basename "$file")"
        fi
      done
      [ "$FAILED" -eq 0 ] || exit 1
  rules:
    - changes:
        - invoices/**

La variable FACTURX_API_KEY est définie dans Settings > CI/CD > Variables avec le flag Masked activé.

Gestion des erreurs dans le pipeline

Un pipeline robuste distingue trois catégories de résultat, chacune avec un comportement différent.

Code retourSignificationAction pipeline
0Document conformeContinuer vers la transmission
1Document non conforme (erreur métier)Bloquer + alerter l’équipe
2Erreur technique (réseau, API indisponible)Retry avec backoff, puis alerter

Idempotency et retries

Pour les appels de réparation en contexte de retry automatique, incluez un header Idempotency-Key pour éviter les doubles traitements.

IDEM_KEY="repair-$(sha256sum "$file" | cut -d' ' -f1)"

curl -X POST "${API_URL}/repair" \
  -H "X-API-Key: ${FACTURX_API_KEY}" \
  -H "Idempotency-Key: ${IDEM_KEY}" \
  -F "file=@${file}" \
  --max-time 30 \
  --retry 2 \
  --retry-delay 5

L’Idempotency-Key est construite à partir du hash SHA-256 du fichier. Contrairement à une clé basée sur le nom de fichier ou l’identifiant du run CI, le hash garantit que deux fichiers différents portant le même nom ne partagent pas la même clé, et qu’un même fichier retried produit bien la même clé. Si le même appel est rejoué (timeout réseau, runner interrompu), l’API retourne le résultat déjà calculé sans retraiter le fichier.

Timeout et fallback

Configurez un timeout explicite (--max-time 30) sur chaque appel API. Si l’API ne répond pas dans le délai, curl retourne un code d’erreur non-zéro qui déclenche le chemin d’erreur technique (code 2). Un pipeline qui attend indéfiniment une réponse API bloque tout le workflow de déploiement.

Webhook et alerting en production

Au-delà du CI/CD, les factures générées en production (par un job cron, un webhook ERP, un flux temps réel) nécessitent un circuit d’alerte dédié.

notify_failure() {
  local file="$1"
  local errors="$2"

  # Slack
  curl -s -X POST "${SLACK_WEBHOOK_URL}" \
    -H "Content-Type: application/json" \
    -d "{
      \"text\": \"Validation Factur-X échouée : ${file}\",
      \"attachments\": [{
        \"color\": \"danger\",
        \"text\": $(echo "$errors" | jq -Rs .)
      }]
    }"

  # Email via votre service (Brevo, SendGrid, etc.)
  curl -s -X POST "https://api.brevo.com/v3/smtp/email" \
    -H "api-key: ${BREVO_API_KEY}" \
    -H "Content-Type: application/json" \
    -d "{
      \"sender\": {\"email\": \"[email protected]\"},
      \"to\": [{\"email\": \"[email protected]\"}],
      \"subject\": \"[ALERTE] Factur-X non conforme : ${file}\",
      \"textContent\": \"Erreurs de validation :\\n${errors}\"
    }"
}

L’alerte doit contenir le nom du fichier, la liste des erreurs avec leurs codes BR-*, et un lien vers le rapport complet. Sans contexte suffisant, l’équipe comptable ne peut pas diagnostiquer le problème.

Aller plus loin

#CI/CD #factur-x #validation #automatisation #GitHub Actions #pipeline #EN16931