AutomationFlowsGeneral › GitHub PR Comment Fix Workflow

GitHub PR Comment Fix Workflow

Original n8n title: Jump-section: Comment Fix Pipeline

jump-section: Comment Fix Pipeline. Uses httpRequest. Webhook trigger; 24 nodes.

Webhook trigger★★★★☆ complexity24 nodesHTTP Request
General Trigger: Webhook Nodes: 24 Complexity: ★★★★☆ Added:

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "name": "jump-section: Comment Fix Pipeline",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "comment-fix",
        "responseMode": "onReceived",
        "responseData": "noData"
      },
      "id": "webhook-trigger",
      "name": "Webhook: PR Comment Event",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 1.1,
      "position": [
        240,
        300
      ]
    },
    {
      "parameters": {
        "conditions": {
          "conditions": [
            {
              "id": "action-check",
              "leftValue": "={{ $json.body.action }}",
              "rightValue": "created",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ]
        }
      },
      "id": "check-action",
      "name": "IF: action=created",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        460,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "// GitHub Actions\uc5d0\uc11c \uc624\uba74 payload\uac00 \uc9c1\uc811, \uc9c1\uc811 webhook\uc774\uba74 .body\uc5d0 \ub798\ud551\ub428\nconst raw = $input.first().json;\nconst body = raw.body || raw;\nconst comment = body.comment;\nconst repo = body.repository;\n\nconst commentBody = (comment.body || '').trim();\nlet command = null;\n\nif (commentBody === '/fix-build') command = 'fix-build';\nelse if (commentBody === '/fix-lint') command = 'fix-lint';\nelse if (commentBody === '/fix' && comment.in_reply_to_id) command = 'fix';\n\nif (!command) return [];\n\nlet prNumber, prTitle, prUrl, headBranch, headSha;\n\nif (body.pull_request) {\n  // pull_request_review_comment \uc774\ubca4\ud2b8\n  const pr = body.pull_request;\n  prNumber = pr.number;\n  prTitle = pr.title;\n  prUrl = pr.html_url;\n  headBranch = pr.head.ref;\n  headSha = pr.head.sha;\n} else if (body.issue && body.issue.pull_request) {\n  // issue_comment \uc774\ubca4\ud2b8 (\uc77c\ubc18 PR \ucf54\uba58\ud2b8)\n  prNumber = body.issue.number;\n  prTitle = body.issue.title;\n  prUrl = body.issue.html_url;\n\n  // headBranch/headSha\ub294 API\ub85c \uc870\ud68c\n  const token = $env.GITHUB_TOKEN;\n  const prData = await new Promise((resolve, reject) => {\n    const https = require('https');\n    const req = https.request({\n      hostname: 'api.github.com',\n      path: `/repos/${repo.owner.login}/${repo.name}/pulls/${prNumber}`,\n      method: 'GET',\n      headers: {\n        'Authorization': `Bearer ${token}`,\n        'Accept': 'application/vnd.github+json',\n        'X-GitHub-Api-Version': '2022-11-28',\n        'User-Agent': 'n8n-jump-section'\n      }\n    }, (res) => {\n      let data = '';\n      res.on('data', chunk => data += chunk);\n      res.on('end', () => {\n        if (res.statusCode >= 400) { reject(new Error(`HTTP ${res.statusCode}: ${data}`)); return; }\n        try { resolve(JSON.parse(data)); } catch (e) { reject(new Error('Invalid JSON: ' + data.slice(0, 200))); }\n      });\n    });\n    req.setTimeout(15000, () => { req.destroy(); reject(new Error('Request timeout')); });\n    req.on('error', reject);\n    req.end();\n  });\n\n  headBranch = prData.head.ref;\n  headSha = prData.head.sha;\n} else {\n  return [];\n}\n\nreturn [{\n  json: {\n    command,\n    prNumber,\n    prTitle,\n    prUrl,\n    headBranch,\n    headSha,\n    repoOwner: repo.owner.login,\n    repoName: repo.name,\n    commentId: comment.id,\n    filePath: comment.path || null,\n    inReplyToId: comment.in_reply_to_id || null\n  }\n}];"
      },
      "id": "detect-command",
      "name": "\ucee4\ub9e8\ub4dc \uac10\uc9c0",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        680,
        300
      ]
    },
    {
      "parameters": {
        "conditions": {
          "combinator": "and",
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "is-fix-build",
              "leftValue": "={{ $json.command }}",
              "rightValue": "fix-build",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ]
        }
      },
      "id": "if-fix-build",
      "name": "IF: fix-build?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        900,
        300
      ]
    },
    {
      "parameters": {
        "conditions": {
          "combinator": "and",
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "is-fix-lint",
              "leftValue": "={{ $json.command }}",
              "rightValue": "fix-lint",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ]
        }
      },
      "id": "if-fix-lint",
      "name": "IF: fix-lint?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        900,
        480
      ]
    },
    {
      "parameters": {
        "jsCode": "const ctx = $input.first().json;\nconst token = $env.GITHUB_TOKEN;\n\nfunction ghGet(path) {\n  return new Promise((resolve, reject) => {\n    const https = require('https');\n    const req = https.request({\n      hostname: 'api.github.com',\n      path,\n      method: 'GET',\n      headers: {\n        'Authorization': `Bearer ${token}`,\n        'Accept': 'application/vnd.github+json',\n        'X-GitHub-Api-Version': '2022-11-28',\n        'User-Agent': 'n8n-jump-section'\n      }\n    }, (res) => {\n      let data = '';\n      res.on('data', chunk => data += chunk);\n      res.on('end', () => {\n        if (res.statusCode >= 400) reject(new Error(`HTTP ${res.statusCode}: ${data}`));\n        else resolve(JSON.parse(data));\n      });\n    });\n    req.setTimeout(15000, () => { req.destroy(); reject(new Error('Request timeout')); });\n    req.on('error', reject);\n    req.end();\n  });\n}\n\nconst filesData = await ghGet(`/repos/${ctx.repoOwner}/${ctx.repoName}/pulls/${ctx.prNumber}/files?per_page=30`);\n\nconst files = await Promise.all(filesData.map(async (f) => {\n  const contentData = await ghGet(`/repos/${ctx.repoOwner}/${ctx.repoName}/contents/${f.filename}?ref=${ctx.headBranch}`);\n  const content = Buffer.from((contentData.content || '').replace(/\\n/g, ''), 'base64').toString('utf8');\n  return { path: f.filename, content, sha: contentData.sha };\n}));\n\nconst checkData = await ghGet(`/repos/${ctx.repoOwner}/${ctx.repoName}/commits/${ctx.headSha}/check-runs?per_page=30`);\nconst failed = (checkData.check_runs || []).filter(c =>\n  c.conclusion === 'failure' || c.conclusion === 'timed_out'\n);\n\nconst ciSection = failed.length > 0\n  ? failed.map(c => {\n      const lines = [`### \u274c ${c.name}`];\n      if (c.output?.summary) lines.push(c.output.summary);\n      if (c.output?.text) lines.push(c.output.text.slice(0, 3000));\n      return lines.join('\\n');\n    }).join('\\n\\n')\n  : '(CI \uc2e4\ud328 \uc5c6\uc74c)';\n\nconst filesContext = files.map(f =>\n  `### ${f.path}\\n\\`\\`\\`typescript\\n${f.content}\\n\\`\\`\\``\n).join('\\n\\n');\n\nconst userPrompt = `## PR #${ctx.prNumber}: ${ctx.prTitle}\\n\ube0c\ub79c\uce58: \\`${ctx.headBranch}\\`\\n\\n## CI \uc2e4\ud328 \uc815\ubcf4\\n\\n${ciSection}\\n\\n---\\n\\n## \ud604\uc7ac \ucf54\ub4dc\\n\\n${filesContext}\\n\\n---\\n\\nCI \ube4c\ub4dc/\ud0c0\uc785 \uc5d0\ub7ec\ub97c \ubd84\uc11d\ud558\uc5ec \uc218\uc815\ud558\uc138\uc694. \uc6d0\uc778\uc774 \uba85\ud655\ud55c \uacbd\uc6b0\uc5d0\ub9cc write_file\ub85c \uc81c\ucd9c\ud569\ub2c8\ub2e4.`;\n\nconst geminiPayload = {\n  system_instruction: {\n    parts: [{ text: 'TypeScript \uc804\ubb38 \uac1c\ubc1c\uc790\uc785\ub2c8\ub2e4. CI \ube4c\ub4dc \uc5d0\ub7ec\uc640 \ud0c0\uc785 \uc5d0\ub7ec\ub97c \uc218\uc815\ud569\ub2c8\ub2e4.\\n\\n## \ud544\uc218 \uaddc\uce59\\n\\n### Prettier \ud3ec\ub9f7\\n- \ub4e4\uc5ec\uc4f0\uae30: \uc2a4\ud398\uc774\uc2a4 2\uce78, \uc138\ubbf8\ucf5c\ub860 \ud56d\uc0c1, \uc2f1\uae00\ucffc\ud2b8, \ucd5c\ub300 100\uc790, trailing comma \ud56d\uc0c1\\n\\n### \uc218\uc815 \uc6d0\uce59\\n- CI \uc5d0\ub7ec \uc6d0\uc778\uc774 \uba85\ud655\ud558\uace0 \uc218\uc815 \ubc29\ud5a5\uc774 \ud655\uc2e4\ud55c \uacbd\uc6b0\uc5d0\ub9cc write_file\ub85c \uc81c\ucd9c\\n- \ubd88\ud655\uc2e4\ud558\uba74 \ud574\ub2f9 \ud30c\uc77c\uc740 \uac74\ub108\ub700\\n- \uae30\uc874 Public API \ud558\uc704 \ud638\ud658\uc131 \uc720\uc9c0\\n- \uc218\uc815\uc774 \ud544\uc694 \uc5c6\ub294 \ud30c\uc77c\uc740 write_file \uc0dd\ub7b5' }]\n  },\n  contents: [{ role: 'user', parts: [{ text: userPrompt }] }],\n  tools: [{\n    functionDeclarations: [{\n      name: 'write_file',\n      description: '\uc218\uc815\ub41c \ud30c\uc77c\uc758 \uc804\uccb4 \ub0b4\uc6a9\uc744 \uc81c\ucd9c\ud569\ub2c8\ub2e4.',\n      parameters: {\n        type: 'OBJECT',\n        properties: {\n          path: { type: 'STRING', description: '\ud30c\uc77c \uacbd\ub85c' },\n          content: { type: 'STRING', description: '\ud30c\uc77c \uc804\uccb4 \ub0b4\uc6a9' },\n          description: { type: 'STRING', description: '\ubcc0\uacbd \uc124\uba85' }\n        },\n        required: ['path', 'content', 'description']\n      }\n    }]\n  }],\n  toolConfig: { functionCallingConfig: { mode: 'AUTO' } },\n  generationConfig: { maxOutputTokens: 8192 }\n};\n\nreturn [{ json: { ...ctx, files, geminiPayload } }];"
      },
      "id": "prepare-build",
      "name": "[build] \ud30c\uc77c + CI \uc870\ud68c \ubc0f \ud504\ub86c\ud504\ud2b8 \uc900\ube44",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1120,
        200
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={{ $env.GEMINI_API_KEY }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ $json.geminiPayload }}",
        "options": {}
      },
      "id": "gemini-fix-build",
      "name": "[build] Gemini: \ucf54\ub4dc \uc218\uc815",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1340,
        200
      ]
    },
    {
      "parameters": {
        "jsCode": "const response = $input.first().json;\nconst ctx = $('[build] \ud30c\uc77c + CI \uc870\ud68c \ubc0f \ud504\ub86c\ud504\ud2b8 \uc900\ube44').first().json;\n\nif (!response.candidates || response.candidates.length === 0) {\n  return [{ json: { noChanges: true, geminiError: response.promptFeedback || response.error || 'no candidates', ...ctx } }];\n}\n\nconst candidate = response.candidates[0];\nif (!candidate.content || !candidate.content.parts || candidate.finishReason === 'MALFORMED_FUNCTION_CALL') {\n  return [{ json: { noChanges: true, geminiError: candidate.finishReason || 'no content', ...ctx } }];\n}\n\nconst parts = candidate.content.parts;\nconst functionCalls = parts.filter(p => p.functionCall && p.functionCall.name === 'write_file');\n\nif (functionCalls.length === 0) {\n  return [{ json: { noChanges: true, ...ctx } }];\n}\n\nconst fixedFiles = functionCalls.map(p => {\n  const rawContent = p.functionCall.args.content || ''; const content = rawContent.replace(/^```[\\w]*\\n?/, '').replace(/\\n?```$/, '').trim();\n  const original = ctx.files.find(f => f.path === p.functionCall.args.path);\n  return {\n    path: p.functionCall.args.path,\n    content,\n    description: p.functionCall.args.description,\n    sha: original?.sha || null\n  };\n});\n\nconst filesSummary = fixedFiles.map(f => `\u2022 \\`${f.path}\\`: ${f.description}`).join('\\n');\n\nreturn [{ json: { noChanges: false, fixedFiles, filesSummary, ...ctx } }];"
      },
      "id": "parse-build",
      "name": "[build] \uc218\uc815 \uacb0\uacfc \ud30c\uc2f1",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1560,
        200
      ]
    },
    {
      "parameters": {
        "conditions": {
          "conditions": [
            {
              "id": "has-changes",
              "leftValue": "={{ $json.noChanges }}",
              "rightValue": false,
              "operator": {
                "type": "boolean",
                "operation": "false"
              }
            }
          ]
        }
      },
      "id": "check-changes-build",
      "name": "[build] IF: \uc218\uc815\ub41c \ud30c\uc77c \uc788\ub294\uc9c0",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1780,
        200
      ]
    },
    {
      "parameters": {
        "jsCode": "const data = $input.first().json;\nif (data.noChanges || !data.fixedFiles || data.fixedFiles.length === 0) return [];\nconst fixedFiles = data.fixedFiles.filter(f => f.path && f.content && f.content.trim().length > 0);\nif (fixedFiles.length === 0) return [];\nconst { headBranch, repoOwner, repoName, filesSummary } = data;\nconst token = $env.GITHUB_TOKEN;\n\nconst headers = {\n  'Authorization': `Bearer ${token}`,\n  'Accept': 'application/vnd.github+json',\n  'X-GitHub-Api-Version': '2022-11-28',\n  'Content-Type': 'application/json',\n  'User-Agent': 'n8n-jump-section'\n};\n\nconst baseUrl = `https://api.github.com/repos/${repoOwner}/${repoName}`;\n\nfunction request(method, url, body) {\n  return new Promise((resolve, reject) => {\n    const https = require('https');\n    const path = url.replace('https://api.github.com', '');\n    const options = { hostname: 'api.github.com', path, method, headers };\n    const req = https.request(options, (res) => {\n      let d = '';\n      res.on('data', chunk => d += chunk);\n      res.on('end', () => {\n        if (res.statusCode >= 400) reject(new Error(`HTTP ${res.statusCode}: ${d}`));\n        else resolve(JSON.parse(d));\n      });\n    });\n    req.setTimeout(15000, () => { req.destroy(); reject(new Error('Request timeout')); });\n    req.on('error', reject);\n    if (body) req.write(JSON.stringify(body));\n    req.end();\n  });\n}\n\nconst refData = await request('GET', `${baseUrl}/git/refs/heads/${headBranch}`);\nconst baseSha = refData.object.sha;\nconst commitData = await request('GET', `${baseUrl}/git/commits/${baseSha}`);\nconst baseTreeSha = commitData.tree.sha;\n\nconst newTree = await request('POST', `${baseUrl}/git/trees`, {\n  base_tree: baseTreeSha,\n  tree: fixedFiles.map(f => ({ path: f.path, mode: '100644', type: 'blob', content: f.content }))\n});\n\nconst newCommit = await request('POST', `${baseUrl}/git/commits`, {\n  message: `fix: fix build errors\\n\\n${filesSummary}`,\n  tree: newTree.sha,\n  parents: [baseSha]\n});\n\nawait request('PATCH', `${baseUrl}/git/refs/heads/${headBranch}`, { sha: newCommit.sha });\n\nawait request('POST', `${baseUrl}/actions/workflows/auto-format.yml/dispatches`, {\n  ref: headBranch,\n  inputs: { branch: headBranch }\n});\n\nreturn [{ json: { ...data, commitSha: newCommit.sha } }];"
      },
      "id": "commit-build",
      "name": "[build] GitHub: \ucee4\ubc0b",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2000,
        140
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://api.github.com/repos/{{ $json.repoOwner }}/{{ $json.repoName }}/issues/{{ $json.prNumber }}/comments",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept",
              "value": "application/vnd.github+json"
            },
            {
              "name": "X-GitHub-Api-Version",
              "value": "2022-11-28"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Authorization",
              "value": "=Bearer {{ $env.GITHUB_TOKEN }}"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ { body: '\u2705 \ube4c\ub4dc \uc5d0\ub7ec \uc218\uc815 \uc644\ub8cc\\n\\n' + $json.filesSummary + '\\n\\ncommit: `' + $json.commitSha.slice(0, 7) + '`' } }}",
        "options": {}
      },
      "id": "reply-build",
      "name": "[build] GitHub: PR \ucf54\uba58\ud2b8",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2220,
        140
      ]
    },
    {
      "parameters": {
        "jsCode": "const ctx = $input.first().json;\nconst token = $env.GITHUB_TOKEN;\n\nfunction ghGet(path) {\n  return new Promise((resolve, reject) => {\n    const https = require('https');\n    const req = https.request({\n      hostname: 'api.github.com',\n      path,\n      method: 'GET',\n      headers: {\n        'Authorization': `Bearer ${token}`,\n        'Accept': 'application/vnd.github+json',\n        'X-GitHub-Api-Version': '2022-11-28',\n        'User-Agent': 'n8n-jump-section'\n      }\n    }, (res) => {\n      let data = '';\n      res.on('data', chunk => data += chunk);\n      res.on('end', () => {\n        if (res.statusCode >= 400) reject(new Error(`HTTP ${res.statusCode}: ${data}`));\n        else resolve(JSON.parse(data));\n      });\n    });\n    req.setTimeout(15000, () => { req.destroy(); reject(new Error('Request timeout')); });\n    req.on('error', reject);\n    req.end();\n  });\n}\n\nconst filesData = await ghGet(`/repos/${ctx.repoOwner}/${ctx.repoName}/pulls/${ctx.prNumber}/files?per_page=30`);\n\nconst tsFiles = filesData.filter(f => /\\.(ts|tsx|js|jsx)$/.test(f.filename));\n\nconst files = await Promise.all(tsFiles.map(async (f) => {\n  const contentData = await ghGet(`/repos/${ctx.repoOwner}/${ctx.repoName}/contents/${f.filename}?ref=${ctx.headBranch}`);\n  const content = Buffer.from((contentData.content || '').replace(/\\n/g, ''), 'base64').toString('utf8');\n  return { path: f.filename, content, sha: contentData.sha };\n}));\n\nconst filesContext = files.map(f =>\n  `### ${f.path}\\n\\`\\`\\`typescript\\n${f.content}\\n\\`\\`\\``\n).join('\\n\\n');\n\nconst userPrompt = `## PR #${ctx.prNumber}: ${ctx.prTitle}\\n\\n\uc544\ub798 \ud30c\uc77c\ub4e4\uc5d0 Prettier \ud3ec\ub9f7\uc744 \uc815\ud655\ud788 \uc801\uc6a9\ud558\uc138\uc694.\\n\ud3ec\ub9f7\uc774 \uc774\ubbf8 \uc62c\ubc14\ub978 \ud30c\uc77c\uc740 write_file\uc744 \ud638\ucd9c\ud558\uc9c0 \ub9c8\uc138\uc694.\\n\\n${filesContext}`;\n\nconst geminiPayload = {\n  system_instruction: {\n    parts: [{ text: 'TypeScript \ucf54\ub4dc \ud3ec\ub9f7\ud130\uc785\ub2c8\ub2e4. Prettier \uaddc\uce59\uc744 \uc815\ud655\ud788 \uc801\uc6a9\ud569\ub2c8\ub2e4.\\n\\n## Prettier \uaddc\uce59 (\ubc18\ub4dc\uc2dc \uc900\uc218)\\n- \ub4e4\uc5ec\uc4f0\uae30: \uc2a4\ud398\uc774\uc2a4 2\uce78\\n- \uc138\ubbf8\ucf5c\ub860: \ud56d\uc0c1 \ubd99\uc784\\n- \ub530\uc634\ud45c: \uc2f1\uae00\ucffc\ud2b8\\n- \ud55c \uc904 \ucd5c\ub300 \uae38\uc774: 100\uc790\\n- trailing comma: \ud56d\uc0c1 \ubd99\uc784 (\ud568\uc218 \uc778\uc790 \ub9c8\uc9c0\ub9c9 \ud3ec\ud568)\\n- \uc904\ubc14\uafc8: LF (\\\\n)\\n\\n## \uc218\uc815 \uc6d0\uce59\\n- \ud3ec\ub9f7\ub9cc \uc218\uc815, \ub85c\uc9c1 \ubcc0\uacbd \uae08\uc9c0\\n- \uc774\ubbf8 \uc62c\ubc14\ub978 \ud30c\uc77c\uc740 write_file \uc0dd\ub7b5' }]\n  },\n  contents: [{ role: 'user', parts: [{ text: userPrompt }] }],\n  tools: [{\n    functionDeclarations: [{\n      name: 'write_file',\n      description: '\ud3ec\ub9f7 \uc801\uc6a9\ub41c \ud30c\uc77c\uc758 \uc804\uccb4 \ub0b4\uc6a9\uc744 \uc81c\ucd9c\ud569\ub2c8\ub2e4.',\n      parameters: {\n        type: 'OBJECT',\n        properties: {\n          path: { type: 'STRING', description: '\ud30c\uc77c \uacbd\ub85c' },\n          content: { type: 'STRING', description: '\ud30c\uc77c \uc804\uccb4 \ub0b4\uc6a9' },\n          description: { type: 'STRING', description: '\ubcc0\uacbd \uc124\uba85' }\n        },\n        required: ['path', 'content', 'description']\n      }\n    }]\n  }],\n  toolConfig: { functionCallingConfig: { mode: 'AUTO' } },\n  generationConfig: { maxOutputTokens: 8192 }\n};\n\nreturn [{ json: { ...ctx, files, geminiPayload } }];"
      },
      "id": "prepare-lint",
      "name": "[lint] \ud30c\uc77c \uc870\ud68c \ubc0f \ud504\ub86c\ud504\ud2b8 \uc900\ube44",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1120,
        400
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={{ $env.GEMINI_API_KEY }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ $json.geminiPayload }}",
        "options": {}
      },
      "id": "gemini-fix-lint",
      "name": "[lint] Gemini: \ud3ec\ub9f7 \uc218\uc815",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1340,
        400
      ]
    },
    {
      "parameters": {
        "jsCode": "const response = $input.first().json;\nconst ctx = $('[lint] \ud30c\uc77c \uc870\ud68c \ubc0f \ud504\ub86c\ud504\ud2b8 \uc900\ube44').first().json;\n\nif (!response.candidates || response.candidates.length === 0) {\n  return [{ json: { noChanges: true, geminiError: response.promptFeedback || response.error || 'no candidates', ...ctx } }];\n}\n\nconst candidate = response.candidates[0];\nif (!candidate.content || !candidate.content.parts || candidate.finishReason === 'MALFORMED_FUNCTION_CALL') {\n  return [{ json: { noChanges: true, geminiError: candidate.finishReason || 'no content', ...ctx } }];\n}\n\nconst parts = candidate.content.parts;\nconst functionCalls = parts.filter(p => p.functionCall && p.functionCall.name === 'write_file');\n\nif (functionCalls.length === 0) {\n  return [{ json: { noChanges: true, ...ctx } }];\n}\n\nconst fixedFiles = functionCalls.map(p => {\n  const rawContent = p.functionCall.args.content || ''; const content = rawContent.replace(/^```[\\w]*\\n?/, '').replace(/\\n?```$/, '').trim();\n  const original = ctx.files.find(f => f.path === p.functionCall.args.path);\n  return {\n    path: p.functionCall.args.path,\n    content,\n    description: p.functionCall.args.description,\n    sha: original?.sha || null\n  };\n});\n\nconst filesSummary = fixedFiles.map(f => `\u2022 \\`${f.path}\\`: ${f.description}`).join('\\n');\n\nreturn [{ json: { noChanges: false, fixedFiles, filesSummary, ...ctx } }];"
      },
      "id": "parse-lint",
      "name": "[lint] \uc218\uc815 \uacb0\uacfc \ud30c\uc2f1",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1560,
        400
      ]
    },
    {
      "parameters": {
        "conditions": {
          "conditions": [
            {
              "id": "has-changes",
              "leftValue": "={{ $json.noChanges }}",
              "rightValue": false,
              "operator": {
                "type": "boolean",
                "operation": "false"
              }
            }
          ]
        }
      },
      "id": "check-changes-lint",
      "name": "[lint] IF: \uc218\uc815\ub41c \ud30c\uc77c \uc788\ub294\uc9c0",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1780,
        400
      ]
    },
    {
      "parameters": {
        "jsCode": "const data = $input.first().json;\nif (data.noChanges || !data.fixedFiles || data.fixedFiles.length === 0) return [];\nconst fixedFiles = data.fixedFiles.filter(f => f.path && f.content && f.content.trim().length > 0);\nif (fixedFiles.length === 0) return [];\nconst { headBranch, repoOwner, repoName, filesSummary } = data;\nconst token = $env.GITHUB_TOKEN;\n\nconst headers = {\n  'Authorization': `Bearer ${token}`,\n  'Accept': 'application/vnd.github+json',\n  'X-GitHub-Api-Version': '2022-11-28',\n  'Content-Type': 'application/json',\n  'User-Agent': 'n8n-jump-section'\n};\n\nconst baseUrl = `https://api.github.com/repos/${repoOwner}/${repoName}`;\n\nfunction request(method, url, body) {\n  return new Promise((resolve, reject) => {\n    const https = require('https');\n    const path = url.replace('https://api.github.com', '');\n    const options = { hostname: 'api.github.com', path, method, headers };\n    const req = https.request(options, (res) => {\n      let d = '';\n      res.on('data', chunk => d += chunk);\n      res.on('end', () => {\n        if (res.statusCode >= 400) reject(new Error(`HTTP ${res.statusCode}: ${d}`));\n        else resolve(JSON.parse(d));\n      });\n    });\n    req.setTimeout(15000, () => { req.destroy(); reject(new Error('Request timeout')); });\n    req.on('error', reject);\n    if (body) req.write(JSON.stringify(body));\n    req.end();\n  });\n}\n\nconst refData = await request('GET', `${baseUrl}/git/refs/heads/${headBranch}`);\nconst baseSha = refData.object.sha;\nconst commitData = await request('GET', `${baseUrl}/git/commits/${baseSha}`);\nconst baseTreeSha = commitData.tree.sha;\n\nconst newTree = await request('POST', `${baseUrl}/git/trees`, {\n  base_tree: baseTreeSha,\n  tree: fixedFiles.map(f => ({ path: f.path, mode: '100644', type: 'blob', content: f.content }))\n});\n\nconst newCommit = await request('POST', `${baseUrl}/git/commits`, {\n  message: `chore: apply prettier format\\n\\n${filesSummary}`,\n  tree: newTree.sha,\n  parents: [baseSha]\n});\n\nawait request('PATCH', `${baseUrl}/git/refs/heads/${headBranch}`, { sha: newCommit.sha });\n\nreturn [{ json: { ...data, commitSha: newCommit.sha } }];"
      },
      "id": "commit-lint",
      "name": "[lint] GitHub: \ucee4\ubc0b",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2000,
        340
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://api.github.com/repos/{{ $json.repoOwner }}/{{ $json.repoName }}/issues/{{ $json.prNumber }}/comments",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept",
              "value": "application/vnd.github+json"
            },
            {
              "name": "X-GitHub-Api-Version",
              "value": "2022-11-28"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Authorization",
              "value": "=Bearer {{ $env.GITHUB_TOKEN }}"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ { body: '\u2705 Prettier \ud3ec\ub9f7 \uc218\uc815 \uc644\ub8cc\\n\\n' + $json.filesSummary + '\\n\\ncommit: `' + $json.commitSha.slice(0, 7) + '`' } }}",
        "options": {}
      },
      "id": "reply-lint",
      "name": "[lint] GitHub: PR \ucf54\uba58\ud2b8",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2220,
        340
      ]
    },
    {
      "parameters": {
        "method": "GET",
        "url": "=https://api.github.com/repos/{{ $('\ucee4\ub9e8\ub4dc \uac10\uc9c0').first().json.repoOwner }}/{{ $('\ucee4\ub9e8\ub4dc \uac10\uc9c0').first().json.repoName }}/pulls/comments/{{ $('\ucee4\ub9e8\ub4dc \uac10\uc9c0').first().json.inReplyToId }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept",
              "value": "application/vnd.github+json"
            },
            {
              "name": "X-GitHub-Api-Version",
              "value": "2022-11-28"
            },
            {
              "name": "Authorization",
              "value": "=Bearer {{ $env.GITHUB_TOKEN }}"
            }
          ]
        },
        "options": {}
      },
      "id": "get-original-comment",
      "name": "[fix] GitHub: \uc6d0\ubcf8 \ucf54\uba58\ud2b8 \uc870\ud68c",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1120,
        620
      ]
    },
    {
      "parameters": {
        "method": "GET",
        "url": "=https://api.github.com/repos/{{ $('\ucee4\ub9e8\ub4dc \uac10\uc9c0').first().json.repoOwner }}/{{ $('\ucee4\ub9e8\ub4dc \uac10\uc9c0').first().json.repoName }}/contents/{{ $('\ucee4\ub9e8\ub4dc \uac10\uc9c0').first().json.filePath }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept",
              "value": "application/vnd.github+json"
            },
            {
              "name": "X-GitHub-Api-Version",
              "value": "2022-11-28"
            },
            {
              "name": "Authorization",
              "value": "=Bearer {{ $env.GITHUB_TOKEN }}"
            }
          ]
        },
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "ref",
              "value": "={{ $('\ucee4\ub9e8\ub4dc \uac10\uc9c0').first().json.headBranch }}"
            }
          ]
        },
        "options": {}
      },
      "id": "get-file-content",
      "name": "[fix] GitHub: \ud30c\uc77c \ub0b4\uc6a9 \uc77d\uae30",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1120,
        760
      ]
    },
    {
      "parameters": {
        "jsCode": "const ctx = $('\ucee4\ub9e8\ub4dc \uac10\uc9c0').first().json;\nconst originalComment = $('[fix] GitHub: \uc6d0\ubcf8 \ucf54\uba58\ud2b8 \uc870\ud68c').first().json;\nconst fileData = $('[fix] GitHub: \ud30c\uc77c \ub0b4\uc6a9 \uc77d\uae30').first().json;\n\nconst fileContent = Buffer.from((fileData.content || '').replace(/\\n/g, ''), 'base64').toString('utf8');\nconst fileSha = fileData.sha;\n\nconst commentAuthor = originalComment.user?.login || 'unknown';\nconst commentBody = originalComment.body || '';\nconst commentLine = originalComment.original_line || originalComment.line || '?';\nconst commentDiff = originalComment.diff_hunk || '';\n\n// PR \ubcc0\uacbd \ud30c\uc77c \uc804\uccb4 \uc870\ud68c (\ucf54\uba58\ud2b8 \ub2ec\ub9b0 \ud30c\uc77c + \uad00\ub828 \ud30c\uc77c)\nconst token = $env.GITHUB_TOKEN;\nfunction ghGet(path) {\n  return new Promise((resolve, reject) => {\n    const https = require('https');\n    const req = https.request({\n      hostname: 'api.github.com',\n      path,\n      method: 'GET',\n      headers: {\n        'Authorization': `Bearer ${token}`,\n        'Accept': 'application/vnd.github+json',\n        'X-GitHub-Api-Version': '2022-11-28',\n        'User-Agent': 'n8n-jump-section'\n      }\n    }, (res) => {\n      let data = '';\n      res.on('data', chunk => data += chunk);\n      res.on('end', () => {\n        if (res.statusCode >= 400) reject(new Error(`HTTP ${res.statusCode}: ${data}`));\n        else resolve(JSON.parse(data));\n      });\n    });\n    req.setTimeout(15000, () => { req.destroy(); reject(new Error('Request timeout')); });\n    req.on('error', reject);\n    req.end();\n  });\n}\n\nconst prFilesData = await ghGet(`/repos/${ctx.repoOwner}/${ctx.repoName}/pulls/${ctx.prNumber}/files?per_page=30`);\nconst tsFiles = prFilesData.filter(f => /\\.(ts|tsx)$/.test(f.filename));\n\nconst otherFiles = await Promise.all(\n  tsFiles\n    .filter(f => f.filename !== ctx.filePath)\n    .map(async (f) => {\n      const d = await ghGet(`/repos/${ctx.repoOwner}/${ctx.repoName}/contents/${f.filename}?ref=${ctx.headBranch}`);\n      const content = Buffer.from((d.content || '').replace(/\\n/g, ''), 'base64').toString('utf8');\n      return { path: f.filename, content, sha: d.sha };\n    })\n);\n\nconst allFiles = [\n  { path: ctx.filePath, content: fileContent, sha: fileSha },\n  ...otherFiles\n];\n\nconst otherFilesContext = otherFiles.length > 0\n  ? '\\n\\n## PR\uc758 \ub2e4\ub978 \ubcc0\uacbd \ud30c\uc77c (\ucc38\uace0\uc6a9)\\n\\n' + otherFiles.map(f =>\n      `### ${f.path}\\n\\`\\`\\`typescript\\n${f.content}\\n\\`\\`\\``\n    ).join('\\n\\n')\n  : '';\n\nconst userPrompt = `## \uc218\uc815 \uc694\uccad\\n\\nPR: #${ctx.prNumber} ${ctx.prTitle}\\n\\n## \ucf54\uba58\ud2b8 (${commentAuthor}, ${ctx.filePath} line ${commentLine})\\n\\n${commentBody}\\n\\n## \ud574\ub2f9 \ucf54\ub4dc \ucee8\ud14d\uc2a4\ud2b8\\n\\n\\`\\`\\`diff\\n${commentDiff}\\n\\`\\`\\`\\n\\n## \ucf54\uba58\ud2b8 \ub2ec\ub9b0 \ud30c\uc77c \uc804\uccb4\\n\\n\\`\\`\\`typescript\\n${fileContent}\\n\\`\\`\\`${otherFilesContext}\\n\\n---\\n\\n\ucf54\uba58\ud2b8 \ub0b4\uc6a9\uc744 \ubc18\uc601\ud558\uc138\uc694. \uc218\uc815\uc774 \uc5ec\ub7ec \ud30c\uc77c\uc5d0 \uac78\uccd0 \ud544\uc694\ud558\uba74 \uac01 \ud30c\uc77c\ub9c8\ub2e4 write_file\uc744 \ud638\ucd9c\ud558\uc138\uc694. \ucf54\uba58\ud2b8 \uc758\ub3c4\uac00 \uba85\ud655\ud558\uace0 \uc218\uc815 \ubc29\ud5a5\uc774 \ud655\uc2e4\ud55c \uacbd\uc6b0\uc5d0\ub9cc write_file\uc744 \ud638\ucd9c\ud558\uc138\uc694.`;\n\nconst geminiPayload = {\n  system_instruction: {\n    parts: [{ text: 'TypeScript \uc804\ubb38 \uac1c\ubc1c\uc790\uc785\ub2c8\ub2e4. PR \ub9ac\ubdf0 \ucf54\uba58\ud2b8\ub97c \ubc18\uc601\ud574 \ucf54\ub4dc\ub97c \uc218\uc815\ud569\ub2c8\ub2e4.\\n\\n## \ud544\uc218 \uaddc\uce59\\n\\n### Prettier \ud3ec\ub9f7\\n- \ub4e4\uc5ec\uc4f0\uae30: \uc2a4\ud398\uc774\uc2a4 2\uce78, \uc138\ubbf8\ucf5c\ub860 \ud56d\uc0c1, \uc2f1\uae00\ucffc\ud2b8, \ucd5c\ub300 100\uc790, trailing comma \ud56d\uc0c1\\n\\n### \uc218\uc815 \uc6d0\uce59\\n- \ucf54\uba58\ud2b8 \uc758\ub3c4\uac00 \uba85\ud655\ud558\uace0 \uc218\uc815 \ubc29\ud5a5\uc774 \ud655\uc2e4\ud55c \uacbd\uc6b0\uc5d0\ub9cc write_file \ud638\ucd9c\\n- \uc218\uc815\uc774 \uc5ec\ub7ec \ud30c\uc77c\uc5d0 \uac78\uccd0 \ud544\uc694\ud558\uba74 \uac01 \ud30c\uc77c\ub9c8\ub2e4 write_file\uc744 \ud638\ucd9c (\ud55c \ubc88\uc5d0 \ubaa8\ub450 \uc81c\ucd9c)\\n- \ubaa8\ud638\ud558\uba74 write_file \ud638\ucd9c\ud558\uc9c0 \uc54a\uc74c\\n- \uae30\uc874 Public API \ud558\uc704 \ud638\ud658\uc131 \uc720\uc9c0' }]\n  },\n  contents: [{ role: 'user', parts: [{ text: userPrompt }] }],\n  tools: [{\n    functionDeclarations: [{\n      name: 'write_file',\n      description: '\uc218\uc815\ub41c \ud30c\uc77c\uc758 \uc804\uccb4 \ub0b4\uc6a9\uc744 \uc81c\ucd9c\ud569\ub2c8\ub2e4. \uc5ec\ub7ec \ud30c\uc77c \uc218\uc815 \uc2dc \ud30c\uc77c\ub9c8\ub2e4 \ud55c \ubc88\uc529 \ud638\ucd9c\ud558\uc138\uc694.',\n      parameters: {\n        type: 'OBJECT',\n        properties: {\n          path: { type: 'STRING', description: '\ud30c\uc77c \uacbd\ub85c' },\n          content: { type: 'STRING', description: '\ud30c\uc77c \uc804\uccb4 \ub0b4\uc6a9' },\n          description: { type: 'STRING', description: '\ubcc0\uacbd \uc124\uba85' }\n        },\n        required: ['path', 'content', 'description']\n      }\n    }]\n  }],\n  toolConfig: { functionCallingConfig: { mode: 'ANY' } },\n  generationConfig: { maxOutputTokens: 16384 }\n};\n\nreturn [{\n  json: {\n    ...ctx,\n    allFiles,\n    fileSha,\n    fileContent,\n    commentBody,\n    commentLine,\n    geminiPayload\n  }\n}];"
      },
      "id": "prepare-fix",
      "name": "[fix] Gemini \ud504\ub86c\ud504\ud2b8 \uc900\ube44",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1340,
        680
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={{ $env.GEMINI_API_KEY }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ $json.geminiPayload }}",
        "options": {}
      },
      "id": "gemini-fix",
      "name": "[fix] Gemini: \ucf54\ub4dc \uc218\uc815",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1560,
        680
      ]
    },
    {
      "parameters": {
        "jsCode": "const response = $input.first().json;\nconst ctx = $('[fix] Gemini \ud504\ub86c\ud504\ud2b8 \uc900\ube44').first().json;\n\nif (!response.candidates || response.candidates.length === 0) {\n  return [{ json: { noChanges: true, geminiError: JSON.stringify(response.promptFeedback || response.error || 'no candidates'), ...ctx } }];\n}\n\nconst candidate = response.candidates[0];\nif (!candidate.content || !candidate.content.parts || candidate.finishReason === 'MALFORMED_FUNCTION_CALL') {\n  return [{ json: { noChanges: true, geminiError: candidate.finishReason || 'no content', ...ctx } }];\n}\n\nconst parts = candidate.content.parts;\nconst functionCalls = parts.filter(p => p.functionCall && p.functionCall.name === 'write_file');\n\nif (functionCalls.length === 0) {\n  return [{ json: { noChanges: true, ...ctx } }];\n}\n\nconst fixedFiles = functionCalls.map(p => {\n  const rawContent = p.functionCall.args.content || ''; const content = rawContent.replace(/^```[\\w]*\\n?/, '').replace(/\\n?```$/, '').trim();\n  const original = (ctx.allFiles || []).find(f => f.path === p.functionCall.args.path);\n  return {\n    path: p.functionCall.args.path,\n    content,\n    description: p.functionCall.args.description,\n    sha: original?.sha || null\n  };\n});\n\nconst filesSummary = fixedFiles.map(f => `\u2022 \\`${f.path}\\`: ${f.description}`).join('\\n');\n\nreturn [{\n  json: {\n    noChanges: false,\n    ...ctx,\n    fixedFiles,\n    filesSummary\n  }\n}];"
      },
      "id": "parse-fix",
      "name": "[fix] \uc218\uc815 \uacb0\uacfc \ud30c\uc2f1",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1780,
        680
      ]
    },
    {
      "parameters": {
        "jsCode": "const data = $input.first().json;\nif (data.noChanges || !data.fixedFiles || data.fixedFiles.length === 0) return [];\nconst fixedFiles = data.fixedFiles.filter(f => f.path && f.content && f.content.trim().length > 0);\nif (fixedFiles.length === 0) return [];\nconst { headBranch, repoOwner, repoName, filesSummary } = data;\nconst token = $env.GITHUB_TOKEN;\nconst baseUrl = `https://api.github.com/repos/${repoOwner}/${repoName}`;\n\nconst headers = {\n  'Authorization': `Bearer ${token}`,\n  'Accept': 'application/vnd.github+json',\n  'X-GitHub-Api-Version': '2022-11-28',\n  'Content-Type': 'application/json',\n  'User-Agent': 'n8n-jump-section'\n};\n\nfunction request(method, url, body) {\n  return new Promise((resolve, reject) => {\n    const https = require('https');\n    const path = url.replace('https://api.github.com', '');\n    const options = { hostname: 'api.github.com', path, method, headers };\n    const req = https.request(options, (res) => {\n      let d = '';\n      res.on('data', chunk => d += chunk);\n      res.on('end', () => {\n        if (res.statusCode >= 400) reject(new Error(`HTTP ${res.statusCode}: ${d}`));\n        else resolve(JSON.parse(d));\n      });\n    });\n    req.setTimeout(15000, () => { req.destroy(); reject(new Error('Request timeout')); });\n    req.on('error', reject);\n    if (body) req.write(JSON.stringify(body));\n    req.end();\n  });\n}\n\nconst refData = await request('GET', `${baseUrl}/git/refs/heads/${headBranch}`);\nconst baseSha = refData.object.sha;\nconst commitData = await request('GET', `${baseUrl}/git/commits/${baseSha}`);\nconst baseTreeSha = commitData.tree.sha;\n\nconst newTree = await request('POST', `${baseUrl}/git/trees`, {\n  base_tree: baseTreeSha,\n  tree: fixedFiles.map(f => ({ path: f.path, mode: '100644', type: 'blob', content: f.content }))\n});\n\nconst newCommit = await request('POST', `${baseUrl}/git/commits`, {\n  message: `fix: apply review comment suggestion\\n\\n${filesSummary}`,\n  tree: newTree.sha,\n  parents: [baseSha]\n});\n\nawait request('PATCH', `${baseUrl}/git/refs/heads/${headBranch}`, { sha: newCommit.sha });\n\nawait request('POST', `${baseUrl}/actions/workflows/auto-format.yml/dispatches`, {\n  ref: headBranch,\n  inputs: { branch: headBranch }\n});\n\nreturn [{ json: { ...data, commitSha: newCommit.sha } }];"
      },
      "id": "commit-fix",
      "name": "[fix] GitHub: \ucee4\ubc0b",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2000,
        680
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://api.github.com/repos/{{ $json.repoOwner }}/{{ $json.repoName }}/pulls/{{ $json.prNumber }}/comments/{{ $json.commentId }}/replies",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept",
              "value": "application/vnd.github+json"
            },
            {
              "name": "X-GitHub-Api-Version",
              "value": "2022-11-28"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Authorization",
              "value": "=Bearer {{ $env.GITHUB_TOKEN }}"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ { body: '\u2705 \uc218\uc815 \uc644\ub8cc\\n\\n' + $json.filesSummary + '\\n\\ncommit: `' + $json.commitSha.slice(0, 7) + '`' } }}",
        "options": {}
      },
      "id": "reply-fix",
      "name": "[fix] GitHub: \ucf54\uba58\ud2b8 \ub2f5\uc7a5",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2220,
        680
      ]
    }
  ],
  "connections": {
    "Webhook: PR Comment Event": {
      "main": [
        [
          {
            "node": "IF: action=created",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF: action=created": {
      "main": [
        [
          {
            "node": "\ucee4\ub9e8\ub4dc \uac10\uc9c0",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ucee4\ub9e8\ub4dc \uac10\uc9c0": {
      "main": [
        [
          {
            "node": "IF: fix-build?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF: fix-build?": {
      "main": [
        [
          {
            "node": "[build] \ud30c\uc77c + CI \uc870\ud68c \ubc0f \ud504\ub86c\ud504\ud2b8 \uc900\ube44",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "IF: fix-lint?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF: fix-lint?": {
      "main": [
        [
          {
            "node": "[lint] \ud30c\uc77c \uc870\ud68c \ubc0f \ud504\ub86c\ud504\ud2b8 \uc900\ube44",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "[fix] GitHub: \uc6d0\ubcf8 \ucf54\uba58\ud2b8 \uc870\ud68c",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "[build] \ud30c\uc77c + CI \uc870\ud68c \ubc0f \ud504\ub86c\ud504\ud2b8 \uc900\ube44": {
      "main": [
        [
          {
            "node": "[build] Gemini: \ucf54\ub4dc \uc218\uc815",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "[build] Gemini: \ucf54\ub4dc \uc218\uc815": {
      "main": [
        [
          {
            "node": "[build] \uc218\uc815 \uacb0\uacfc \ud30c\uc2f1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "[build] \uc218\uc815 \uacb0\uacfc \ud30c\uc2f1": {
      "main": [
        [
          {
            "node": "[build] IF: \uc218\uc815\ub41c \ud30c\uc77c \uc788\ub294\uc9c0",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "[build] IF: \uc218\uc815\ub41c \ud30c\uc77c \uc788\ub294\uc9c0": {
      "main": [
        [
          {
            "node": "[build] GitHub: \ucee4\ubc0b",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "[build] GitHub: \ucee4\ubc0b": {
      "main": [
        [
          {
            "node": "[build] GitHub: PR \ucf54\uba58\ud2b8",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "[lint] \ud30c\uc77c \uc870\ud68c \ubc0f \ud504\ub86c\ud504\ud2b8 \uc900\ube44": {
      "main": [
        [
          {
            "node": "[lint] Gemini: \ud3ec\ub9f7 \uc218\uc815",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "[lint] Gemini: \ud3ec\ub9f7 \uc218\uc815": {
      "main": [
        [
          {
            "node": "[lint] \uc218\uc815 \uacb0\uacfc \ud30c\uc2f1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "[lint] \uc218\uc815 \uacb0\uacfc \ud30c\uc2f1": {
      "main": [
        [
          {
            "node": "[lint] IF: \uc218\uc815\ub41c \ud30c\uc77c \uc788\ub294\uc9c0",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "[lint] IF: \uc218\uc815\ub41c \ud30c\uc77c \uc788\ub294\uc9c0": {
      "main": [
        [
          {
            "node": "[lint] GitHub: \ucee4\ubc0b",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "[lint] GitHub: \ucee4\ubc0b": {
      "main": [
        [
          {
            "node": "[lint] GitHub: PR \ucf54\uba58\ud2b8",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "[fix] GitHub: \uc6d0\ubcf8 \ucf54\uba58\ud2b8 \uc870\ud68c": {
      "main": [
        [
          {
            "node": "[fix] GitHub: \ud30c\uc77c \ub0b4\uc6a9 \uc77d\uae30",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "[fix] GitHub: \ud30c\uc77c \ub0b4\uc6a9 \uc77d\uae30": {
      "main": [
        [
          {
            "node": "[fix] Gemini \ud504\ub86c\ud504\ud2b8 \uc900\ube44",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "[fix] Gemini \ud504\ub86c\ud504\ud2b8 \uc900\ube44": {
      "main": [
        [
          {
            "node": "[fix] Gemini: \ucf54\ub4dc \uc218\uc815",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "[fix] Gemini: \ucf54\ub4dc \uc218\uc815": {
      "main": [
        [
          {
            "node": "[fix] \uc218\uc815 \uacb0\uacfc \ud30c\uc2f1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "[fix] \uc218\uc815 \uacb0\uacfc \ud30c\uc2f1": {
      "main": [
        [
          {
            "node": "[fix] GitHub: \ucee4\ubc0b",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "[fix] GitHub: \ucee4\ubc0b": {
      "main": [
        [
          {
            "node": "[fix] GitHub: \ucf54\uba58\ud2b8 \ub2f5\uc7a5",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "tags": [
    {
      "name": "jump-section"
    },
    {
      "name": "comment-fix"
    }
  ]
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

How this works

This workflow automates the resolution of common code issues in pull requests by detecting specific commands in comments and applying targeted fixes, saving developers hours of manual debugging and ensuring cleaner code merges. It is ideal for teams using GitHub or similar platforms who want to streamline their review process without interrupting their flow. The key step involves a webhook trigger that captures new PR comments, followed by conditional checks to identify fix requests, culminating in an HTTP request to a service like Gemini for generating precise code corrections based on the project's context.

Use this pipeline when your repository frequently encounters repeatable errors such as build failures or linting violations that can be addressed via AI-assisted patches, particularly in collaborative environments with multiple contributors. Avoid it for complex, context-heavy bugs requiring human oversight or in non-code repositories. Common variations include adapting the command detection for different issue types or integrating with additional tools like Slack for fix notifications.

About this workflow

jump-section: Comment Fix Pipeline. Uses httpRequest. Webhook trigger; 24 nodes.

Source: https://github.com/bae080311/jump-section/blob/c566471882fa8b8f0e4d00704693b7c92d62c519/.n8n/workflows/comment-fix-pipeline.json — original creator credit. Request a take-down →

More General workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

General

Portfolio Orchestrator. Uses httpRequest. Webhook trigger; 59 nodes.

HTTP Request
General

GitHub Issues Router (Linear / Jira / ClickUp). Uses stickyNote, httpRequest, respondToWebhook. Webhook trigger; 23 nodes.

HTTP Request
General

Form to CRM Lead Router (Pipedrive / HubSpot / Salesforce). Uses stickyNote, httpRequest, respondToWebhook. Webhook trigger; 22 nodes.

HTTP Request
General

Calendly to CRM Sync (Pipedrive / HubSpot / Salesforce). Uses stickyNote, httpRequest, respondToWebhook. Webhook trigger; 22 nodes.

HTTP Request
General

Reputation Engine — Technical SEO Implementer. Uses httpRequest. Webhook trigger; 22 nodes.

HTTP Request