{
  "id": "ABpNEZh2zrFMu9yj",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "API Mock Auto-Refresh",
  "tags": [],
  "nodes": [
    {
      "id": "b07ce725-2040-4454-83e1-6e072f23a6a5",
      "name": "Sticky Note - How It Works",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -880,
        -64
      ],
      "parameters": {
        "width": 452,
        "height": 1028,
        "content": "## How It Works\n\n\nThis process handles the full lifecycle of an OpenAPI spec update \u2014 from GitHub push to Postman mock refresh, spec comparison, PR comment and team notification.\n\nWhen a push lands on `develop`, the **GitHub Webhook** fires \u2192 config is loaded \u2192 **Postman mock is refreshed** \u2192 old and new specs are downloaded. GitHub Actions runs `openapi-diff` and POSTs the result back to the **Webhook OpenAPI-diff** node. If differences are found, they are parsed and cleaned, then **posted as a PR comment** and **emailed to the team**.\n\n\n---\n\n## Setup Steps\n\n1. **Import** this workflow JSON into your n8n instance.\n\n2. **Open `Set: Config`** and update:\n   - `postman.apiKey` \u2014 your Postman API key\n   - `postman.mockId` \u2014 your Postman Mock Server ID\n   - `github.repo` \u2014 your GitHub repo (format: `owner/repo`)\n   - `github.token` \u2014 a GitHub Personal Access Token with `repo` scope\n   - `github.prId` \u2014 the Pull Request ID to comment on\n\n3. **Configure your GitHub Webhook** to point to `/webhook/api-update` on the `develop` branch.\n\n4. **Connect your Gmail account** in the `Send Summary Email1` node.\n\n5. **Connect your GitHub credentials** in the `Comment to PR` node (Header Auth PR).\n\n6. **Set up openapi-diff** via Docker in GitHub Actions and configure it to POST results to `/webhook/api-diff-result`.\n\n7. **Activate the workflow** and test by pushing a spec change to `develop`.\n\n Never commit real API keys or tokens to source control \u2014 use n8n credentials instead."
      },
      "typeVersion": 1
    },
    {
      "id": "63156d49-3bcc-475d-8809-05b9a1547a44",
      "name": "Sticky Note - GitHub Webhook",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -320,
        -64
      ],
      "parameters": {
        "width": 432,
        "height": 348,
        "content": "###GitHub Webhook\n**Trigger \u2014 Entry Point**\n\nListens for `POST` requests at `/webhook/api-update`.\n\nFired by GitHub when a push or PR event occurs on the `develop` branch. Payload includes repo name, branch and PR details.\n\nConfigure this URL in your GitHub repo under **Settings \u2192 Webhooks**."
      },
      "typeVersion": 1
    },
    {
      "id": "38b57660-1662-44b7-8639-1a1c4e1f13c4",
      "name": "Sticky Note - Set Config",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        144,
        -64
      ],
      "parameters": {
        "height": 220,
        "content": "###  Set: Config\n**Load All Configuration**\n\n\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "2077eb11-904a-4abc-a2d2-2cf72b8e88a1",
      "name": "Sticky Note - Update Postman",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        432,
        -80
      ],
      "parameters": {
        "width": 304,
        "height": 348,
        "content": "### Update Postman Mock\n**Refresh Mock Server**\n\nCalls the Postman API to refresh the mock server using the configured `mockId`.\n\n\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "a9c82976-2b99-45c9-aefc-cd480c9fa5e9",
      "name": "Sticky Note - Old Spec",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        800,
        -64
      ],
      "parameters": {
        "width": 320,
        "height": 376,
        "content": "###  Download OLD OpenAPI Spec\n**Fetch Baseline Spec**\n\nDownloads `openapi_old.yaml` from the `develop` branch of the GitHub repo via raw URL.\n\nThis is the **previous version** of the API spec \u2014 used as the baseline for comparison by `openapi-diff`."
      },
      "typeVersion": 1
    },
    {
      "id": "3c7cde90-d2a5-408e-bd7a-02536d63a352",
      "name": "Sticky Note - New Spec",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1152,
        -32
      ],
      "parameters": {
        "width": 416,
        "height": 428,
        "content": "###  Download NEW OpenAPI Spec\n**Fetch Updated Spec**\n\nDownloads `openapi_new.yaml` from the `develop` branch of the GitHub repo via raw URL.\n\nThis is the **latest version** of the API spec \u2014 compared against the old spec to detect breaking changes.\n\nAfter this, GitHub Actions runs `openapi-diff` (Docker) and POSTs the result back."
      },
      "typeVersion": 1
    },
    {
      "id": "ed807808-15e5-4c7e-81eb-cf8aee375f46",
      "name": "Sticky Note - Diff Webhook",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -368,
        352
      ],
      "parameters": {
        "width": 368,
        "height": 294,
        "content": "### Webhook OpenAPI-diff\n**Receive Diff Result**\n\nA second webhook at `/webhook/api-diff-result` that receives the result POSTed back by GitHub Actions after running `openapi-diff` via Docker.\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "c55a54de-86f2-495a-aafb-a8669851e39c",
      "name": "Sticky Note - Check Empty",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        96,
        304
      ],
      "parameters": {
        "width": 352,
        "height": 322,
        "content": "### Check If Data Is Not Empty\n**Guard Clause**\n\nChecks whether the `diff` field in the incoming payload is non-empty.\n\n**True** \u2192 Proceed to parse diff and notify\n**False** \u2192 Route to Stop and Error\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "69e097c7-b5ff-4bdc-8f64-54f94764d3d1",
      "name": "Sticky Note - Stop Error",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -176,
        752
      ],
      "parameters": {
        "width": 528,
        "height": 248,
        "content": " Stop and Error\n**Halt \u2014 No Changes Detected**\n\nTerminates the execution with the message:\n> *\"No difference is available\"*\n\n\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "3dcd2e01-97ec-46ae-8f6b-7f1bd4890bde",
      "name": "Sticky Note - Get Fields",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        480,
        464
      ],
      "parameters": {
        "width": 320,
        "height": 454,
        "content": "### Get Fields\n**Parse & Clean Diff Data**\n\nExtracts and sanitizes fields from the webhook body:\n- `owner` \u2014 first part of `repo` string\n- `repo` \u2014 second part of `repo` string\n- `pr` \u2014 Pull Request ID\n- `diff` \u2014 removes `=====` and `-----` separator lines, strips extra blank lines and trims whitespace\n\nOutput is clean Markdown-ready text."
      },
      "typeVersion": 1
    },
    {
      "id": "2206905e-558a-47f8-9636-306250187dd7",
      "name": "Sticky Note - Comment PR",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        832,
        448
      ],
      "parameters": {
        "width": 336,
        "height": 486,
        "content": "### Comment to PR\n**Post Diff to GitHub PR**\n\nCalls the GitHub REST API to post the cleaned diff as a comment on the Pull Request.\n\nEndpoint:\n`POST /repos/{owner}/{repo}/issues/{pr}/comments`\n\nThe comment is formatted under the heading:\n`### OpenAPI Diff Result`\n\nUses `Header Auth PR` credential."
      },
      "typeVersion": 1
    },
    {
      "id": "ccd00b87-c68f-4e21-a945-191ca4245701",
      "name": "Sticky Note - Email",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1264,
        448
      ],
      "parameters": {
        "width": 352,
        "height": 454,
        "content": "###  Send Summary Email1\n**Notify Team via Gmail**\n\nSends a rich HTML email to the team with:\n- Repository name and owner\n- Pull Request number\n- Full diff output (in `<pre>` block)\n- Direct link to the PR on GitHub\n\n Update `sendTo` with your team's email address.\n Uses Gmail OAuth2 credential."
      },
      "typeVersion": 1
    },
    {
      "id": "e4fc1822-f992-428b-acf7-73167f479e70",
      "name": "Update Postman Mock",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        544,
        80
      ],
      "parameters": {
        "url": "=https://api.getpostman.com/mocks/{{ $json.postman.mockId }}",
        "options": {},
        "authentication": "headerAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "5a20f6f9-e559-47ed-80ff-b1167b51a255",
      "name": "Comment to PR",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        944,
        784
      ],
      "parameters": {
        "url": "=https://api.github.com/repos/{{ $json.owner }}/{{ $json.repo }}/issues/{{ $json.pr }}/comments",
        "options": {},
        "requestMethod": "POST",
        "authentication": "headerAuth",
        "jsonParameters": true,
        "bodyParametersJson": "={{ JSON.stringify({\n  body: \"### OpenAPI Diff Result\\n\\n\" + $json.diff\n}) }}",
        "headerParametersJson": "={\n \"Accept\": \"application/vnd.github+json\",\n \"Content-Type\": \"application/json\"\n}"
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "405025f2-718c-408e-b289-c12197b4adc9",
      "name": "Set: Config",
      "type": "n8n-nodes-base.set",
      "position": [
        208,
        16
      ],
      "parameters": {
        "values": {
          "string": [
            {
              "name": "postman.apiKey",
              "value": "PMAK-69ae6adcaa02a000013daf43-01b4c0b7b6809dac30e2f3434f68b839fb"
            },
            {
              "name": "postman.mockId",
              "value": "26cc4d4b-9b09-4bdf-86e2-2c4c411197fe"
            },
            {
              "name": "github.repo",
              "value": "https://github.com/learning-n8n/n8n-test.git"
            },
            {
              "name": "github.token",
              "value": "github_patYOUR_AIRTABLE_TOKEN_HERE"
            },
            {
              "name": "github.prId",
              "value": "1"
            }
          ]
        },
        "options": {}
      },
      "typeVersion": 1
    },
    {
      "id": "357d92eb-a68d-4f84-9a48-08a4cf33cbbf",
      "name": "GitHub Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -160,
        144
      ],
      "parameters": {
        "path": "api-update",
        "options": {},
        "httpMethod": "POST"
      },
      "typeVersion": 1
    },
    {
      "id": "30d765be-440b-4df3-bafc-d22c0990fae9",
      "name": "Download OLD OpenAPI Spec",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        912,
        144
      ],
      "parameters": {
        "url": "=https://raw.githubusercontent.com/learning-n8n/n8n-test/develop/openapi_specs/openapi_old.yaml",
        "options": {}
      },
      "typeVersion": 4.4
    },
    {
      "id": "ce0c80c5-15a6-4959-a6b4-4501159ab10d",
      "name": "Download NEW OpenAPI Spec",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1312,
        208
      ],
      "parameters": {
        "url": "=https://raw.githubusercontent.com/learning-n8n/n8n-test/develop/openapi_specs/openapi_new.yaml\n",
        "options": {}
      },
      "typeVersion": 4.4
    },
    {
      "id": "a3a8fddd-5fca-4204-9c5c-0b0657f72a47",
      "name": "Webhook OpenAPI-diff",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -240,
        496
      ],
      "parameters": {
        "path": "api-diff-result",
        "options": {},
        "httpMethod": "POST"
      },
      "typeVersion": 2.1
    },
    {
      "id": "784fd60f-69cf-463a-9c6e-1a3665d3b5ee",
      "name": "Stop and Error",
      "type": "n8n-nodes-base.stopAndError",
      "position": [
        32,
        864
      ],
      "parameters": {
        "errorMessage": "No difference is available"
      },
      "typeVersion": 1
    },
    {
      "id": "bfd686d0-7c19-424c-a708-000d59a15508",
      "name": "Get Fields",
      "type": "n8n-nodes-base.set",
      "position": [
        592,
        768
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "de0bfceb-e44e-418a-acd1-ef9fc11e0aba",
              "name": "=owner",
              "type": "string",
              "value": "={{ $json.body.repo.split(\"/\")[0] }}"
            },
            {
              "id": "b6f253c9-b75b-42ac-8b74-d2982f4eaef5",
              "name": "repo",
              "type": "string",
              "value": "={{ $json.body.repo.split(\"/\")[1] }}"
            },
            {
              "id": "c0a887ad-4ab9-48f4-b3f4-ab730f0ca903",
              "name": "pr",
              "type": "string",
              "value": "={{ $json.body.pr }}"
            },
            {
              "id": "a70fcc61-36e6-4b2c-88a8-c7775f546ea7",
              "name": "diff",
              "type": "string",
              "value": "={{\n$json.body.diff\n.replace(/^[=\\-]{5,}.*$/gm,'')   // remove ===== and ---- lines\n.replace(/\\n{2,}/g,'\\n')         // remove extra blank lines\n.trim()\n}}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "f61c3a4d-4303-4184-a1f0-f613289b317f",
      "name": "Check If data is not empty",
      "type": "n8n-nodes-base.if",
      "position": [
        224,
        480
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "f70e48ee-d113-408a-a8b8-221487403489",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              },
              "leftValue": "={{$json.body.diff}}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "ceebd209-58f9-478b-9d4d-5b21d01e14d1",
      "name": "Send Summary Email1",
      "type": "n8n-nodes-base.gmail",
      "position": [
        1376,
        736
      ],
      "parameters": {
        "sendTo": "user@example.com",
        "message": "=<h2>OpenAPI Diff Result</h2>\n\n<p><b>Repository:</b> {{ $('Get Fields').item.json.owner }}/{{ $('Get Fields').item.json.repo }}   </p>\n<p><b>Pull Request:</b> #{{ $('Get Fields').item.json.pr }} </p>\n\n<pre>{{ $('Get Fields').item.json.diff }}   </pre>\n\n<p>PR Link:</p>\n<a href=\"https://github.com/{{ $('Get Fields').item.json.owner }}/{{ $('Get Fields').item.json.repo }}/pull/{{ $('Get Fields').item.json.pr }}\">\nView Pull Request\n</a>",
        "options": {},
        "subject": "=OpenAPI Diff Result for PR #{{ $('Get Fields').item.json.pr }}"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "6d9a8172-b05a-456f-90e1-661f0e41ff24",
  "connections": {
    "Get Fields": {
      "main": [
        [
          {
            "node": "Comment to PR",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set: Config": {
      "main": [
        [
          {
            "node": "Update Postman Mock",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Comment to PR": {
      "main": [
        [
          {
            "node": "Send Summary Email1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GitHub Webhook": {
      "main": [
        [
          {
            "node": "Set: Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Postman Mock": {
      "main": [
        [
          {
            "node": "Download OLD OpenAPI Spec",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook OpenAPI-diff": {
      "main": [
        [
          {
            "node": "Check If data is not empty",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download OLD OpenAPI Spec": {
      "main": [
        [
          {
            "node": "Download NEW OpenAPI Spec",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check If data is not empty": {
      "main": [
        [
          {
            "node": "Get Fields",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Stop and Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}