{
  "name": "GitHub CI Failure Monitor",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 10
            }
          ]
        }
      },
      "id": "schedule-trigger",
      "name": "Schedule (10\ubd84)",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        240,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "const staticData = $getWorkflowStaticData('global');\nconst lastCheckedAt = staticData.last_checked_at ||\n  new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();\nstaticData.last_checked_at = new Date().toISOString();\nreturn [{ json: { last_checked_at: lastCheckedAt } }];"
      },
      "id": "read-cursor",
      "name": "\ucee4\uc11c \uc77d\uae30\u00b7\uac31\uc2e0",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        340,
        300
      ]
    },
    {
      "parameters": {
        "url": "https://api.github.com/repos/rumdice/platformA/actions/runs",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "status",
              "value": "failure"
            },
            {
              "name": "per_page",
              "value": "20"
            },
            {
              "name": "created",
              "value": "={{ '>=' + $('\ucee4\uc11c \uc77d\uae30\u00b7\uac31\uc2e0').first().json.last_checked_at }}"
            }
          ]
        },
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "id": "github-get-failed-runs",
      "name": "GitHub: \uc2e4\ud328\ud55c CI \uc870\ud68c",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        460,
        300
      ],
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "fieldToSplitOut": "body.workflow_runs",
        "options": {}
      },
      "id": "split-runs",
      "name": "\uac01 \uc2e4\ud328 run \ubd84\ub9ac",
      "type": "n8n-nodes-base.splitOut",
      "typeVersion": 1,
      "position": [
        680,
        300
      ]
    },
    {
      "parameters": {
        "url": "=https://api.github.com/repos/rumdice/platformA/actions/runs/{{ $json.id }}/jobs",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {}
      },
      "id": "github-get-jobs",
      "name": "GitHub: Job \uc0c1\uc138 \uc870\ud68c",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        900,
        300
      ],
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const jobs = $json.body.jobs || [];\nconst run = $('\uac01 \uc2e4\ud328 run \ubd84\ub9ac').first().json;\nconst branch = run.head_branch;\nconst runId = run.id;\nconst runName = run.name;\nconst commitSha = run.head_sha;\n\n// \uc2e4\ud328\ud55c job\uc758 steps\uc5d0\uc11c \uc2e4\ud328 \uc720\ud615 \ubd84\ub958\nlet failureType = 'build_failed';\nlet fixable = false;\nlet logExcerpt = '';\nlet githubJobId = null;\n\nfor (const job of jobs) {\n  if (job.conclusion !== 'failure') continue;\n  githubJobId = job.id;\n  for (const step of (job.steps || [])) {\n    if (step.conclusion !== 'failure') continue;\n    const stepName = (step.name || '').toLowerCase();\n    if (stepName.includes('format check (whitespace)') || stepName.includes('charset')) {\n      failureType = 'format_failed'; fixable = true;\n    } else if (stepName.includes('format check (style)')) {\n      failureType = 'style_failed'; fixable = true;\n    } else if (stepName.includes('test')) {\n      failureType = 'test_failed';\n    } else if (stepName.includes('gate-check') || stepName.includes('sdlc gate')) {\n      failureType = 'gate_failed';\n    }\n    logExcerpt = `Step: ${step.name} / Job: ${job.name}`;\n    break;\n  }\n  if (logExcerpt) break;\n}\n\nreturn [{\n  json: {\n    failure_type: failureType,\n    source: 'github_actions',\n    message: `CI \uc2e4\ud328: ${runName} / branch: ${branch} / run_id: ${runId}`,\n    log_excerpt: logExcerpt,\n    fixable_by_ai: fixable,\n    metadata: JSON.stringify({ branch, run_id: runId, run_name: runName }),\n    branch,\n    git_hub_run_id: runId,\n    git_hub_job_id: githubJobId,\n    commit_sha: commitSha,\n    workflow_name: runName\n  }\n}];"
      },
      "id": "classify-failure",
      "name": "\uc2e4\ud328 \ubd84\ub958",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1120,
        300
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO sdlc.ai_failures\n    (failure_type, source, message, log_excerpt, fixable_by_ai, resolved,\n     created_at, metadata, branch, git_hub_run_id, git_hub_job_id, commit_sha, workflow_name)\nVALUES ($1, $2, $3, $4, $5, false, NOW(), $6::jsonb, $7, $8, $9, $10, $11)\nON CONFLICT (git_hub_run_id, git_hub_job_id, failure_type)\nWHERE git_hub_run_id IS NOT NULL AND git_hub_job_id IS NOT NULL\nDO NOTHING",
        "additionalFields": {
          "queryParams": "={{ [$json.failure_type, $json.source, $json.message, $json.log_excerpt, $json.fixable_by_ai, $json.metadata, $json.branch, $json.git_hub_run_id, $json.git_hub_job_id, $json.commit_sha, $json.workflow_name] }}"
        }
      },
      "id": "postgres-insert",
      "name": "PostgreSQL: ai_failures INSERT",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        1340,
        300
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    }
  ],
  "connections": {
    "Schedule (10\ubd84)": {
      "main": [
        [
          {
            "node": "\ucee4\uc11c \uc77d\uae30\u00b7\uac31\uc2e0",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ucee4\uc11c \uc77d\uae30\u00b7\uac31\uc2e0": {
      "main": [
        [
          {
            "node": "GitHub: \uc2e4\ud328\ud55c CI \uc870\ud68c",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GitHub: \uc2e4\ud328\ud55c CI \uc870\ud68c": {
      "main": [
        [
          {
            "node": "\uac01 \uc2e4\ud328 run \ubd84\ub9ac",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\uac01 \uc2e4\ud328 run \ubd84\ub9ac": {
      "main": [
        [
          {
            "node": "GitHub: Job \uc0c1\uc138 \uc870\ud68c",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GitHub: Job \uc0c1\uc138 \uc870\ud68c": {
      "main": [
        [
          {
            "node": "\uc2e4\ud328 \ubd84\ub958",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\uc2e4\ud328 \ubd84\ub958": {
      "main": [
        [
          {
            "node": "PostgreSQL: ai_failures INSERT",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "tags": [
    {
      "name": "sdlc"
    },
    {
      "name": "monitoring"
    }
  ],
  "_comment": {
    "setup": [
      "1. n8n UI > Settings > Credentials > 'GitHub PAT' (HTTP Header Auth, Authorization: Bearer ghp_...)",
      "2. n8n UI > Settings > Credentials > 'SDLC PostgreSQL' (Host: postgres, Port: 5432, DB: platforma_sdlc, User: platforma)",
      "3. n8n UI > Workflows > Import from file > .n8n/workflows/github-failure-monitor.json",
      "4. \uc6cc\ud06c\ud50c\ub85c \ud65c\uc131\ud654 (Toggle Active)"
    ],
    "cursor": [
      "\ucee4\uc11c \uae30\ubc18 \ud3f4\ub9c1: n8n Static Data\uc5d0 last_checked_at \uc800\uc7a5 \u2192 \ud3f4\ub9c1 \uac04 \uc0c1\ud0dc \uc720\uc9c0",
      "\uae30\ubcf8\uac12: \uccab \uc2e4\ud589 \uc2dc 24\uc2dc\uac04 \uc804 (\ub178\ud2b8\ubd81 \uc7ac\uc2dc\uc791 \ud6c4\uc5d0\ub3c4 \uacfc\uac70 \uc2e4\ud328 \uc18c\uae09 \ucc98\ub9ac)",
      "\ub099\uad00\uc801 \uac31\uc2e0: \uc2e4\ud589 \uc2dc\uc791 \uc2dc \ucee4\uc11c \uc989\uc2dc \uac31\uc2e0 \u2192 ON CONFLICT DO NOTHING\uc73c\ub85c \uc911\ubcf5 INSERT \ubc29\uc5b4"
    ]
  }
}