{
  "id": "M2M3G1dv1HTy9jmP",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Git Tag \u2192 Release Notes \u2192 Jira \u2192 Slack (Dev + QA)",
  "tags": [],
  "nodes": [
    {
      "id": "3c74c452-1ad0-48a4-adfe-e95390d1468d",
      "name": "Git Tag Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -1728,
        400
      ],
      "parameters": {
        "path": "git-tag-webhook",
        "options": {},
        "responseMode": "responseNode"
      },
      "typeVersion": 1.1
    },
    {
      "id": "f3392442-28e4-42c9-a9bb-4c7ea7357712",
      "name": "Is Valid Semver?",
      "type": "n8n-nodes-base.if",
      "position": [
        -1488,
        400
      ],
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.isValid }}",
              "value2": true
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "4f6ddf56-e8c4-481f-a8ac-f6781e29728a",
      "name": "Fetch Commits from Branch",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "Fetches commit list between current tag and specified branch (default: main). Supports GitHub, GitLab (cloud & self-hosted) and Bitbucket. For GitLab self-hosted, set GITLAB_TOKEN environment variable with your GitLab API token. The PRIVATE-TOKEN header will be added automatically for GitLab servers.",
      "position": [
        -1152,
        112
      ],
      "parameters": {
        "url": "=Here_your_git_url",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept",
              "value": "="
            },
            {
              "name": "User-Agent"
            },
            {
              "name": "PRIVATE-TOKEN",
              "value": "="
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "de26cab7-ed47-41a2-8e8c-da0877e68060",
      "name": "Generate Release Notes",
      "type": "n8n-nodes-base.code",
      "position": [
        -928,
        112
      ],
      "parameters": {
        "jsCode": "// Generate release notes with commit data\nconst tagData = $input.item.json;\nconst tagName = tagData.tagName;\nconst version = tagData.version;\nconst major = parseInt(tagData.major) || 0;\nconst minor = parseInt(tagData.minor) || 0;\nconst patch = parseInt(tagData.patch) || 0;\nconst preRelease = tagData.preRelease;\nconst repository = tagData.repositoryFullName || tagData.repository;\nconst commitSha = tagData.commitSha;\nconst baseUrl = tagData.baseUrl;\nconst defaultBranch = tagData.defaultBranch || 'main';\n\n// Try to get commit data from previous node (HTTP Request)\nlet commits = [];\nlet commitCount = 0;\nlet commitData = null;\n\ntry {\n  // Check if we have commit data from HTTP Request node\n  const httpNode = $('Fetch Commits from Branch');\n  if (httpNode && httpNode.first()) {\n    commitData = httpNode.first().json;\n    \n    // Check for error responses\n    if (commitData.error || commitData.message) {\n      console.log('API returned error:', commitData.error || commitData.message);\n      // Continue without commit data\n    }\n    // GitHub format\n    else if (commitData.commits && Array.isArray(commitData.commits)) {\n      commits = commitData.commits.map(c => ({\n        sha: c.sha?.substring(0, 7) || '',\n        message: c.commit?.message || c.message || '',\n        author: c.commit?.author?.name || c.author?.login || c.author?.name || 'unknown',\n        date: c.commit?.author?.date || c.date || ''\n      }));\n      commitCount = commitData.commits.length;\n    }\n    // GitLab format (cloud & self-hosted)\n    else if (commitData.commits && Array.isArray(commitData.commits)) {\n      commits = commitData.commits.map(c => ({\n        sha: c.id?.substring(0, 7) || c.short_id?.substring(0, 7) || '',\n        message: c.message || c.title || '',\n        author: c.author_name || c.author?.name || c.committer_name || 'unknown',\n        date: c.created_at || c.committed_date || c.date || ''\n      }));\n      commitCount = commitData.commits.length;\n    }\n    // GitLab compare API returns commits array directly\n    else if (Array.isArray(commitData)) {\n      commits = commitData.map(c => ({\n        sha: c.id?.substring(0, 7) || c.short_id?.substring(0, 7) || '',\n        message: c.message || c.title || '',\n        author: c.author_name || c.author?.name || 'unknown',\n        date: c.created_at || c.committed_date || ''\n      }));\n      commitCount = commitData.length;\n    }\n  }\n} catch (e) {\n  // If HTTP request failed or not available, continue without commit data\n  console.log('No commit data available, continuing without it:', e.message);\n}\n\n// Determine release type\nlet releaseType = '';\nlet releaseEmoji = '';\nlet notes = '';\n\nif (preRelease) {\n  releaseType = `Pre-release (${preRelease})`;\n  releaseEmoji = '\ud83d\udea7';\n  notes = `\\n${releaseEmoji} **Pre-release Build**\\n\\nThis is a ${preRelease} release for testing purposes.\\n\\n`;\n} else if (major > 0 && minor === 0 && patch === 0) {\n  releaseType = 'Major Release';\n  releaseEmoji = '\ud83c\udf89';\n  notes = `\\n${releaseEmoji} **Major Release**\\n\\nThis release includes breaking changes and significant new features.\\n\\n`;\n} else if (patch === 0) {\n  releaseType = 'Minor Release';\n  releaseEmoji = '\u2728';\n  notes = `\\n${releaseEmoji} **Minor Release**\\n\\nThis release includes new features and improvements.\\n\\n`;\n} else {\n  releaseType = 'Patch Release';\n  releaseEmoji = '\ud83d\udc1b';\n  notes = `\\n${releaseEmoji} **Patch Release**\\n\\nThis release includes bug fixes and minor improvements.\\n\\n`;\n}\n\n// Add commit information if available\nif (commits.length > 0) {\n  notes += `**Changes in this release:**\\n\\n`;\n  notes += `Total commits: ${commitCount}\\n\\n`;\n  \n  // Add commit list (limit to 20 most recent)\n  const recentCommits = commits.slice(0, 20);\n  recentCommits.forEach(commit => {\n    const firstLine = commit.message.split('\\n')[0];\n    notes += `- ${commit.sha}: ${firstLine} (${commit.author})\\n`;\n  });\n  \n  if (commits.length > 20) {\n    notes += `\\n... and ${commits.length - 20} more commits.\\n`;\n  }\n  \n  notes += `\\n**View all changes:** ${baseUrl}/compare/${tagName}...${defaultBranch}\\n\\n`;\n} else {\n  notes += `**View changes:** ${baseUrl}/compare/${tagName}...${defaultBranch} or ${baseUrl}/-/tags/${tagName}\\n\\n`;\n}\n\nnotes += `**Release Information:**\\n`;\nnotes += `- Version: ${version}\\n`;\nnotes += `- Repository: ${repository}\\n`;\nnotes += `- Release Type: ${releaseType}\\n`;\nnotes += `- Tagged by: ${tagData.tagger}\\n`;\nif (commitSha) {\n  notes += `- Commit: ${commitSha.substring(0, 7)}\\n`;\n}\nnotes += `\\n**Action Items:**\\n`;\nnotes += `- [ ] Review release notes\\n`;\nnotes += `- [ ] Perform QA testing\\n`;\nnotes += `- [ ] Update deployment documentation\\n`;\nnotes += `- [ ] Notify stakeholders\\n`;\n\nreturn {\n  json: {\n    ...tagData,\n    releaseType: releaseType,\n    releaseEmoji: releaseEmoji,\n    releaseNotes: notes,\n    commitCount: commitCount,\n    commits: commits\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "85a948c3-97da-44fa-ac70-b9e696764ff0",
      "name": "Prepare Slack Payload",
      "type": "n8n-nodes-base.set",
      "position": [
        -480,
        112
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "tagInfo",
              "name": "tagInfo",
              "type": "object",
              "value": "={{ $('Parse & Validate Tag').first().json }}"
            },
            {
              "id": "jiraTicket",
              "name": "jiraTicket",
              "type": "object",
              "value": "={{ $json }}"
            },
            {
              "id": "releaseNotes",
              "name": "releaseNotes",
              "type": "object",
              "value": "={{ $('Generate Release Notes').first().json }}"
            }
          ]
        }
      },
      "typeVersion": 3.3
    },
    {
      "id": "ad5d1a4c-9623-49db-833f-7fe2583bad2b",
      "name": "Slack: Dev Channel",
      "type": "n8n-nodes-base.slack",
      "position": [
        -192,
        -48
      ],
      "parameters": {
        "text": "={{ $json.releaseNotes.releaseEmoji + ' *New Release Created!*\\n\\n' + '**Version:** ' + $json.tagInfo.tagName + '\\n' + '**Repository:** ' + $json.tagInfo.repositoryFullName + '\\n' + '**Tagged by:** ' + $json.tagInfo.tagger + '\\n' + '**Release Type:** ' + $json.releaseNotes.releaseType + '\\n\\n' + ($json.releaseNotes.releaseNotes ? $json.releaseNotes.releaseNotes.substring(0, 500) + '...' : '') + '\\n\\n**Jira Ticket:** ' + $json.jiraTicket.key + '\\n' + '\ud83d\udd17 ' + ($env.JIRA_URL || '') + '/browse/' + $json.jiraTicket.key }}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "C09EV0SGCE5",
          "cachedResultName": "team-n8n-workflow"
        },
        "otherOptions": {}
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "07116db3-dacd-41ab-b425-a78ddfffe1a5",
      "name": "Slack: QA Channel",
      "type": "n8n-nodes-base.slack",
      "position": [
        -176,
        240
      ],
      "parameters": {
        "text": "={{ '\ud83e\uddea *Release Ready for QA Testing*\\n\\n' + '**Version:** ' + $json.tagInfo.tagName + '\\n' + '**Repository:** ' + $json.tagInfo.repositoryFullName + '\\n' + '**Release Type:** ' + $json.releaseNotes.releaseType + '\\n\\n' + ($json.releaseNotes.commitCount > 0 ? '**Total Changes:** ' + $json.releaseNotes.commitCount + ' commits\\n' : '') + '\\n**Testing Checklist:**\\n' + '- [ ] Smoke testing\\n' + '- [ ] Regression testing\\n' + '- [ ] Performance testing\\n' + '- [ ] Security testing\\n\\n**Jira Ticket:** ' + $json.jiraTicket.key + '\\n' + '\ud83d\udd17 ' + ($env.JIRA_URL || '') + '/browse/' + $json.jiraTicket.key + '\\n\\n**View Release:** ' + $json.tagInfo.baseUrl + '/releases/tag/' + $json.tagInfo.tagName }}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "C09EV0SGCE5",
          "cachedResultName": "team-n8n-workflow"
        },
        "otherOptions": {}
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "cdb03d73-f6f5-4e07-8ba1-b9e58aa3eef5",
      "name": "Slack: Invalid Format Warning",
      "type": "n8n-nodes-base.slack",
      "position": [
        -1136,
        576
      ],
      "parameters": {
        "text": "={{ '\u26a0\ufe0f *Warning: Invalid Version Format*\\n\\n' + 'The tag **' + $json.tagName + '** does not match the expected semantic versioning format (e.g., v1.2.3 or 1.2.3).\\n\\n' + '**Repository:** ' + $json.repositoryFullName + '\\n' + '**Tagged by:** ' + $json.tagger + '\\n' + '**Tag:** ' + $json.tagName + '\\n\\n' + '**Expected Format:**\\n' + '- Semantic versioning: `v1.2.3` or `1.2.3`\\n' + '- Pre-release: `v1.2.3-alpha1` or `v1.2.3-beta1`\\n\\n' + 'Please ensure tags follow semantic versioning format to trigger automated release workflows.' }}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "C09EV0SGCE5",
          "cachedResultName": "team-n8n-workflow"
        },
        "otherOptions": {}
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "b958653e-cf56-40d7-9f4c-bd875cc88972",
      "name": "Webhook Response",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        96,
        96
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={{ { success: true, message: 'Release workflow completed successfully', jiraKey: $json.jiraTicket?.key || '', tag: $json.tagInfo?.tagName || '', releaseType: $json.releaseNotes?.releaseType || '' } }}"
      },
      "typeVersion": 1
    },
    {
      "id": "667f43d9-8a5a-4803-837a-cd169a6b203a",
      "name": "Webhook Error Response",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        -896,
        576
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={{ { success: false, message: 'Invalid tag format', tag: $json.tagName || 'unknown', error: 'Tag does not match semantic versioning format' } }}"
      },
      "typeVersion": 1
    },
    {
      "id": "0eda8408-b5dc-4e30-8686-b9861293fcac",
      "name": "Create an issue",
      "type": "n8n-nodes-base.jira",
      "position": [
        -704,
        208
      ],
      "parameters": {
        "project": {
          "__rl": true,
          "mode": "list",
          "value": "10000",
          "cachedResultName": "n8n sample project"
        },
        "summary": "Here_your_prefered _summry_msg_for_task",
        "issueType": {
          "__rl": true,
          "mode": "list",
          "value": "10003",
          "cachedResultName": "Task"
        },
        "additionalFields": {}
      },
      "credentials": {
        "jiraSoftwareCloudApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "dd7726f7-27d8-4bba-b890-5091bc8b133e",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1792,
        304
      ],
      "parameters": {
        "color": 7,
        "width": 224,
        "height": 240,
        "content": "Receives GitLab tag push event and starts the workflow automatically."
      },
      "typeVersion": 1
    },
    {
      "id": "408f5d66-a8e2-4192-94ed-eee589f0bfff",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1552,
        304
      ],
      "parameters": {
        "color": 7,
        "width": 208,
        "height": 240,
        "content": "Checks whether the tag follows version format like v1.0.0."
      },
      "typeVersion": 1
    },
    {
      "id": "916c1fdf-0caf-48f1-8fcb-69d0dc23ce86",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1264,
        416
      ],
      "parameters": {
        "color": 7,
        "width": 592,
        "height": 352,
        "content": "**Slack: Invalid Format Warning:** Sends alert if tag version format is incorrect.\n\n**Webhook Error Response:** Returns error response when validation fails."
      },
      "typeVersion": 1
    },
    {
      "id": "72e00f83-9aac-4c8e-b416-d703f8700360",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1264,
        -16
      ],
      "parameters": {
        "color": 7,
        "width": 944,
        "height": 384,
        "content": "## Release Processing Steps\n\nThis section fetches commit changes from GitLab and creates automatic release notes for the new version. It also creates a Jira task for QA tracking and prepares the final Slack notification message for Dev and QA teams."
      },
      "typeVersion": 1
    },
    {
      "id": "6843c629-fb5b-4055-8130-07f6b101786d",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -272,
        -144
      ],
      "parameters": {
        "color": 7,
        "width": 256,
        "height": 256,
        "content": "**Slack: Dev Channel:** Sends release notification to the developer Slack channel."
      },
      "typeVersion": 1
    },
    {
      "id": "15810b49-e0a7-4890-83e0-be4c0ddae937",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -272,
        144
      ],
      "parameters": {
        "color": 7,
        "width": 256,
        "height": 256,
        "content": "**Slack: QA Channel:** Sends testing notification to the QA Slack channel."
      },
      "typeVersion": 1
    },
    {
      "id": "f315ea9e-e3c9-4272-8d9e-e73b98793973",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        32,
        16
      ],
      "parameters": {
        "color": 7,
        "width": 272,
        "height": 256,
        "content": "Returns success response after workflow completes."
      },
      "typeVersion": 1
    },
    {
      "id": "efdf5478-62f9-4009-8484-69ddb5e3d9c7",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2352,
        -272
      ],
      "parameters": {
        "width": 368,
        "height": 576,
        "content": "## How it Works\n\nThis workflow starts automatically when a new GitLab tag is created, such as v1.0.0. First, it checks whether the tag follows the correct version format. If the format is valid, it fetches recent commit changes from GitLab and uses them to generate release notes. After that, it creates a Jira task so the QA team can track and test the release. Finally, it sends notifications to separate Slack channels for the Development team and QA team with release details, Jira ticket information and testing updates. If the tag format is invalid, the workflow stops and sends a warning message to Slack.\n\n\n## Setup steps\n**1.** Connect your GitLab account and add the webhook URL in your GitLab repository webhook settings.\n**2.** Connect your Jira Cloud account and select the project where release tasks should be created.\n**3.** Connect your Slack account and choose separate Dev and QA channels.\n**4.** Add your GitLab API token in the HTTP Request node to fetch commits.\n**5.** Test by creating a new tag like v1.0.0 in GitLab.\n**6.** Activate the workflow after successful testing."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "f510a14b-88d0-4044-88a8-1c6bc2f34627",
  "connections": {
    "Create an issue": {
      "main": [
        [
          {
            "node": "Prepare Slack Payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Git Tag Webhook": {
      "main": [
        [
          {
            "node": "Is Valid Semver?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Valid Semver?": {
      "main": [
        [
          {
            "node": "Fetch Commits from Branch",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Slack: Invalid Format Warning",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack: QA Channel": {
      "main": [
        [
          {
            "node": "Webhook Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack: Dev Channel": {
      "main": [
        [
          {
            "node": "Webhook Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Slack Payload": {
      "main": [
        [
          {
            "node": "Slack: Dev Channel",
            "type": "main",
            "index": 0
          },
          {
            "node": "Slack: QA Channel",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Release Notes": {
      "main": [
        [
          {
            "node": "Create an issue",
            "type": "main",
            "index": 0
          },
          {
            "node": "Prepare Slack Payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Commits from Branch": {
      "main": [
        [
          {
            "node": "Generate Release Notes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack: Invalid Format Warning": {
      "main": [
        [
          {
            "node": "Webhook Error Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}