{
  "name": "Deploy Gate",
  "nodes": [
    {
      "parameters": {
        "path": "deploy-gate",
        "httpMethod": "POST",
        "responseMode": "onReceived",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 1.1,
      "position": [
        0,
        0
      ],
      "id": "a1b2c3d4-0001-4000-8000-000000000001",
      "name": "Webhook"
    },
    {
      "parameters": {
        "jsCode": "const body = $input.first().json.body || {};\nconst service = (body.service || '').trim();\nconst version = (body.version || '').trim();\nconst deployer = (body.deployer || 'unknown').trim();\nconst healthUrl = (body.healthUrl || '').trim();\n\nif (!service) {\n  throw new Error('Missing required field: service');\n}\n\nreturn [{\n  json: {\n    service,\n    version,\n    deployer,\n    healthUrl,\n    timestamp: $now.toISO(),\n    hour: $now.hour,\n    weekday: $now.weekday\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        220,
        0
      ],
      "id": "a1b2c3d4-0002-4000-8000-000000000002",
      "name": "Validate Deploy"
    },
    {
      "parameters": {
        "operation": "availability",
        "calendar": {
          "__rl": true,
          "mode": "id",
          "value": "new17353@gmail.com"
        },
        "timeMin": "={{ $now }}",
        "timeMax": "={{ $now.plus(1, 'hour') }}"
      },
      "type": "n8n-nodes-base.googleCalendar",
      "typeVersion": 1,
      "position": [
        440,
        -160
      ],
      "id": "a1b2c3d4-0003-4000-8000-000000000003",
      "name": "Check Calendar",
      "onError": "continueRegularOutput",
      "credentials": {
        "googleCalendarOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "var items = $input.all();\nvar deployData = $('Validate Deploy').first().json;\nvar firstJson = (items[0] && items[0].json) ? items[0].json : {};\nvar error = firstJson.error;\n\nif (error) {\n  return [{ json: { check: 'calendar', passed: true, reason: 'Calendar check failed, assuming clear', service: deployData.service } }];\n}\n\nvar hasBusy = false;\nfor (var i = 0; i < items.length; i++) {\n  var j = (items[i] && items[i].json) ? items[i].json : {};\n  if (j.status === 'busy' || j.busy === true || j.availability === 'busy') {\n    hasBusy = true;\n    break;\n  }\n}\n\nvar reason = hasBusy ? 'Calendar shows busy in next 60 min' : 'No calendar conflicts in next 60 min';\nreturn [{ json: { check: 'calendar', passed: !hasBusy, reason: reason, service: deployData.service } }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        660,
        -160
      ],
      "id": "a1b2c3d4-0004-4000-8000-000000000004",
      "name": "Parse Calendar"
    },
    {
      "parameters": {
        "operation": "getAll",
        "returnAll": false,
        "limit": 10,
        "filters": {},
        "options": {}
      },
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 1,
      "position": [
        440,
        0
      ],
      "id": "a1b2c3d4-0005-4000-8000-000000000005",
      "name": "Check Emails",
      "onError": "continueRegularOutput",
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const items = $input.all();\nconst deployData = $('Validate Deploy').first().json;\nconst error = items[0]?.json?.error;\n\nif (error) {\n  return [{ json: { check: 'email', passed: true, reason: 'Gmail check failed, assuming no freeze', service: deployData.service } }];\n}\n\nvar freezeFound = false;\nvar freezeSubject = '';\nvar freezeKeywords = ['deploy freeze', 'code freeze', 'deployment freeze', 'release freeze', 'change freeze'];\n\nfor (var i = 0; i < items.length; i++) {\n  var subject = (items[i].json.subject || '').toLowerCase();\n  var snippet = (items[i].json.snippet || '').toLowerCase();\n  var text = subject + ' ' + snippet;\n  for (var k = 0; k < freezeKeywords.length; k++) {\n    if (text.indexOf(freezeKeywords[k]) !== -1) {\n      freezeFound = true;\n      freezeSubject = items[i].json.subject || '';\n      break;\n    }\n  }\n  if (freezeFound) break;\n}\n\nvar reason = freezeFound ? 'Freeze email found: ' + freezeSubject : 'No deploy freeze emails found (' + items.length + ' checked)';\nreturn [{ json: { check: 'email', passed: !freezeFound, reason: reason, service: deployData.service } }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        660,
        0
      ],
      "id": "a1b2c3d4-0006-4000-8000-000000000006",
      "name": "Parse Freeze Emails"
    },
    {
      "parameters": {
        "url": "={{ $json.healthUrl }}",
        "options": {
          "timeout": 5000,
          "redirect": {
            "redirect": {}
          }
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        440,
        160
      ],
      "id": "a1b2c3d4-0007-4000-8000-000000000007",
      "name": "Check Service Health",
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "const item = $input.first().json;\nconst deployData = $('Validate Deploy').first().json;\nvar error = item.error;\nvar healthUrl = deployData.healthUrl;\n\nif (!healthUrl) {\n  return [{ json: { check: 'health', passed: true, reason: 'No health URL provided, skipping check', service: deployData.service } }];\n}\n\nif (error) {\n  var msg = typeof error === 'object' ? (error.message || JSON.stringify(error)) : String(error);\n  return [{ json: { check: 'health', passed: false, reason: 'Health check failed: ' + msg, service: deployData.service } }];\n}\n\nreturn [{ json: { check: 'health', passed: true, reason: 'Service responding OK at ' + healthUrl, service: deployData.service } }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        660,
        160
      ],
      "id": "a1b2c3d4-0008-4000-8000-000000000008",
      "name": "Parse Health"
    },
    {
      "parameters": {
        "mode": "append",
        "numberInputs": 3,
        "options": {}
      },
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3,
      "position": [
        880,
        0
      ],
      "id": "a1b2c3d4-0009-4000-8000-000000000009",
      "name": "Merge Checks"
    },
    {
      "parameters": {
        "jsCode": "var checks = $input.all();\nvar deployData = $('Validate Deploy').first().json;\nvar allPassed = true;\nvar failures = [];\nvar warnings = [];\nvar NL = String.fromCharCode(10);\n\nfor (var i = 0; i < checks.length; i++) {\n  var c = checks[i].json;\n  if (!c.passed) {\n    allPassed = false;\n    failures.push(c.check + ': ' + c.reason);\n  }\n}\n\nvar hour = deployData.hour;\nvar weekday = deployData.weekday;\nvar isWeekend = weekday === 6 || weekday === 7;\nvar isLateNight = hour < 6 || hour >= 22;\n\nif (isWeekend) warnings.push('Weekend deployment');\nif (isLateNight) warnings.push('Late night deployment (hour: ' + hour + ')');\n\nvar decision = 'approve';\nif (failures.length > 0) {\n  decision = 'deny';\n} else if (warnings.length > 0) {\n  decision = 'review';\n}\n\nvar lines = [];\nlines.push('**Deploy Gate: ' + deployData.service + ' v' + deployData.version + '**');\nlines.push('Deployer: ' + deployData.deployer);\nlines.push('Time: ' + deployData.timestamp);\nlines.push('');\n\nfor (var j = 0; j < checks.length; j++) {\n  var ch = checks[j].json;\n  var icon = ch.passed ? '\\u2705' : '\\u274c';\n  lines.push(icon + ' ' + ch.check + ': ' + ch.reason);\n}\n\nif (warnings.length > 0) {\n  lines.push('');\n  lines.push('\\u26a0\\ufe0f Warnings:');\n  for (var w = 0; w < warnings.length; w++) {\n    lines.push('  - ' + warnings[w]);\n  }\n}\n\nlines.push('');\nlines.push('Decision: **' + decision.toUpperCase() + '**');\n\nvar report = lines.join(NL);\nreturn [{ json: { decision: decision, report: report, service: deployData.service, version: deployData.version, deployer: deployData.deployer, failureCount: failures.length, warningCount: warnings.length } }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1100,
        0
      ],
      "id": "a1b2c3d4-0010-4000-8000-000000000010",
      "name": "Evaluate Decision"
    },
    {
      "parameters": {
        "mode": "rules",
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "cond-approve",
                    "leftValue": "={{ $json.decision }}",
                    "rightValue": "approve",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputLabel": "Approve"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "cond-deny",
                    "leftValue": "={{ $json.decision }}",
                    "rightValue": "deny",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputLabel": "Deny"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "cond-review",
                    "leftValue": "={{ $json.decision }}",
                    "rightValue": "review",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputLabel": "Review"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3,
      "position": [
        1320,
        0
      ],
      "id": "a1b2c3d4-0011-4000-8000-000000000011",
      "name": "Route Decision"
    },
    {
      "parameters": {
        "mode": "manual",
        "assignments": {
          "assignments": [
            {
              "id": "set-approve-1",
              "name": "emoji",
              "value": "=\\u2705",
              "type": "string"
            },
            {
              "id": "set-approve-2",
              "name": "status",
              "value": "APPROVED",
              "type": "string"
            },
            {
              "id": "set-approve-3",
              "name": "message",
              "value": "={{ $json.report }}",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1540,
        -160
      ],
      "id": "a1b2c3d4-0012-4000-8000-000000000012",
      "name": "Set Approved"
    },
    {
      "parameters": {
        "mode": "manual",
        "assignments": {
          "assignments": [
            {
              "id": "set-deny-1",
              "name": "emoji",
              "value": "=\\u274c",
              "type": "string"
            },
            {
              "id": "set-deny-2",
              "name": "status",
              "value": "DENIED",
              "type": "string"
            },
            {
              "id": "set-deny-3",
              "name": "message",
              "value": "={{ $json.report }}",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1540,
        0
      ],
      "id": "a1b2c3d4-0013-4000-8000-000000000013",
      "name": "Set Denied"
    },
    {
      "parameters": {
        "mode": "manual",
        "assignments": {
          "assignments": [
            {
              "id": "set-review-1",
              "name": "emoji",
              "value": "=\\u26a0\\ufe0f",
              "type": "string"
            },
            {
              "id": "set-review-2",
              "name": "status",
              "value": "NEEDS REVIEW",
              "type": "string"
            },
            {
              "id": "set-review-3",
              "name": "message",
              "value": "={{ $json.report }}",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1540,
        160
      ],
      "id": "a1b2c3d4-0014-4000-8000-000000000014",
      "name": "Set Review"
    },
    {
      "parameters": {
        "authentication": "webhook",
        "content": "={{ $json.message }}",
        "options": {}
      },
      "type": "n8n-nodes-base.discord",
      "typeVersion": 2,
      "position": [
        1760,
        -160
      ],
      "id": "a1b2c3d4-0015-4000-8000-000000000015",
      "name": "Discord Approved",
      "credentials": {
        "discordWebhookApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "authentication": "webhook",
        "content": "={{ $json.message }}",
        "options": {}
      },
      "type": "n8n-nodes-base.discord",
      "typeVersion": 2,
      "position": [
        1760,
        0
      ],
      "id": "a1b2c3d4-0016-4000-8000-000000000016",
      "name": "Discord Denied",
      "credentials": {
        "discordWebhookApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "authentication": "webhook",
        "content": "={{ $json.message }}",
        "options": {}
      },
      "type": "n8n-nodes-base.discord",
      "typeVersion": 2,
      "position": [
        1760,
        160
      ],
      "id": "a1b2c3d4-0017-4000-8000-000000000017",
      "name": "Discord Review",
      "credentials": {
        "discordWebhookApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "var data = $input.first().json;\nreturn [{ json: { completedAt: $now.toISO(), service: data.service, version: data.version, deployer: data.deployer, decision: data.decision, status: data.status } }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1980,
        0
      ],
      "id": "a1b2c3d4-0018-4000-8000-000000000018",
      "name": "Log Result"
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.errorTrigger",
      "typeVersion": 1,
      "position": [
        880,
        -280
      ],
      "id": "a1b2c3d4-0019-4000-8000-000000000019",
      "name": "Error Trigger"
    },
    {
      "parameters": {
        "authentication": "webhook",
        "content": "=\u274c Deploy Gate Error: {{ $json.execution.error.message }}",
        "options": {}
      },
      "type": "n8n-nodes-base.discord",
      "typeVersion": 2,
      "position": [
        1100,
        -280
      ],
      "id": "a1b2c3d4-0020-4000-8000-000000000020",
      "name": "Error to Discord",
      "credentials": {
        "discordWebhookApi": {
          "name": "<your credential>"
        }
      }
    }
  ],
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "Validate Deploy",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Deploy": {
      "main": [
        [
          {
            "node": "Check Calendar",
            "type": "main",
            "index": 0
          },
          {
            "node": "Check Emails",
            "type": "main",
            "index": 0
          },
          {
            "node": "Check Service Health",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Calendar": {
      "main": [
        [
          {
            "node": "Parse Calendar",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Emails": {
      "main": [
        [
          {
            "node": "Parse Freeze Emails",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Service Health": {
      "main": [
        [
          {
            "node": "Parse Health",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Calendar": {
      "main": [
        [
          {
            "node": "Merge Checks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Freeze Emails": {
      "main": [
        [
          {
            "node": "Merge Checks",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Parse Health": {
      "main": [
        [
          {
            "node": "Merge Checks",
            "type": "main",
            "index": 2
          }
        ]
      ]
    },
    "Merge Checks": {
      "main": [
        [
          {
            "node": "Evaluate Decision",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Evaluate Decision": {
      "main": [
        [
          {
            "node": "Route Decision",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route Decision": {
      "main": [
        [
          {
            "node": "Set Approved",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Set Denied",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Set Review",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Approved": {
      "main": [
        [
          {
            "node": "Discord Approved",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Denied": {
      "main": [
        [
          {
            "node": "Discord Denied",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Review": {
      "main": [
        [
          {
            "node": "Discord Review",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Discord Approved": {
      "main": [
        [
          {
            "node": "Log Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Discord Denied": {
      "main": [
        [
          {
            "node": "Log Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Discord Review": {
      "main": [
        [
          {
            "node": "Log Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Error Trigger": {
      "main": [
        [
          {
            "node": "Error to Discord",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}