{
  "name": "CodeBrewerz — Pipeline failure digest",
  "nodes": [
    {
      "parameters": {
        "content": "## Pipeline failure digest\n\nEvery weekday morning, pulls the last 24 hours of GitLab CI pipelines for one\nproject, counts them by status, and posts to Slack only when something failed.\nA green day stays silent, so the alert keeps meaning something.\n\n### Before you activate it\n\n1. Create a **GitLab API** credential (a personal or project access token with\n   `read_api`) and attach it to *Fetch pipelines*.\n2. In *Fetch pipelines*, replace `PROJECT_ID` in the URL with your numeric\n   project id, URL-encoded path, or point it at your self-hosted host.\n3. Create a **Slack** credential with `chat:write` and attach it to\n   *Post digest*. Change `#engineering` to your channel.\n4. Run **Execute workflow** once to see the shape of the summary, then activate.\n\nSwap the GitLab call for the GitHub Actions or Bitbucket equivalent and the\nrest of the workflow is unchanged — only *Fetch pipelines* knows the provider.\n\nMIT licensed. Review it before you point production traffic at it.",
        "height": 440,
        "width": 420
      },
      "id": "8a1b2c3d-0001-4b20-8e02-3c4d5e6f7081",
      "name": "Setup",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [-460, 60]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "days",
              "triggerAtHour": 9,
              "triggerAtMinute": 0
            }
          ]
        }
      },
      "id": "8a1b2c3d-0002-4b20-8e02-3c4d5e6f7081",
      "name": "Every weekday 09:00",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.1,
      "position": [0, 300]
    },
    {
      "parameters": {
        "url": "https://gitlab.com/api/v4/projects/PROJECT_ID/pipelines",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "gitlabApi",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "updated_after",
              "value": "={{ $now.minus(1, 'days').toISO() }}"
            },
            {
              "name": "per_page",
              "value": "100"
            }
          ]
        },
        "options": {}
      },
      "id": "8a1b2c3d-0003-4b20-8e02-3c4d5e6f7081",
      "name": "Fetch pipelines",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "position": [220, 300]
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// One item in, one summary out. `items` is every pipeline the API returned.\nconst pipelines = items.map((item) => item.json);\n\nconst byStatus = {};\nfor (const pipeline of pipelines) {\n  const status = pipeline.status || 'unknown';\n  byStatus[status] = (byStatus[status] || 0) + 1;\n}\n\nconst failedPipelines = pipelines.filter((p) => p.status === 'failed');\nconst total = pipelines.length;\nconst failed = failedPipelines.length;\n\nreturn [\n  {\n    json: {\n      total,\n      failed,\n      passRate: total === 0 ? 100 : Math.round(((total - failed) / total) * 100),\n      byStatus,\n      failedRefs: [...new Set(failedPipelines.map((p) => p.ref))],\n      failedUrls: failedPipelines.slice(0, 5).map((p) => p.web_url),\n    },\n  },\n];\n"
      },
      "id": "8a1b2c3d-0004-4b20-8e02-3c4d5e6f7081",
      "name": "Summarise",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [440, 300]
    },
    {
      "parameters": {
        "conditions": {
          "number": [
            {
              "value1": "={{ $json.failed }}",
              "operation": "larger",
              "value2": 0
            }
          ]
        }
      },
      "id": "8a1b2c3d-0005-4b20-8e02-3c4d5e6f7081",
      "name": "Anything failed?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 1,
      "position": [660, 300]
    },
    {
      "parameters": {
        "select": "channel",
        "channelId": "#engineering",
        "text": "=:rotating_light: *{{ $json.failed }} of {{ $json.total }} pipelines failed in the last 24h* ({{ $json.passRate }}% pass rate)\n\nBranches: {{ $json.failedRefs.join(', ') }}\n\n{{ $json.failedUrls.join('\\n') }}",
        "otherOptions": {}
      },
      "id": "8a1b2c3d-0006-4b20-8e02-3c4d5e6f7081",
      "name": "Post digest",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2,
      "position": [900, 180]
    },
    {
      "parameters": {},
      "id": "8a1b2c3d-0007-4b20-8e02-3c4d5e6f7081",
      "name": "Stay quiet",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [900, 420]
    }
  ],
  "pinData": {},
  "connections": {
    "Every weekday 09:00": {
      "main": [[{ "node": "Fetch pipelines", "type": "main", "index": 0 }]]
    },
    "Fetch pipelines": {
      "main": [[{ "node": "Summarise", "type": "main", "index": 0 }]]
    },
    "Summarise": {
      "main": [[{ "node": "Anything failed?", "type": "main", "index": 0 }]]
    },
    "Anything failed?": {
      "main": [
        [{ "node": "Post digest", "type": "main", "index": 0 }],
        [{ "node": "Stay quiet", "type": "main", "index": 0 }]
      ]
    }
  },
  "active": false,
  "settings": { "executionOrder": "v1" },
  "versionId": "8a1b2c3d-1000-4b20-8e02-3c4d5e6f7081",
  "meta": { "templateCredsSetupCompleted": false },
  "tags": []
}
