AutomationFlowsEmail & Gmail › Automate Gitlab Tag Releases to Jira and Slack for Dev and Qa

Automate Gitlab Tag Releases to Jira and Slack for Dev and Qa

ByWeblineIndia @weblineindia on n8n.io

This workflow automatically detects a new GitLab tag, validates the version, fetches commit changes, generates release notes, creates a Jira task and sends notifications to separate Slack channels for Development and QA teams.

Webhook trigger★★★★☆ complexity19 nodesHTTP RequestSlackJira
Email & Gmail Trigger: Webhook Nodes: 19 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #15354 — we link there as the canonical source.

This workflow follows the HTTP Request → Slack recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "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
          }
        ]
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow automatically detects a new GitLab tag, validates the version, fetches commit changes, generates release notes, creates a Jira task and sends notifications to separate Slack channels for Development and QA teams.

Source: https://n8n.io/workflows/15354/ — original creator credit. Request a take-down →

More Email & Gmail workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Email & Gmail

This workflow is a fully automated AI matte painting generation system for VFX pipelines, designed to convert a single environment prompt into multiple cinematic background variations. It handles gene

HTTP Request, Jira, Slack +3
Email & Gmail

Suspicious_login_detection. Uses postgres, httpRequest, noOp, html. Webhook trigger; 43 nodes.

Postgres, HTTP Request, Gmail +1
Email & Gmail

This n8n workflow is designed for security monitoring and incident response when suspicious login events are detected. It can be initiated either manually from within the n8n UI for testing or automat

Postgres, HTTP Request, Gmail +1
Email & Gmail

Receive inventory movements via webhook, validate data, update stock levels, and trigger automatic alerts when products need reordering.

HTTP Request, Slack, Gmail +1
Email & Gmail

Wait. Uses httpRequest, itemLists, slack, gmail. Webhook trigger; 29 nodes.

HTTP Request, Item Lists, Slack +2