Skip to Content
Mélodium 0.10.3 is now available!
DocsExemplesTriage des échecs CI

Triage des échecs CI

Source: showcase/ci_failure_triage See in Playground

Deux étapes CI s’exécutent sur des conteneurs provisionnés, et la sortie brute de chaque étape est classifiée de façon déterministe par une petite fonction JavaScript avant toute autre chose. Seule l’étape que le classificateur signale effectivement comme échouée est confiée à un LLM distant pour un diagnostic en langage clair et une suggestion de correction ; l’étape jugée correcte par le classificateur ne coûte rien de plus que la classification elle-même, aucune requête LLM n’est faite pour elle.

Note

Nécessite un jeton API Mélodium Services et une clé API d’un fournisseur LLM. Placez MELODIUM_API_TOKEN dans l’environnement et exécutez avec --api-report ; voir Cadence.CI pour obtenir un jeton et suivre l’exécution.

Exécution

cd showcase/ci_failure_triage export MELODIUM_API_TOKEN="my-melodium-services-token" melodium run --api-report Compo.toml --api_key sk-...

Exécute deux étapes sur l’image standard python:3.13-slim, sans rien à cloner ni à installer :

  • unit_tests : trois assertions triviales, toujours vraies. Réussit toujours.
  • integration_check : lit un dictionnaire de configuration via une clé mal orthographiée ('tiemout' au lieu de 'timeout'). Échoue toujours avec une KeyError.

Les deux sont délibérément déterministes, si bien que cet exemple s’exécute de la même façon à chaque fois, indépendamment de l’état d’un dépôt externe quelconque. Voici à quoi ressemble ci_failure_report.md :

## unit_tests Status: **passed** Classification: `{"category":"none","status":"passed","summary":"no known failure signature found in the step output"}` No issues detected by the deterministic classifier; no AI analysis was requested for this step. ## integration_check Status: **failed** Classification: `{"category":"key_error","status":"failed","summary":"A dictionary key does not exist: tiemout"}` ## Root Cause The script contains a typo in the dictionary key lookup: `config['tiemout']` instead of `config['timeout']`. Python raises a `KeyError` because `'tiemout'` does not exist as a key in the `config` dictionary. The first `print` succeeds (retries prints correctly), but execution halts on the second `print` when it tries to access the misspelled key. ## Suggested Fix Correct the typo in the key name from `'tiemout'` to `'timeout'`: ```python # Before (broken) print('effective timeout:', config['tiemout']) # After (fixed) print('effective timeout:', config['timeout']) ``` This is a one-character transposition (`ie` to `ei`) and is the only change needed.

Fonctionnement

Un modèle JavaScriptEngine contient le classificateur déterministe, qui compare la sortie capturée d’une étape à une courte liste de signatures d’échec connues :

