Modèle de révision de code Gitlab

Avancé

Ceci est unAI Summarization, Multimodal AIworkflow d'automatisation du domainecontenant 41 nœuds.Utilise principalement des nœuds comme If, Set, Code, Jira, Merge. Automatisation de la révision de code pour les demandes de fusion GitLab avec Gemini AI et contexte JIRA

Prérequis
  • Point de terminaison HTTP Webhook (généré automatiquement par n8n)
  • Peut nécessiter les informations d'identification d'authentification de l'API cible
  • Clé API Google Gemini
Aperçu du workflow
Visualisation des connexions entre les nœuds, avec support du zoom et du déplacement
Exporter le workflow
Copiez la configuration JSON suivante dans n8n pour importer et utiliser ce workflow
{
  "id": "Rwpn5OG2ql8rIOzH",
  "meta": {
    "instanceId": "ebde2b8e011b15f5db59a3b84e187c4795dbe1d4b2c695e9940aa4dbcd102f98",
    "templateCredsSetupCompleted": true
  },
  "name": "Gitlab Code Review Template",
  "tags": [],
  "nodes": [
    {
      "id": "c3fedf3c-ed52-49d7-b9d5-30050d4324f2",
      "name": "À réviser",
      "type": "n8n-nodes-base.if",
      "position": [
        -4256,
        1260
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "617eb2c5-dd4b-4e28-b533-0c32ea6ca961",
              "operator": {
                "name": "filter.operator.equals",
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.body.object_attributes.note }}",
              "rightValue": "coro-bot-review"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "d30412f1-f714-41cb-a867-3a45f202c378",
      "name": "Ignorer les modifications de fichiers",
      "type": "n8n-nodes-base.if",
      "position": [
        -2016,
        1408
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "c6e1430b-84a7-47ce-8fe9-7b94da0f2d31",
              "operator": {
                "type": "boolean",
                "operation": "false",
                "singleValue": true
              },
              "leftValue": "={{ $json.changes.renamed_file }}",
              "rightValue": ""
            },
            {
              "id": "bf6e9eb9-d72d-459c-a722-9614bab8842c",
              "operator": {
                "type": "boolean",
                "operation": "false",
                "singleValue": true
              },
              "leftValue": "={{ $json.changes.deleted_file }}",
              "rightValue": ""
            },
            {
              "id": "03200577-a262-4f46-ad25-9c15b0c8146d",
              "operator": {
                "type": "string",
                "operation": "startsWith"
              },
              "leftValue": "={{ $json.changes.diff }}",
              "rightValue": "@@"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "17752d5e-75d2-43ff-8e1b-ff439e6329b8",
      "name": "Chaîne LLM de base",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        -1344,
        1104
      ],
      "parameters": {
        "text": "=First, consider the context from the associated JIRA ticket:\n------------\n{{ $json.jiraParentContext || $json.jiraContext || 'No JIRA context was provided.' }}\n------------\n\nFile path:{{ $('Skip File Changes').item.json.new_path }}\n\n```Original code\n {{ $json.originalCode }}\n```\nchange to\n```New code\n {{ $json.newCode }}\n```\nPlease review the code changes in this section:",
        "messages": {
          "messageValues": [
            {
              "message": "=You are an automated code review bot. Your primary goal is to identify high-value issues in code.\n\n**Instructions:**\n\n1. **Analysis Focus:** Concentrate only on business logic, correctness, security, and performance. Ignore style, naming, filenames, version bumps, and non-source files. Assume placeholders for yml files will be filled with correct values.\n2. **Output for Issues:** When you find an issue, begin your response with the prefix `🤖 **AI Review:** `. Follow it with a concise, actionable explanation. Prefer one-liners that are immediately actionable - what or where or why and how to fix.\n3. **Output for No Issues:** If your analysis finds **zero** issues worth reporting, your **ENTIRE** response must be the single keyword: `ALL_CLEAR`. Do not add any other text or explanation.\n4. ** Findings to ignore in yaml files:**\n    - Ignore if a value for vaultSecretPath in a values file or a yaml contains a double slash (//)\n    - Ignore errors where tag appears to be very or too specific\n\n**Important Rules:**\n\n* NEVER write praise, summaries, or conversational phrases like \"I have no findings.\"\n* If you find issues, start every comment with the `🤖 **AI Review:** ` prefix.\n* If you find no issues, don't output anything."
            }
          ]
        },
        "promptType": "define"
      },
      "typeVersion": 1.5
    },
    {
      "id": "483ec727-0b0b-46a7-9bd9-fbd52f2bb51a",
      "name": "Google Gemini Modèle de chat",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "position": [
        -1272,
        1328
      ],
      "parameters": {
        "options": {
          "topK": 1,
          "temperature": 0
        },
        "modelName": "models/gemini-2.5-pro"
      },
      "credentials": {
        "googlePalmApi": {
          "id": "GeKFUmcR07GOvVpv",
          "name": "Google Gemini(PaLM) Api account 2"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "06884594-3ad3-4329-b357-21359eb13503",
      "name": "Extraire l'ID d'issue JIRA",
      "type": "n8n-nodes-base.code",
      "position": [
        -3584,
        1304
      ],
      "parameters": {
        "jsCode": "// Get the MR description from the previous node's input\nconst description = $input.first().json.description\n// Regex to find a JIRA issue key (e.g., PROJ-123).\n// The \\b ensures it matches a whole word.\nconst jiraRegex = /\\b([A-Z]+-\\d+)\\b/;\nconst match = description.match(jiraRegex);\n\n// If a key is found, return it.\nif (match && match[0]) {\n  // We name the output 'jiraIssueKey' for clarity.\n  return [{\n    json: {\n      jiraIssueKey: match[0]\n    }\n  }];\n} else {\n  // Return an empty array to stop this path if no key is found.\n  return [];\n}"
      },
      "typeVersion": 2
    },
    {
      "id": "24204fee-6c47-4026-a6a6-12ced75a49da",
      "name": "Obtenir l'issue JIRA",
      "type": "n8n-nodes-base.jira",
      "position": [
        -3360,
        1304
      ],
      "parameters": {
        "issueKey": "={{ $json.jiraIssueKey }}",
        "operation": "get",
        "additionalFields": {}
      },
      "credentials": {
        "jiraSoftwareCloudApi": {
          "id": "T58uHg2XK8A2NA7s",
          "name": "Jira SW Cloud account"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "7b1da623-5452-4783-ba8f-77896d3c8ea9",
      "name": "Formater le contexte JIRA",
      "type": "n8n-nodes-base.set",
      "position": [
        -2688,
        1452
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "59f4091a-0260-4f24-aa8d-c211f7c243e2",
              "name": "jiraContext",
              "type": "string",
              "value": "=JIRA Ticket Context: \nTitle: {{ $json.fields.summary }} \nType: {{ $json.fields.issuetype.name }} \nDescription: {{ $json.fields.description }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "2ac7ced8-60b5-4fab-902f-bd1360e41a3a",
      "name": "Extraire les détails de la MR",
      "type": "n8n-nodes-base.set",
      "position": [
        -3808,
        1356
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "59f4091a-0260-4f24-aa8d-c211f7c243e2",
              "name": "projectId",
              "type": "string",
              "value": "={{ $json.body.project_id }}"
            },
            {
              "id": "cc5bdd4b-812f-461d-b3a9-059994176291",
              "name": "iid",
              "type": "string",
              "value": "={{ $json.body.merge_request.iid }}"
            },
            {
              "id": "7afeacf0-888f-4e75-be3c-d024cd223e3b",
              "name": "description",
              "type": "string",
              "value": "={{ $json.body.merge_request.description }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "9aeed83e-1f0b-461a-8e2b-75efb7be5ca5",
      "name": "Si sous-tâche JIRA",
      "type": "n8n-nodes-base.if",
      "position": [
        -3136,
        1260
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "5338bc5b-9f30-4b66-b6a2-5b85dc012765",
              "operator": {
                "type": "object",
                "operation": "notEmpty",
                "singleValue": true
              },
              "leftValue": "={{ $json.fields.parent }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "bf6f14f1-b684-4100-9085-f97399c8911f",
      "name": "Obtenir l'issue parente JIRA",
      "type": "n8n-nodes-base.jira",
      "position": [
        -2912,
        1260
      ],
      "parameters": {
        "issueKey": "={{ $json.fields.parent.key }}",
        "operation": "get",
        "additionalFields": {}
      },
      "credentials": {
        "jiraSoftwareCloudApi": {
          "id": "T58uHg2XK8A2NA7s",
          "name": "Jira SW Cloud account"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "5e547d2f-c978-42bc-acb2-25c194308154",
      "name": "Formater le contexte parent JIRA",
      "type": "n8n-nodes-base.set",
      "position": [
        -2688,
        1260
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "59f4091a-0260-4f24-aa8d-c211f7c243e2",
              "name": "jiraParentContext",
              "type": "string",
              "value": "=JIRA Parent Ticket Context: \nTitle: {{ $json.fields.summary }} \nType: {{ $json.fields.issuetype.name }} \nDescription: {{ $json.fields.description }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "d5e3ffd7-03d0-4b54-ab34-67fe43830531",
      "name": "Fusionner",
      "type": "n8n-nodes-base.merge",
      "position": [
        -2464,
        1392
      ],
      "parameters": {
        "mode": "combine",
        "options": {
          "includeUnpaired": true
        },
        "combineBy": "combineByPosition",
        "numberInputs": 3
      },
      "typeVersion": 3.2
    },
    {
      "id": "a49bf76b-1a4b-4aba-b392-419ddc07e110",
      "name": "Obtenir les modifications de la MR",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -2688,
        1644
      ],
      "parameters": {
        "url": "=https://gitlab.com/api/v4/projects/{{ $json.projectId }}/merge_requests/{{ $json.iid }}/changes",
        "options": {},
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "PRIVATE-TOKEN",
              "value": "={{$env.GITLAB_TOKEN}}"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "8a3beea4-640b-4ddc-a801-719ace2643d0",
      "name": "Préparer les modifications de code",
      "type": "n8n-nodes-base.code",
      "position": [
        -1568,
        1408
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nvar diff = $input.item.json.gitDiff\n\nlet lines = diff.trimEnd().split('\\n');\n\nlet originalCode = '';\nlet newCode = '';\n\nlines.forEach(line => {\n  console.log(line)\n    if (line.startsWith('-')) {\n        originalCode += line + \"\\n\";\n    } else if (line.startsWith('+')) {\n        newCode += line + \"\\n\";\n    } else {\n        originalCode += line + \"\\n\";\n        newCode += line + \"\\n\";\n    }\n});\n\nreturn { ...$json, originalCode, newCode };\n\n"
      },
      "typeVersion": 2
    },
    {
      "id": "057832e9-fa3b-4fe5-a561-78b69e60c813",
      "name": "Déclencheur d'erreur",
      "type": "n8n-nodes-base.errorTrigger",
      "position": [
        352,
        1692
      ],
      "parameters": {},
      "executeOnce": false,
      "retryOnFail": false,
      "typeVersion": 1,
      "alwaysOutputData": false
    },
    {
      "id": "aeba4982-b0fc-4d97-a06b-35b559135361",
      "name": "Agréger",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        -768,
        1408
      ],
      "parameters": {
        "options": {},
        "aggregate": "aggregateAllItemData"
      },
      "typeVersion": 1
    },
    {
      "id": "8ff7eb74-fbcc-4d4f-8b53-50ed4d543a82",
      "name": "Fusionner la sortie LLM avec l'entrée",
      "type": "n8n-nodes-base.merge",
      "position": [
        -992,
        1408
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineByPosition"
      },
      "typeVersion": 3.2
    },
    {
      "id": "39f6aa6b-3296-47e6-b71d-59bcc7efd34c",
      "name": "Filtrer les champs non pertinents",
      "type": "n8n-nodes-base.set",
      "position": [
        -1280,
        1504
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "f40092dc-5c3a-4882-b33c-29e8c50f90a6",
              "name": "iid",
              "type": "string",
              "value": "={{$json.iid}}"
            },
            {
              "id": "38bd9645-0f83-4b66-9850-403e675a360d",
              "name": "project_id",
              "type": "string",
              "value": "={{$json.project_id}}"
            },
            {
              "id": "6b432472-ad72-4eaf-977d-9b2c80cbe2bf",
              "name": "diff_refs",
              "type": "object",
              "value": "={{$json.diff_refs}}"
            },
            {
              "id": "d24732f4-3f12-4b77-8df1-f34edb0d7f4d",
              "name": "lastNewLine",
              "type": "string",
              "value": "={{ $json.lastNewLine }}"
            },
            {
              "id": "4baecc19-56b2-472e-a015-bfc2e36727ce",
              "name": "lastOldLine",
              "type": "string",
              "value": "={{ $json.lastOldLine }}"
            },
            {
              "id": "12489e32-3521-4fd1-af78-dcaaba028ee1",
              "name": "new_path",
              "type": "string",
              "value": "={{ $json.changes.new_path }}"
            },
            {
              "id": "602b5de5-aeab-43bf-8ccb-2fce3853278c",
              "name": "old_path",
              "type": "string",
              "value": "={{ $json.changes.old_path }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "ebe94dfb-7395-4788-896d-14c3d5380c18",
      "name": "Des problèmes détectés ?",
      "type": "n8n-nodes-base.if",
      "position": [
        -320,
        1408
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "70dd215a-813b-4bcf-9fa6-db4581ccad77",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.noIssuesFound }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "21e1a59a-3af6-45ba-83d8-73260736b92a",
      "name": "Séparer les modifications",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        -2240,
        1408
      ],
      "parameters": {
        "include": "selectedOtherFields",
        "options": {
          "includeBinary": true
        },
        "fieldToSplitOut": "changes",
        "fieldsToInclude": "jiraParentContext, jiraContext,iid,project_id,diff_refs"
      },
      "typeVersion": 1
    },
    {
      "id": "43acb79d-02c5-406b-9ae0-288e365518fa",
      "name": "Séparer les commentaires",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        -96,
        1428
      ],
      "parameters": {
        "include": "selectedOtherFields",
        "options": {
          "includeBinary": true
        },
        "fieldToSplitOut": "commentsToPost",
        "fieldsToInclude": "iid,project_id,diff_refs"
      },
      "typeVersion": 1
    },
    {
      "id": "95e75bb0-94c4-492d-af8f-e6d79f442d92",
      "name": "Préparer la requête",
      "type": "n8n-nodes-base.code",
      "position": [
        128,
        1428
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\n\n// Reference the nested 'commentsToPost' object\nconst postData = item.commentsToPost;\n\n// Create the nested position object\nconst position = {\n  position_type: 'text',\n  old_path: postData.old_path,\n  new_path: postData.new_path,\n  base_sha: postData.diff_refs.base_sha,\n  start_sha: postData.diff_refs.start_sha,\n  head_sha: postData.diff_refs.head_sha,\n};\n\n// Conditionally add the line numbers to the nested position object\nif (postData.lastNewLine !== null && postData.lastNewLine !== '') {\n  position.new_line = parseInt(postData.lastNewLine, 10);\n}\nif (postData.lastOldLine !== null && postData.lastOldLine !== '') {\n  position.old_line = parseInt(postData.lastOldLine, 10);\n}\n\n// Build the final request body with the nested 'position' key\nconst requestBody = {\n  body: postData.text,\n  position: position,\n};\n\n// Attach the final body for the next node\nitem.requestBody = requestBody;\n\nreturn item;"
      },
      "typeVersion": 2
    },
    {
      "id": "4e2ab9f1-0b12-4a25-8f2c-981500889660",
      "name": "Préparer la requête sans position",
      "type": "n8n-nodes-base.code",
      "position": [
        576,
        1500
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\n\n// Reference the nested 'commentsToPost' object\nconst postData = item.commentsToPost;\n\n// Build the final request body with the nested 'position' key\nconst requestBody = {\n  body: postData.text,\n};\n\n// Attach the final body for the next node\nitem.requestBody = requestBody;\n\nreturn item;"
      },
      "typeVersion": 2
    },
    {
      "id": "975691b1-9667-4913-aa56-bc177e202dae",
      "name": "Définir les informations d'exécution du workflow",
      "type": "n8n-nodes-base.code",
      "position": [
        -4032,
        1260
      ],
      "parameters": {
        "jsCode": "// initialize staticData object\nconst workflowStaticData = $getWorkflowStaticData('global');\nconst executionId = $execution.id\nconst projectId = $input.first().json[\"body\"][\"project_id\"]\nconst mrId = $input.first().json[\"body\"][\"merge_request\"][\"iid\"]\n\nworkflowStaticData[executionId] = {\"projectId\":projectId, \"mrId\": mrId}\n\nreturn $input.all();"
      },
      "typeVersion": 2
    },
    {
      "id": "a24362b0-e288-47af-9756-eff4acde1682",
      "name": "Publier 'Revue démarrée'",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -3808,
        1164
      ],
      "parameters": {
        "url": "=https://gitlab.com/api/v4/projects/{{ $json.body.project_id }}/merge_requests/{{ $json.body.merge_request.iid }}/notes",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "sendHeaders": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "body",
              "value": "🤖 AI code review initiated. This may take up to 30 minutes for large merge requests. I'll post my findings as comments on the relevant files."
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "PRIVATE-TOKEN",
              "value": "={{$env.GITLAB_TOKEN}}"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "81762b68-7a5d-4f03-8479-c209d2696970",
      "name": "Écouter les commentaires Gitlab",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -4480,
        1260
      ],
      "webhookId": "REPLACE_WITH_UNIQUE_ID",
      "parameters": {
        "path": "REPLACE_WITH_UNIQUE_PATH",
        "options": {},
        "httpMethod": "POST"
      },
      "typeVersion": 2
    },
    {
      "id": "7ed4ec35-2b23-4b97-ba7d-f434ff54b93c",
      "name": "Analyser le diff",
      "type": "n8n-nodes-base.code",
      "position": [
        -1792,
        1408
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const parseLastDiff = (gitDiff) => {\n  gitDiff = gitDiff.replace(/\\n\\\\ No newline at end of file/, '')\n  \n  const diffList = gitDiff.trimEnd().split('\\n').reverse();\n  const lastLineFirstChar = diffList?.[0]?.[0];\n  const lastDiff =\n    diffList.find((item) => {\n      return /^@@ \\-\\d+,\\d+ \\+\\d+,\\d+ @@/g.test(item);\n    }) || '';\n\n  const [lastOldLineCount, lastNewLineCount] = lastDiff\n    .replace(/@@ \\-(\\d+),(\\d+) \\+(\\d+),(\\d+) @@.*/g, ($0, $1, $2, $3, $4) => {\n      return `${+$1 + +$2},${+$3 + +$4}`;\n    })\n    .split(',');\n  \n  if (!/^\\d+$/.test(lastOldLineCount) || !/^\\d+$/.test(lastNewLineCount)) {\n    return {\n      lastOldLine: -1,\n      lastNewLine: -1,\n      gitDiff,\n    };\n  }\n\n\n  const lastOldLine = lastLineFirstChar === '+' ? null : (parseInt(lastOldLineCount) || 0) - 1;\n  const lastNewLine = lastLineFirstChar === '-' ? null : (parseInt(lastNewLineCount) || 0) - 1;\n\n  return {\n    lastOldLine,\n    lastNewLine,\n    gitDiff,\n  };\n};\n\nconst extra = parseLastDiff($json.changes.diff);\nreturn { ...$json, ...extra };"
      },
      "typeVersion": 2
    },
    {
      "id": "601bc26b-1349-499c-a4eb-f08e3a064503",
      "name": "Publier 'Aucun problème trouvé'",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        800,
        1212
      ],
      "parameters": {
        "url": "=https://gitlab.com/api/v4/projects/{{ $json.projectId }}/merge_requests/{{ $json.mrId }}/notes",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "sendHeaders": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "body",
              "value": "=🤖 AI review complete. No significant issues were found. LGTM!"
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "PRIVATE-TOKEN",
              "value": "={{$env.GITLAB_TOKEN}}"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "aa200c64-05c9-4437-8f18-980642e4ec6c",
      "name": "Publier 'Une erreur est survenue'",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        800,
        1692
      ],
      "parameters": {
        "url": "=https://gitlab.com/api/v4/projects/{{ $json.projectId }}/merge_requests/{{ $json.mrId }}/notes",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "sendHeaders": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "body",
              "value": "=🤖 An error occurred during the code review: {{ $json.error }}. Please trigger the review again."
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "PRIVATE-TOKEN",
              "value": "={{$env.GITLAB_TOKEN}}"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "e108c8b4-e8bb-4c43-aa01-0aa2cb70e83c",
      "name": "Obtenir le contexte statique incluant l'erreur",
      "type": "n8n-nodes-base.code",
      "position": [
        576,
        1692
      ],
      "parameters": {
        "jsCode": "const workflowStaticData = $getWorkflowStaticData('global');\nconst executionId = $input.first().json.execution.id;\nconst errorMessage = $input.first().json.execution.error.message;\n\nreturn {...workflowStaticData[executionId], error: errorMessage};"
      },
      "typeVersion": 2
    },
    {
      "id": "1afcd3cf-8d54-4e11-8518-a82f246023ec",
      "name": "Obtenir le contexte statique",
      "type": "n8n-nodes-base.code",
      "position": [
        576,
        1212
      ],
      "parameters": {
        "jsCode": "const workflowStaticData = $getWorkflowStaticData('global');\nconst executionId = $execution.id;\nreturn workflowStaticData[executionId];"
      },
      "typeVersion": 2
    },
    {
      "id": "21c6891d-1a57-4c4b-b7c4-41d84b20b744",
      "name": "Nettoyer le contexte statique",
      "type": "n8n-nodes-base.code",
      "position": [
        1024,
        1504
      ],
      "parameters": {
        "jsCode": "const workflowStaticData = $getWorkflowStaticData('global');\nconst executionId = $execution.id;\ndelete workflowStaticData[executionId];\nreturn [];"
      },
      "typeVersion": 2
    },
    {
      "id": "1d97e536-a1c5-4520-a078-fb06baa377c4",
      "name": "Traiter les commentaires",
      "type": "n8n-nodes-base.code",
      "position": [
        -544,
        1408
      ],
      "parameters": {
        "jsCode": "const allItems = $input.first().json.data;\n\n// Filter for items that have actual review text\nconst commentsToPost = allItems.filter(item => {\n  const text = item.text || '';\n  const isNotEmpty = text.trim() !== '';\n  const isNotClear = !text.includes('ALL_CLEAR');\n  const isNotNoFindings = !text.toLowerCase().includes('no findings');\n  const isNotNoIssues = !text.toLowerCase().includes('no issues found');\n  return isNotEmpty && isNotClear && isNotNoFindings && isNotNoIssues;\n});\n\nreturn [{\n  json: {\n    commentsToPost: commentsToPost,\n    noIssuesFound: commentsToPost.length === 0\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "db40d370-1216-4f36-9d87-86f26524e4cd",
      "name": "Publier les commentaires MR Gitlab",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueErrorOutput",
      "position": [
        352,
        1428
      ],
      "parameters": {
        "url": "=https://gitlab.com/api/v4/projects/{{ $json.commentsToPost.project_id }}/merge_requests/{{ $json.commentsToPost.iid }}/discussions",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ $json.requestBody }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "PRIVATE-TOKEN",
              "value": "={{$env.GITLAB_TOKEN}}"
            }
          ]
        }
      },
      "retryOnFail": false,
      "typeVersion": 4.2
    },
    {
      "id": "eca87aa8-3d57-4513-a2d0-98d8cfcd1174",
      "name": "Essayer !",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -5264,
        1072
      ],
      "parameters": {
        "color": 6,
        "width": 640,
        "height": 288,
        "content": "### ⭐️ Try It Out!\n\nWelcome! This template adds AI-powered code reviews to your GitLab Merge Requests. Simply comment **`ai-review`** on any MR and watch the workflow:\n\n- Captures the comment event.\n- Fetches diff & optional JIRA context.\n- Asks an LLM for insights.\n- Posts inline comments or an 'all clear' note.\n\nPerfect for catching logic, security and performance issues fast."
      },
      "typeVersion": 1
    },
    {
      "id": "9bf7c660-9b1d-430c-95e9-37e89aae852d",
      "name": "Étape 1 : Écouter & Capturer",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -4480,
        1488
      ],
      "parameters": {
        "color": 2,
        "width": 600,
        "height": 232,
        "content": "### 🟢 Step 1: Listen & Capture\n\nA Webhook node listens for merge request note events. When the comment text matches your trigger (default: `ai-review`), it:\n\n- Extracts the Project & MR IDs and stores them in a **Static Context** (see below) so later nodes always know where to post results.\n- Posts a short \"AI review started\" message back to the MR to let reviewers know the bot is working."
      },
      "typeVersion": 1
    },
    {
      "id": "f2e6745f-cb17-4d0c-a5c9-c3031759100a",
      "name": "Étape 2 : Préparer & Réviser",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2432,
        960
      ],
      "parameters": {
        "color": 4,
        "width": 600,
        "height": 280,
        "content": "### 🟣 Step 2: Prepare & Review\n\nThe workflow fetches the merge request changes via the GitLab API and parses the diff into original vs new code blocks.\n\nIf the MR description contains a Jira key, the workflow fetches the issue summary and, if it's a subtask, also fetches the parent ticket for extra context. Both summaries are combined into the prompt.\n\nAn LLM (Gemini by default) is prompted to find critical issues only—logic bugs, security flaws and performance problems—and to return comments with file and line numbers. If no issues exist, the model returns `ALL_CLEAR`."
      },
      "typeVersion": 1
    },
    {
      "id": "1bd4a994-6826-4b49-87a8-9214508b94c8",
      "name": "Étape 3 : Publier & Solution de repli",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1232,
        1456
      ],
      "parameters": {
        "color": 5,
        "width": 600,
        "height": 184,
        "content": "### 🟠 Step 3: Post & Fallback\n\nReview comments are converted into GitLab discussion payloads and posted back to the MR at the correct file and line positions.\n\nIf a comment’s position is not calculated properly, the workflow falls back to posting a comment as a thread at the MR level so nothing is lost."
      },
      "typeVersion": 1
    },
    {
      "id": "2d50734d-8c63-4a52-8459-2719710ca2a6",
      "name": "Contexte statique & Gestion des erreurs",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -96,
        1904
      ],
      "parameters": {
        "width": 600,
        "height": 216,
        "content": "### 🔵 Static Context & Error Handling\n\nWorkflow Static Data is used to store persistent values—like the project ID and the MR ID so that they will be available across branches.\n\nThis is critical for the Error Trigger: if something fails, the error path can still post to the correct MR using these stored IDs. Without static context, error comments wouldn’t know where to go."
      },
      "typeVersion": 1
    },
    {
      "id": "223b57bd-6ea3-4e32-ad29-9fec88550b1b",
      "name": "Besoin d'aide & Personnaliser",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1504,
        1744
      ],
      "parameters": {
        "color": 7,
        "width": 600,
        "height": 168,
        "content": "### 🟡 Need Help & Customise\n\n- Change the trigger word in the **IF** node to match your own.\n- Swap in your preferred LLM or adjust the prompt to suit your guidelines.\n- Filter by file type or exclude certain directories in the parsing logic."
      },
      "typeVersion": 1
    },
    {
      "id": "917539f7-c7a2-48a4-9227-93287eeeded9",
      "name": "Publier les commentaires MR Gitlab sans position",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        800,
        1500
      ],
      "parameters": {
        "url": "=https://gitlab.com/api/v4/projects/{{ $json.commentsToPost.project_id }}/merge_requests/{{ $json.commentsToPost.iid }}/discussions",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ $json.requestBody }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "PRIVATE-TOKEN",
              "value": "={{$env.GITLAB_TOKEN}}"
            }
          ]
        }
      },
      "retryOnFail": false,
      "typeVersion": 4.2
    }
  ],
  "active": false,
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "0798cf1c-bb7b-4765-bf7e-7dead605266f",
  "connections": {
    "d5e3ffd7-03d0-4b54-ab34-67fe43830531": {
      "main": [
        [
          {
            "node": "21e1a59a-3af6-45ba-83d8-73260736b92a",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "aeba4982-b0fc-4d97-a06b-35b559135361": {
      "main": [
        [
          {
            "node": "1d97e536-a1c5-4520-a078-fb06baa377c4",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "7ed4ec35-2b23-4b97-ba7d-f434ff54b93c": {
      "main": [
        [
          {
            "node": "8a3beea4-640b-4ddc-a801-719ace2643d0",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "c3fedf3c-ed52-49d7-b9d5-30050d4324f2": {
      "main": [
        [
          {
            "node": "975691b1-9667-4913-aa56-bc177e202dae",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "057832e9-fa3b-4fe5-a561-78b69e60c813": {
      "main": [
        [
          {
            "node": "e108c8b4-e8bb-4c43-aa01-0aa2cb70e83c",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "24204fee-6c47-4026-a6a6-12ced75a49da": {
      "main": [
        [
          {
            "node": "7b1da623-5452-4783-ba8f-77896d3c8ea9",
            "type": "main",
            "index": 0
          },
          {
            "node": "9aeed83e-1f0b-461a-8e2b-75efb7be5ca5",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "a49bf76b-1a4b-4aba-b392-419ddc07e110": {
      "main": [
        [
          {
            "node": "d5e3ffd7-03d0-4b54-ab34-67fe43830531",
            "type": "main",
            "index": 2
          }
        ]
      ]
    },
    "17752d5e-75d2-43ff-8e1b-ff439e6329b8": {
      "main": [
        [
          {
            "node": "8ff7eb74-fbcc-4d4f-8b53-50ed4d543a82",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "9aeed83e-1f0b-461a-8e2b-75efb7be5ca5": {
      "main": [
        [
          {
            "node": "bf6f14f1-b684-4100-9085-f97399c8911f",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "95e75bb0-94c4-492d-af8f-e6d79f442d92": {
      "main": [
        [
          {
            "node": "db40d370-1216-4f36-9d87-86f26524e4cd",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "1d97e536-a1c5-4520-a078-fb06baa377c4": {
      "main": [
        [
          {
            "node": "ebe94dfb-7395-4788-896d-14c3d5380c18",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "ebe94dfb-7395-4788-896d-14c3d5380c18": {
      "main": [
        [
          {
            "node": "1afcd3cf-8d54-4e11-8518-a82f246023ec",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "43acb79d-02c5-406b-9ae0-288e365518fa",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "d30412f1-f714-41cb-a867-3a45f202c378": {
      "main": [
        [
          {
            "node": "7ed4ec35-2b23-4b97-ba7d-f434ff54b93c",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "21e1a59a-3af6-45ba-83d8-73260736b92a": {
      "main": [
        [
          {
            "node": "d30412f1-f714-41cb-a867-3a45f202c378",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "2ac7ced8-60b5-4fab-902f-bd1360e41a3a": {
      "main": [
        [
          {
            "node": "a49bf76b-1a4b-4aba-b392-419ddc07e110",
            "type": "main",
            "index": 0
          },
          {
            "node": "06884594-3ad3-4329-b357-21359eb13503",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "1afcd3cf-8d54-4e11-8518-a82f246023ec": {
      "main": [
        [
          {
            "node": "601bc26b-1349-499c-a4eb-f08e3a064503",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "43acb79d-02c5-406b-9ae0-288e365518fa": {
      "main": [
        [
          {
            "node": "95e75bb0-94c4-492d-af8f-e6d79f442d92",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "7b1da623-5452-4783-ba8f-77896d3c8ea9": {
      "main": [
        [
          {
            "node": "d5e3ffd7-03d0-4b54-ab34-67fe43830531",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "aa200c64-05c9-4437-8f18-980642e4ec6c": {
      "main": [
        [
          {
            "node": "21c6891d-1a57-4c4b-b7c4-41d84b20b744",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "601bc26b-1349-499c-a4eb-f08e3a064503": {
      "main": [
        [
          {
            "node": "21c6891d-1a57-4c4b-b7c4-41d84b20b744",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "8a3beea4-640b-4ddc-a801-719ace2643d0": {
      "main": [
        [
          {
            "node": "17752d5e-75d2-43ff-8e1b-ff439e6329b8",
            "type": "main",
            "index": 0
          },
          {
            "node": "39f6aa6b-3296-47e6-b71d-59bcc7efd34c",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "bf6f14f1-b684-4100-9085-f97399c8911f": {
      "main": [
        [
          {
            "node": "5e547d2f-c978-42bc-acb2-25c194308154",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "db40d370-1216-4f36-9d87-86f26524e4cd": {
      "main": [
        [
          {
            "node": "21c6891d-1a57-4c4b-b7c4-41d84b20b744",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "4e2ab9f1-0b12-4a25-8f2c-981500889660",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "39f6aa6b-3296-47e6-b71d-59bcc7efd34c": {
      "main": [
        [
          {
            "node": "8ff7eb74-fbcc-4d4f-8b53-50ed4d543a82",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "483ec727-0b0b-46a7-9bd9-fbd52f2bb51a": {
      "ai_languageModel": [
        [
          {
            "node": "17752d5e-75d2-43ff-8e1b-ff439e6329b8",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "06884594-3ad3-4329-b357-21359eb13503": {
      "main": [
        [
          {
            "node": "24204fee-6c47-4026-a6a6-12ced75a49da",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "5e547d2f-c978-42bc-acb2-25c194308154": {
      "main": [
        [
          {
            "node": "d5e3ffd7-03d0-4b54-ab34-67fe43830531",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "81762b68-7a5d-4f03-8479-c209d2696970": {
      "main": [
        [
          {
            "node": "c3fedf3c-ed52-49d7-b9d5-30050d4324f2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "8ff7eb74-fbcc-4d4f-8b53-50ed4d543a82": {
      "main": [
        [
          {
            "node": "aeba4982-b0fc-4d97-a06b-35b559135361",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "4e2ab9f1-0b12-4a25-8f2c-981500889660": {
      "main": [
        [
          {
            "node": "917539f7-c7a2-48a4-9227-93287eeeded9",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "e108c8b4-e8bb-4c43-aa01-0aa2cb70e83c": {
      "main": [
        [
          {
            "node": "aa200c64-05c9-4437-8f18-980642e4ec6c",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "975691b1-9667-4913-aa56-bc177e202dae": {
      "main": [
        [
          {
            "node": "a24362b0-e288-47af-9756-eff4acde1682",
            "type": "main",
            "index": 0
          },
          {
            "node": "2ac7ced8-60b5-4fab-902f-bd1360e41a3a",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "917539f7-c7a2-48a4-9227-93287eeeded9": {
      "main": [
        [
          {
            "node": "21c6891d-1a57-4c4b-b7c4-41d84b20b744",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Foire aux questions

Comment utiliser ce workflow ?

Copiez le code de configuration JSON ci-dessus, créez un nouveau workflow dans votre instance n8n et sélectionnez "Importer depuis le JSON", collez la configuration et modifiez les paramètres d'authentification selon vos besoins.

Dans quelles scénarios ce workflow est-il adapté ?

Avancé - Résumé IA, IA Multimodale

Est-ce payant ?

Ce workflow est entièrement gratuit et peut être utilisé directement. Veuillez noter que les services tiers utilisés dans le workflow (comme l'API OpenAI) peuvent nécessiter un paiement de votre part.

Informations sur le workflow
Niveau de difficulté
Avancé
Nombre de nœuds41
Catégorie2
Types de nœuds13
Description de la difficulté

Adapté aux utilisateurs avancés, avec des workflows complexes contenant 16+ nœuds

Auteur
Evgeny Agronsky

Evgeny Agronsky

@jenyok

Exploring LLMs, AI-driven workflows, and practical n8n automations.

Liens externes
Voir sur n8n.io

Partager ce workflow

Catégories

Catégories: 34