Douze. C’est le nombre de fois où un déploiement a été poussé en production avec une suite de tests partiellement rouge, sur un projet suivi pendant six mois, simplement parce que personne dans l’équipe n’avait ouvert l’onglet de la CI ce jour-là. Ce constat, remonté lors d’une revue de processus, a motivé la mise en place d’un indicateur visible directement là où l’équipe technique passe déjà du temps : le tableau de bord de l’administration WordPress.
Cet article ne traite pas de la configuration de la CI elle-même — GitHub Actions, GitLab CI ou tout autre outil équivalent fait déjà tourner la suite de tests PHPUnit ou Jest. L’objectif est de récupérer le résultat de la dernière exécution et de l’afficher sous forme de widget dans wp-admin/index.php, avec un code couleur qui saute aux yeux avant même de lire le chiffre.
Exposer un résumé exploitable en fin de pipeline
La plupart des runners de tests produisent un rapport JUnit XML ou un résumé JSON. Une étape finale du pipeline extrait les chiffres clés — nombre de tests, échecs, durée — et les publie vers un point de terminaison accessible au site WordPress.

Voici une étape finale qui s’exécute quelle que soit l’issue des tests (avec GitHub Actions, on lui donne la condition if: always() ; avec GitLab CI, when: always). Elle lit le rapport JUnit produit par PHPUnit avec l’option --log-junit, construit un petit document JSON et le dépose à une adresse que le site WordPress saura interroger :
#!/usr/bin/env bash
set -euo pipefail
RAPPORT="${1:-build/junit.xml}"
tests=$(xmllint --xpath 'sum(/testsuites/testsuite/@tests)' "$RAPPORT")
echecs=$(xmllint --xpath 'sum(/testsuites/testsuite/@failures)' "$RAPPORT")
erreurs=$(xmllint --xpath 'sum(/testsuites/testsuite/@errors)' "$RAPPORT")
duree=$(xmllint --xpath 'sum(/testsuites/testsuite/@time)' "$RAPPORT")
jq -n \
--argjson tests "${tests:-0}" \
--argjson echecs "$(( ${echecs:-0} + ${erreurs:-0} ))" \
--argjson duree "${duree:-0}" \
--arg branche "${CI_BRANCHE:-inconnue}" \
--arg commit "${CI_COMMIT:-inconnu}" \
--arg date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
'{tests: $tests, echecs: $echecs, duree: $duree, branche: $branche, commit: $commit, date: $date}' \
> resume-ci.json
curl --fail --silent --show-error \
--request PUT \
--header "Authorization: Bearer ${CI_JETON_PUBLICATION}" \
--header "Content-Type: application/json" \
--data @resume-ci.json \
"${CI_URL_RESUME}"
Le fichier produit est volontairement minuscule : quelques nombres, une branche, une date. Plus le contrat est petit, moins il risque de casser quand la CI change. Les variables CI_BRANCHE et CI_COMMIT sont à alimenter depuis les variables natives de votre outil de CI ; CI_URL_RESUME désigne un stockage statique ou un espace d’objets que vous maîtrisez, protégé par un jeton mis dans les secrets du pipeline, jamais dans le dépôt.
Un rapport JUnit signale séparément les échecs (une assertion fausse) et les erreurs (une exception inattendue). Pour un repérage immédiat, ces deux catégories sont additionnées : dans les deux cas, la suite n’est pas verte.
Récupérer le résumé côté WordPress
Côté site, une fonction interroge l’adresse avec wp_remote_get(), contrôle le code de réponse, décode le JSON et vérifie sa forme avant de s’en servir. Le résultat est mis en cache quelques minutes avec set_transient() : un tableau de bord rechargé dix fois de suite ne doit pas déclencher dix appels sortants. L’adresse et le jeton se définissent dans wp-config.php avec define().
function wpm_ci_recuperer_resume() {
$cache = get_transient( 'wpm_ci_resume' );
if ( false !== $cache ) {
return $cache;
}
if ( get_transient( 'wpm_ci_resume_erreur' ) ) {
return new WP_Error( 'wpm_ci_http', __( 'Le résumé de la CI est injoignable.', 'wpm-ci' ) );
}
if ( ! defined( 'WPM_CI_URL' ) || ! defined( 'WPM_CI_JETON' ) ) {
return new WP_Error( 'wpm_ci_config', __( "L'adresse du résumé n'est pas configurée.", 'wpm-ci' ) );
}
$reponse = wp_remote_get( WPM_CI_URL, array(
'timeout' => 5,
'headers' => array( 'Authorization' => 'Bearer ' . WPM_CI_JETON ),
) );
if ( is_wp_error( $reponse ) || 200 !== wp_remote_retrieve_response_code( $reponse ) ) {
set_transient( 'wpm_ci_resume_erreur', 1, 30 );
return new WP_Error( 'wpm_ci_http', __( 'Le résumé de la CI est injoignable.', 'wpm-ci' ) );
}
$donnees = json_decode( wp_remote_retrieve_body( $reponse ), true );
if ( ! is_array( $donnees ) || ! isset( $donnees['tests'], $donnees['echecs'], $donnees['date'] ) ) {
return new WP_Error( 'wpm_ci_format', __( 'Le résumé de la CI a un format inattendu.', 'wpm-ci' ) );
}
set_transient( 'wpm_ci_resume', $donnees, 2 * MINUTE_IN_SECONDS );
return $donnees;
}
Le transient wpm_ci_resume_erreur mémorise une panne pendant trente secondes : tant qu’il existe, la fonction renvoie l’erreur sans relancer d’appel sortant. Le délai de cinq secondes (timeout) évite qu’un service indisponible ne bloque l’affichage de tout le tableau de bord.
Afficher le widget et colorer l’état
L’enregistrement passe par wp_add_dashboard_widget(), appelée depuis le crochet wp_dashboard_setup. Le widget n’est proposé qu’aux utilisateurs qui ont la capacité manage_options : inutile d’exposer l’état technique du projet à tous les rédacteurs.
add_action( 'wp_dashboard_setup', function () {
if ( current_user_can( 'manage_options' ) ) {
wp_add_dashboard_widget(
'wpm_ci_widget',
__( 'Dernière exécution des tests', 'wpm-ci' ),
'wpm_ci_afficher_widget'
);
}
} );
function wpm_ci_etat( array $resume ) {
$age = time() - (int) strtotime( $resume['date'] );
if ( (int) $resume['echecs'] > 0 ) {
return array( '#d63638', __( 'En échec', 'wpm-ci' ) );
}
if ( $age > DAY_IN_SECONDS ) {
return array( '#dba617', __( 'Résultat ancien', 'wpm-ci' ) );
}
return array( '#00a32a', __( 'Succès', 'wpm-ci' ) );
}
function wpm_ci_afficher_widget() {
$resume = wpm_ci_recuperer_resume();
if ( is_wp_error( $resume ) ) {
echo '<p>' . esc_html( $resume->get_error_message() ) . '</p>';
return;
}
list( $couleur, $libelle ) = wpm_ci_etat( $resume );
printf(
'<div style="border-left:4px solid %1$s;padding-left:12px">'
. '<p style="font-size:22px;margin:0"><strong>%2$s</strong></p>'
. '<p style="margin:4px 0">%3$s</p>'
. '<p class="description">%4$s</p></div>',
esc_attr( $couleur ),
esc_html( $libelle ),
esc_html( sprintf(
/* translators: 1: nombre d'échecs, 2: nombre de tests. */
__( '%1$d échec(s) sur %2$d tests', 'wpm-ci' ),
(int) $resume['echecs'],
(int) $resume['tests']
) ),
esc_html( sprintf(
/* translators: 1: branche, 2: durée écoulée. */
__( 'Branche %1$s, il y a %2$s', 'wpm-ci' ),
isset( $resume['branche'] ) ? $resume['branche'] : '?',
human_time_diff( (int) strtotime( $resume['date'] ) )
) )
);
}
Trois états suffisent : le rouge pour au moins un échec, l’orange pour un résultat de plus de vingt-quatre heures (signe que la CI ne tourne plus, ce qui est aussi une mauvaise nouvelle), le vert pour un résultat récent et sans échec. Le libellé textuel accompagne toujours la couleur : une personne daltonienne, ou un lecteur d’écran, n’a pas à deviner l’état d’après la teinte. Les valeurs #d63638, #dba617 et #00a32a reprennent la palette de l’administration. Toutes les données venant d’un service externe passent par esc_html() ou esc_attr() avant d’être affichées.
Pièges et limites
- Un résumé périmé vaut mieux qu’un faux vert. Sans contrôle de l’âge, un pipeline désactivé laisserait indéfiniment un « Succès » affiché ; d’où l’état orange.
- Le secret ne doit jamais apparaître côté navigateur. L’appel est fait par le serveur WordPress, pas par du JavaScript dans le tableau de bord.
- Les échecs sporadiques ne se voient pas dans un simple compteur. Ce widget signale un état, il ne remplace pas l’historique de la CI pour comprendre un test instable.
- Un site en multisite ou plusieurs environnements demande une adresse par projet, sous peine d’afficher le résultat d’une autre branche.
Cas concret
Sur le projet évoqué en introduction, le pipeline déposait son résumé après chaque exécution sur la branche principale. Le tableau de bord affichait le résultat avant même que l’on ouvre l’écran de déploiement, et la règle d’équipe est devenue simple : pas de mise en production tant que le widget n’est pas vert. L’outil ne change rien à la qualité des tests, mais il supprime l’excuse de ne pas avoir regardé.
Un indicateur qu’il faut aller chercher ne sert qu’aux gens déjà vigilants ; placez-le sur la page que tout le monde ouvre chaque matin.
Conclusion
Le dispositif tient en trois morceaux : un script de fin de pipeline qui publie un JSON minuscule, une fonction qui le récupère avec cache et validation, un widget qui l’affiche avec une couleur et un libellé. Ajoutez le contrôle de péremption et la restriction de capacité, et vous obtenez un repère fiable sans dépendance lourde. C’est un exemple typique de ce qui rend un tableau de bord WordPress utile à l’équipe technique : montrer ce qui compte, là où les gens regardent déjà.