model Classifier() : JavaScriptEngine { code = ${{function classify(log) { var signatures = [ { re: /KeyError:\s*'([^']*)'/, category: "key_error", summary: "A dictionary key does not exist" }, { re: /TypeError:\s*(.+)/, category: "type_error", summary: "A type error occurred" }, { re: /ModuleNotFoundError|ImportError/, category: "dependency", summary: "A required module or dependency is missing" }, { re: /SyntaxError:\s*(.+)/, category: "syntax_error", summary: "A syntax error was found" }, { re: /AssertionError/, category: "assertion", summary: "An assertion failed" }, { re: /Traceback \(most recent call last\)/, category: "runtime_error", summary: "An unhandled exception occurred" } ]; for (var i = 0; i < signatures.length; i++) { var match = log.match(signatures[i].re); if (match) { var detail = match[1] ? (": " + match[1]) : ""; return { status: "failed", category: signatures[i].category, summary: signatures[i].summary + detail }; } } return { status: "passed", category: "none", summary: "no known failure signature found in the step output" }; } function statusOf(decision) { return decision.status; } }} }

Un unique RemoteLlm, Analyst, sert à expliquer et suggérer une correction, uniquement pour une étape déjà signalée comme échouée par le classificateur. Contrairement à showcase/smart_llm_router, il n’y a qu’un seul niveau ici, car le levier d’économie de cet exemple n’est pas « quel modèle » mais « faut-il appeler un modèle du tout ».

Capturer la sortie indépendamment du code de sortie

La commande réelle de chaque étape redirige vers un fichier et se termine par ; true, si bien que le conteneur sort toujours avec le code 0 et que le journal est toujours renvoyé en flux via data ; c’est ce traitement qui décide ensuite lui-même, à partir du texte capturé, si l’étape a réellement réussi, plutôt que de faire confiance au code de sortie du conteneur :

unitTests: ciStepAnalysis[dispatcher=dispatcher, classifier=classifier, analyst=analyst]( step_name = "unit_tests", image = "python:3.13-slim", commands = [|command("sh", ["-c", "python3 -c \"assert 1 + 1 == 2; assert 'melodium'.upper() == 'MELODIUM'; assert len([1, 2, 3]) == 3; print('all checks passed')\" > /mnt/data/log.txt 2>&1; true"])] ) integrationCheck: ciStepAnalysis[dispatcher=dispatcher, classifier=classifier, analyst=analyst]( step_name = "integration_check", image = "python:3.13-slim", commands = [|command("sh", ["-c", "python3 -c \"config = {'timeout': 30, 'retries': 3}; print('effective retries:', config['retries']); print('effective timeout:', config['tiemout'])\" > /mnt/data/log.txt 2>&1; true"])] )

Classifier avant de dépenser une requête

Le journal capturé est décodé, réduit à un Block<string>, transformé en Json, et transmis à la fonction JS classify() ; un second petit appel JS, statusOf(decision), ne projette que le champ status, le même pipeline JS en deux étapes que showcase/smart_llm_router :

classifyCall: process[engine=classifier](code="classify(value)") asJson.json -> classifyCall.value decisionOpt: unwrapOr<Json>(default=|null()) classifyCall.result -> decisionOpt.option

Deux portes equalTo plus filterBlock routent ensuite selon ce statut : failed fait passer le journal brut vers le prompt du LLM ; passed fait passer à la place un message fixe et gratuit :

isFailed: equalTo<string>(value="failed") statusLast.last -> isFailed.data gateFailedLog: filterBlock<string>() logLast.last -> gateFailedLog.value isFailed.result -> gateFailedLog.select diagnosisPrompt: stream<string>() gateFailedLog.accepted -> diagnosisPrompt.block diagnosis: llmChat[llm=analyst]() diagnosisPrompt.stream -> diagnosis.prompt
isPassed: equalTo<string>(value="passed") statusLast.last -> isPassed.data passedMessage: emit<string>(value="No issues detected by the deterministic classifier; no AI analysis was requested for this step.") Self.trigger -> passedMessage.trigger gatePassed: filterBlock<string>() passedMessage.emit -> gatePassed.value isPassed.result -> gatePassed.select

Exactement une seule porte transporte quelque chose, donc exactement un des deux cas, « appeler le LLM » ou « dire que rien n’est nécessaire », se produit, et fusionner les deux branches (majoritairement vides) ne fait que produire celle qui s’est déclenchée. chat, et non stream, est utilisé ici car il renvoie une réponse complète par prompt plutôt que des tokens au fur et à mesure, le bon choix quand le résultat va dans un rapport plutôt que vers une connexion en direct.

Assembler le rapport

La section de chaque étape (nom, statut, classification complète, et l’analyse ou le message fixe) est assemblée avec blockEntry/blockInsert/format, et les sections des deux étapes sont combinées de la même façon une fois de plus au niveau supérieur avant d’être écrites dans ci_failure_report.md :

sectionEntries: stream<StringMap>() sectionText: format(format="## {step}\n\nStatus: **{status}**\nClassification: `{classification}`\n\n{analysis}\n\n") sectionLast: trigger<string>() withAnalysis.map -> sectionEntries.block,stream -> sectionText.entries,formatted -> sectionLast.stream sectionLast.last -> Self.report

Dépendances

[dependencies] std = "0.10.3" # flux de base, journalisation, structures de données process = "0.10.3" # exécution de processus externes work = "0.10.3" # provisionnement de runners cloud cicd = "0.10.3" # dispatch et orchestration d'étapes CI/CD encoding = "0.10.3" # encodage / décodage UTF-8 json = "0.10.3" # analyse et sérialisation JSON javascript = "0.10.3" # moteur JavaScript embarqué ml = "0.10.3" # LLM, STT, TTS et inférence de modèles locaux fs = "0.10.3" # lecture/écriture de fichiers locaux