8
n8n 한국어amn8n.com

Gitlab 코드 리뷰 템플릿

고급

이것은AI Summarization, Multimodal AI분야의자동화 워크플로우로, 41개의 노드를 포함합니다.주로 If, Set, Code, Jira, Merge 등의 노드를 사용하며. Gemini AI와 JIRA 컨텍스트를 사용한 GitLab 병합 요청 코드 리뷰 자동화

사전 요구사항
  • HTTP Webhook 엔드포인트(n8n이 자동으로 생성)
  • 대상 API의 인증 정보가 필요할 수 있음
  • Google Gemini API Key
워크플로우 미리보기
노드 연결 관계를 시각적으로 표시하며, 확대/축소 및 이동을 지원합니다
워크플로우 내보내기
다음 JSON 구성을 복사하여 n8n에 가져오면 이 워크플로우를 사용할 수 있습니다
{
  "id": "Rwpn5OG2ql8rIOzH",
  "meta": {
    "instanceId": "ebde2b8e011b15f5db59a3b84e187c4795dbe1d4b2c695e9940aa4dbcd102f98",
    "templateCredsSetupCompleted": true
  },
  "name": "Gitlab Code Review Template",
  "tags": [],
  "nodes": [
    {
      "id": "c3fedf3c-ed52-49d7-b9d5-30050d4324f2",
      "name": "Need Review",
      "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": "Skip File Changes",
      "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": "Basic LLM 체인",
      "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 채팅 모델",
      "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": "Extract the JIRA Issue ID",
      "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": "Get JIRA issue",
      "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": "Format JIRA Context",
      "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": "Extract MR Details",
      "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": "If JIRA Subtask",
      "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": "Get JIRA Parent Issue",
      "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": "Format JIRA Parent Context",
      "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": "병합",
      "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": "Get MR Changes",
      "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": "Prepare 코드 Changes",
      "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": "오류 트리거",
      "type": "n8n-nodes-base.errorTrigger",
      "position": [
        352,
        1692
      ],
      "parameters": {},
      "executeOnce": false,
      "retryOnFail": false,
      "typeVersion": 1,
      "alwaysOutputData": false
    },
    {
      "id": "aeba4982-b0fc-4d97-a06b-35b559135361",
      "name": "집계",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        -768,
        1408
      ],
      "parameters": {
        "options": {},
        "aggregate": "aggregateAllItemData"
      },
      "typeVersion": 1
    },
    {
      "id": "8ff7eb74-fbcc-4d4f-8b53-50ed4d543a82",
      "name": "병합 LLM Output with Input",
      "type": "n8n-nodes-base.merge",
      "position": [
        -992,
        1408
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineByPosition"
      },
      "typeVersion": 3.2
    },
    {
      "id": "39f6aa6b-3296-47e6-b71d-59bcc7efd34c",
      "name": "필터 Irrelevant Fields",
      "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": "Any Issues Found?",
      "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": "분할 출력 Changes",
      "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": "분할 출력 Comments",
      "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": "Prepare Request",
      "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": "Prepare Request Without 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": "설정 workflow execution information",
      "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": "Post Review 시작ed",
      "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": "Listen For Gitlab Comments",
      "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": "Parse 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": "Post No Issues Found",
      "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": "Post Error Occurred",
      "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": "Get Static Context Including Error",
      "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": "Get Static Context",
      "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": "Cleanup Static Context",
      "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": "Process Comments",
      "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": "Post Gitlab MR Comments",
      "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": "Try It Out!",
      "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": "Step 1: Listen & Capture",
      "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": "Step 2: Prepare & Review",
      "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": "Step 3: Post & Fallback",
      "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": "Static Context & Error Handling",
      "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": "Need Help & Customise",
      "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": "Post Gitlab MR Comments WIthout 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": {
    "Merge": {
      "main": [
        [
          {
            "node": "Split Out Changes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate": {
      "main": [
        [
          {
            "node": "1d97e536-a1c5-4520-a078-fb06baa377c4",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "7ed4ec35-2b23-4b97-ba7d-f434ff54b93c": {
      "main": [
        [
          {
            "node": "Prepare Code Changes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "c3fedf3c-ed52-49d7-b9d5-30050d4324f2": {
      "main": [
        [
          {
            "node": "Set workflow execution information",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Error Trigger": {
      "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": "Merge",
            "type": "main",
            "index": 2
          }
        ]
      ]
    },
    "Basic LLM Chain": {
      "main": [
        [
          {
            "node": "Merge LLM Output with Input",
            "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": "Split Out Comments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "d30412f1-f714-41cb-a867-3a45f202c378": {
      "main": [
        [
          {
            "node": "7ed4ec35-2b23-4b97-ba7d-f434ff54b93c",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Out Changes": {
      "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
          }
        ]
      ]
    },
    "Split Out Comments": {
      "main": [
        [
          {
            "node": "95e75bb0-94c4-492d-af8f-e6d79f442d92",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "7b1da623-5452-4783-ba8f-77896d3c8ea9": {
      "main": [
        [
          {
            "node": "Merge",
            "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
          }
        ]
      ]
    },
    "Prepare Code Changes": {
      "main": [
        [
          {
            "node": "Basic LLM Chain",
            "type": "main",
            "index": 0
          },
          {
            "node": "Filter Irrelevant Fields",
            "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
          }
        ]
      ]
    },
    "Filter Irrelevant Fields": {
      "main": [
        [
          {
            "node": "Merge LLM Output with Input",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Google Gemini Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Basic LLM Chain",
            "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": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "81762b68-7a5d-4f03-8479-c209d2696970": {
      "main": [
        [
          {
            "node": "c3fedf3c-ed52-49d7-b9d5-30050d4324f2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge LLM Output with Input": {
      "main": [
        [
          {
            "node": "Aggregate",
            "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
          }
        ]
      ]
    },
    "Set workflow execution information": {
      "main": [
        [
          {
            "node": "Post Review Started",
            "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
          }
        ]
      ]
    }
  }
}
자주 묻는 질문

이 워크플로우를 어떻게 사용하나요?

위의 JSON 구성 코드를 복사하여 n8n 인스턴스에서 새 워크플로우를 생성하고 "JSON에서 가져오기"를 선택한 후, 구성을 붙여넣고 필요에 따라 인증 설정을 수정하세요.

이 워크플로우는 어떤 시나리오에 적합한가요?

고급 - AI 요약, 멀티모달 AI

유료인가요?

이 워크플로우는 완전히 무료이며 직접 가져와 사용할 수 있습니다. 다만, 워크플로우에서 사용하는 타사 서비스(예: OpenAI API)는 사용자 직접 비용을 지불해야 할 수 있습니다.

워크플로우 정보
난이도
고급
노드 수41
카테고리2
노드 유형13
난이도 설명

고급 사용자를 위한 16+개 노드의 복잡한 워크플로우

저자
Evgeny Agronsky

Evgeny Agronsky

@jenyok

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

외부 링크
n8n.io에서 보기

이 워크플로우 공유

카테고리

카테고리: 34