Jira Webhook配置与使用
什么是Jira Webhook(网络钩子)创建Webhook使用Webhook通过Webhook创建时设置的事件来触发通过工作流来触发Webhook 总结一下
什么是Jira Webhook(网络钩子)
Webhook是一种回调接口,一般用在由事件驱动的系统中。那么Jira内置的功能是支持自定义Webhook的,然后在特定的事件或者工作流执行中调用自己定义的Webhook,来实现与第三方系统的通信,并且Webhook中提供了几乎与Issue相关的所有字段信息。
创建Webhook
使用系统管员登录,进入系统设置页面,点击左侧“网络钩子菜单”,进入网络钩子管理页面,点击右上角“创建网络钩子”按钮,添加一个新的Webhook。
在这里,我们使用一个在线工具来测试打印出webhook推送给第三方系统的数据,UU在线工具https://uutool.cn/mock/。
名称:自己取个名字就好了
状态:可以启用或禁用掉Webhook,因为在实际工作中Webhook可以在很多流程里面去触发,所以这里可以禁用掉就不用去修改每个流程了
URL:这里填写的就是第三方系统的接口地址,我这里填写的是UU在线工具的临时接口地址,这个接口是一个挡板能把所有的请求信息打印出来。
事件:这里我们定义在创建和修改Issue时去触发这个Webhook的执行,注意这里也可以不选任何的事件,而是由工作流中我们去触发这个Webhook,事件的主体有Issue、项目、用户、备注还有敏捷相关的看板等等。注意这里选的事件类型与数据主体信息是相对应的,比如选Issue的事件,Raw里面的json数据就是issue相关的;如果选择的是用户事件,那么Raw里面在json主体就是用户信息
排除主体:有些事件发生时我们只需要少量的字段我们可以通过在URL的变量中携带而不必传输整个Raw数据,这时我们勾选排除主体就可以了。
使用Webhook
通过Webhook创建时设置的事件来触发
刚才我们定义的Webhook是在Issue创建和修改的时候触发的,那么我们现在创建一个Issue观察一下Webhook推送的数据。
创建Issue:
查看推送数据:
请求头
请求头,可以看到是通用HttpClient的库发送的请求{ "Content-Type": "application/json; charset=UTF-8", "User-Agent": "Atlassian HttpClient 3.0.4 / JIRA-9.13.1 (9130002) / Default", "Connection": "Keep-Alive", "Host": "mock.uutool.cn", "Content-Length": "7137", "Via": "1.1 localhost (Apache-HttpClient/4.5.14 (cache))", "Accept": "*/*"}
数据结构
Issue的Webhook数据结构中包括了用户、项目、报告人、Issue字段等的信息,如果在第三方系统需要其它信息都可以通过自定义字段去放置。
数据位置和结构,可以看到Webhook在GET参数中带了用户名,具体信息在请求体中通过JSON结构传输的。GET参数:{ "user_id": "admin", "user_key": "JIRAUSER10000"} POST参数:[] Raw:{ "timestamp": 1725868650247, "webhookEvent": "jira:issue_created", "issue_event_type_name": "issue_created", "user": { "self": "http://192.168.1.100:8080/rest/api/2/user?username=admin", "name": "admin", "key": "JIRAUSER10000", "emailAddress": "admin@abdxww.co", "avatarUrls": { "48x48": "http://192.168.1.100:8080/secure/useravatar?avatarId=10351", "24x24": "http://192.168.1.100:8080/secure/useravatar?size=small&avatarId=10351", "16x16": "http://192.168.1.100:8080/secure/useravatar?size=xsmall&avatarId=10351", "32x32": "http://192.168.1.100:8080/secure/useravatar?size=medium&avatarId=10351" }, "displayName": "admin", "active": true, "timeZone": "Asia/Shanghai" }, "issue": { "id": "11104", "self": "http://192.168.1.100:8080/rest/api/2/issue/11104", "key": "TEST-17", "fields": { "issuetype": { "self": "http://192.168.1.100:8080/rest/api/2/issuetype/10006", "id": "10006", "description": "", "iconUrl": "http://192.168.1.100:8080/secure/viewavatar?size=xsmall&avatarId=10303&avatarType=issuetype", "name": "故障", "subtask": false, "avatarId": 10303 }, "timespent": null, "project": { "self": "http://192.168.1.100:8080/rest/api/2/project/10200", "id": "10200", "key": "TEST", "name": "TEST1", "projectTypeKey": "software", "avatarUrls": { "48x48": "http://192.168.1.100:8080/secure/projectavatar?avatarId=10324", "24x24": "http://192.168.1.100:8080/secure/projectavatar?size=small&avatarId=10324", "16x16": "http://192.168.1.100:8080/secure/projectavatar?size=xsmall&avatarId=10324", "32x32": "http://192.168.1.100:8080/secure/projectavatar?size=medium&avatarId=10324" } }, "fixVersions": [], "customfield_10110": null, "aggregatetimespent": null, "resolution": null, "customfield_10104": null, "customfield_10108": "0|i005h3:", "customfield_10109": null, "resolutiondate": null, "workratio": -1, "lastViewed": null, "watches": { "self": "http://192.168.1.100:8080/rest/api/2/issue/TEST-17/watchers", "watchCount": 0, "isWatching": false }, "created": "2024-09-09T15:57:30.142+0800", "priority": { "self": "http://192.168.1.100:8080/rest/api/2/priority/3", "iconUrl": "http://192.168.1.100:8080/images/icons/priorities/medium.svg", "name": "Medium", "id": "3" }, "customfield_10100": null, "customfield_10101": null, "customfield_10102": null, "customfield_10300": null, "labels": [], "customfield_10103": null, "timeestimate": null, "aggregatetimeoriginalestimate": null, "versions": [], "issuelinks": [], "assignee": null, "updated": "2024-09-09T15:57:30.142+0800", "status": { "self": "http://192.168.1.100:8080/rest/api/2/status/10000", "description": "", "iconUrl": "http://192.168.1.100:8080/", "name": "待办", "id": "10000", "statusCategory": { "self": "http://192.168.1.100:8080/rest/api/2/statuscategory/2", "id": 2, "key": "new", "colorName": "default", "name": "待办" } }, "components": [], "timeoriginalestimate": null, "description": null, "timetracking": {}, "archiveddate": null, "customfield_10401": null, "customfield_10203": null, "attachment": [], "aggregatetimeestimate": null, "summary": "test webhook1", "creator": { "self": "http://192.168.1.100:8080/rest/api/2/user?username=admin", "name": "admin", "key": "JIRAUSER10000", "emailAddress": "admin@abdxww.co", "avatarUrls": { "48x48": "http://192.168.1.100:8080/secure/useravatar?avatarId=10351", "24x24": "http://192.168.1.100:8080/secure/useravatar?size=small&avatarId=10351", "16x16": "http://192.168.1.100:8080/secure/useravatar?size=xsmall&avatarId=10351", "32x32": "http://192.168.1.100:8080/secure/useravatar?size=medium&avatarId=10351" }, "displayName": "admin", "active": true, "timeZone": "Asia/Shanghai" }, "subtasks": [], "reporter": { "self": "http://192.168.1.100:8080/rest/api/2/user?username=admin", "name": "admin", "key": "JIRAUSER10000", "emailAddress": "admin@abdxww.co", "avatarUrls": { "48x48": "http://192.168.1.100:8080/secure/useravatar?avatarId=10351", "24x24": "http://192.168.1.100:8080/secure/useravatar?size=small&avatarId=10351", "16x16": "http://192.168.1.100:8080/secure/useravatar?size=xsmall&avatarId=10351", "32x32": "http://192.168.1.100:8080/secure/useravatar?size=medium&avatarId=10351" }, "displayName": "admin", "active": true, "timeZone": "Asia/Shanghai" }, "customfield_10000": "{summaryBean=com.atlassian.jira.plugin.devstatus.rest.SummaryBean@7ae0bb11[summary={pullrequest=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@f3a1dea[byInstanceType={},overall=PullRequestOverallBean{stateCount=0, state='OPEN', details=PullRequestOverallDetails{openCount=0, mergedCount=0, declinedCount=0}}], build=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@4a6368d[byInstanceType={},overall=com.atlassian.jira.plugin.devstatus.summary.beans.BuildOverallBean@5b2caebe[failedBuildCount=0,successfulBuildCount=0,unknownBuildCount=0,count=0,lastUpdated=<null>,lastUpdatedTimestamp=<null>]], review=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@4a735e4b[byInstanceType={},overall=com.atlassian.jira.plugin.devstatus.summary.beans.ReviewsOverallBean@38f61afc[dueDate=<null>,overDue=false,state=<null>,stateCount=0,count=0,lastUpdated=<null>,lastUpdatedTimestamp=<null>]], deployment-environment=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@dd14131[byInstanceType={},overall=com.atlassian.jira.plugin.devstatus.summary.beans.DeploymentOverallBean@33b6576b[showProjects=false,successfulCount=0,topEnvironments=[],count=0,lastUpdated=<null>,lastUpdatedTimestamp=<null>]], repository=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@63e7c650[byInstanceType={},overall=com.atlassian.jira.plugin.devstatus.summary.beans.CommitOverallBean@6bce112a[count=0,lastUpdated=<null>,lastUpdatedTimestamp=<null>]], branch=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@6c110dab[byInstanceType={},overall=com.atlassian.jira.plugin.devstatus.summary.beans.BranchOverallBean@6d1d024d[count=0,lastUpdated=<null>,lastUpdatedTimestamp=<null>]]},configErrors=[],errors=[]], devSummaryJson={\"cachedValue\":{\"errors\":[],\"configErrors\":[],\"summary\":{\"pullrequest\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"stateCount\":0,\"state\":\"OPEN\",\"details\":{\"openCount\":0,\"mergedCount\":0,\"declinedCount\":0,\"total\":0},\"open\":true},\"byInstanceType\":{}},\"build\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"failedBuildCount\":0,\"successfulBuildCount\":0,\"unknownBuildCount\":0},\"byInstanceType\":{}},\"review\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"stateCount\":0,\"state\":null,\"dueDate\":null,\"overDue\":false,\"completed\":false},\"byInstanceType\":{}},\"deployment-environment\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"topEnvironments\":[],\"showProjects\":false,\"successfulCount\":0},\"byInstanceType\":{}},\"repository\":{\"overall\":{\"count\":0,\"lastUpdated\":null},\"byInstanceType\":{}},\"branch\":{\"overall\":{\"count\":0,\"lastUpdated\":null},\"byInstanceType\":{}}}},\"isStale\":false}}", "aggregateprogress": { "progress": 0, "total": 0 }, "customfield_10200": null, "customfield_10201": null, "customfield_10400": null, "customfield_10202": null, "environment": null, "duedate": null, "progress": { "progress": 0, "total": 0 }, "comment": { "comments": [], "maxResults": 0, "total": 0, "startAt": 0 }, "votes": { "self": "http://192.168.1.100:8080/rest/api/2/issue/TEST-17/votes", "votes": 0, "hasVoted": false }, "worklog": { "startAt": 0, "maxResults": 20, "total": 0, "worklogs": [] }, "archivedby": null } }}
通过工作流来触发Webhook
我们创建的Webhook,不勾选任何事件,然后在流程中去触发。
修改项目D35问题类型是故障的工作流,在完成的转换中添加后处理功能,去触发刚才定义的Webhook。
点击后处理功能:
点击添加后处理功能:
点击引发一个webhook,选择刚才定义的webhook。
在D35-1这个Issue中点击完成。
我们可以看到通过流程触发的webhook的数据主体还是Issue,多了流程状态的数据。
GET和POST外的其他方法见Raw数据:GET参数:{ "user_id": "admin", "user_key": "JIRAUSER10000"} POST参数:[] Raw:{ "transition": { "workflowId": 11105, "workflowName": "Software Simplified Workflow for Project D35", "transitionId": 31, "transitionName": "Done", "from_status": "To Do", "to_status": "Done" }, "comment": "", "user": { "self": "http://192.168.1.100:8080/rest/api/2/user?username=admin", "name": "admin", "key": "JIRAUSER10000", "emailAddress": "admin@abdxww.co" "avatarUrls": { "48x48": "http://192.168.1.100:8080/secure/useravatar?avatarId=10351", "24x24": "http://192.168.1.100:8080/secure/useravatar?size=small&avatarId=10351", "16x16": "http://192.168.1.100:8080/secure/useravatar?size=xsmall&avatarId=10351", "32x32": "http://192.168.1.100:8080/secure/useravatar?size=medium&avatarId=10351" }, "displayName": "admin", "active": true, "timeZone": "Asia/Shanghai" }, "issue": { "id": "11105", "self": "http://192.168.1.100:8080/rest/api/2/issue/11105", "key": "D35-1", "fields": { "issuetype": { "self": "http://192.168.1.100:8080/rest/api/2/issuetype/10006", "id": "10006", "description": "", "iconUrl": "http://192.168.1.100:8080/secure/viewavatar?size=xsmall&avatarId=10303&avatarType=issuetype", "name": "故障", "subtask": false, "avatarId": 10303 }, "timespent": null, "project": { "self": "http://192.168.1.100:8080/rest/api/2/project/10302", "id": "10302", "key": "D35", "name": "D35", "projectTypeKey": "software", "avatarUrls": { "48x48": "http://192.168.1.100:8080/secure/projectavatar?avatarId=10324", "24x24": "http://192.168.1.100:8080/secure/projectavatar?size=small&avatarId=10324", "16x16": "http://192.168.1.100:8080/secure/projectavatar?size=xsmall&avatarId=10324", "32x32": "http://192.168.1.100:8080/secure/projectavatar?size=medium&avatarId=10324" } }, "fixVersions": [], "customfield_10110": null, "aggregatetimespent": null, "resolution": null, "customfield_10104": null, "customfield_10108": "0|i005hb:", "customfield_10109": null, "resolutiondate": null, "workratio": -1, "lastViewed": "2024-09-09T16:45:38.601+0800", "watches": { "self": "http://192.168.1.100:8080/rest/api/2/issue/D35-1/watchers", "watchCount": 1, "isWatching": true }, "created": "2024-09-09T16:35:50.000+0800", "priority": { "self": "http://192.168.1.100:8080/rest/api/2/priority/3", "iconUrl": "http://192.168.1.100:8080/images/icons/priorities/medium.svg", "name": "Medium", "id": "3" }, "customfield_10100": null, "customfield_10101": null, "customfield_10102": null, "customfield_10300": null, "labels": [], "customfield_10103": null, "timeestimate": null, "aggregatetimeoriginalestimate": null, "versions": [], "issuelinks": [], "assignee": null, "updated": "2024-09-09T16:44:11.000+0800", "status": { "self": "http://192.168.1.100:8080/rest/api/2/status/10000", "description": "", "iconUrl": "http://192.168.1.100:8080/", "name": "待办", "id": "10000", "statusCategory": { "self": "http://192.168.1.100:8080/rest/api/2/statuscategory/2", "id": 2, "key": "new", "colorName": "default", "name": "待办" } }, "components": [], "timeoriginalestimate": null, "description": null, "timetracking": {}, "archiveddate": null, "customfield_10401": null, "customfield_10203": null, "attachment": [], "aggregatetimeestimate": null, "summary": "bug1", "creator": { "self": "http://192.168.1.100:8080/rest/api/2/user?username=admin", "name": "admin", "key": "JIRAUSER10000", "emailAddress": "admin@abdxww.co", "avatarUrls": { "48x48": "http://192.168.1.100:8080/secure/useravatar?avatarId=10351", "24x24": "http://192.168.1.100:8080/secure/useravatar?size=small&avatarId=10351", "16x16": "http://192.168.1.100:8080/secure/useravatar?size=xsmall&avatarId=10351", "32x32": "http://192.168.1.100:8080/secure/useravatar?size=medium&avatarId=10351" }, "displayName": "admin", "active": true, "timeZone": "Asia/Shanghai" }, "subtasks": [], "reporter": { "self": "http://192.168.1.100:8080/rest/api/2/user?username=admin", "name": "admin", "key": "JIRAUSER10000", "emailAddress": "admin@abdxww.co", "avatarUrls": { "48x48": "http://192.168.1.100:8080/secure/useravatar?avatarId=10351", "24x24": "http://192.168.1.100:8080/secure/useravatar?size=small&avatarId=10351", "16x16": "http://192.168.1.100:8080/secure/useravatar?size=xsmall&avatarId=10351", "32x32": "http://192.168.1.100:8080/secure/useravatar?size=medium&avatarId=10351" }, "displayName": "admin", "active": true, "timeZone": "Asia/Shanghai" }, "customfield_10000": "{summaryBean=com.atlassian.jira.plugin.devstatus.rest.SummaryBean@46fa0a08[summary={pullrequest=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@4c98c539[byInstanceType={},overall=PullRequestOverallBean{stateCount=0, state='OPEN', details=PullRequestOverallDetails{openCount=0, mergedCount=0, declinedCount=0}}], build=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@734de769[byInstanceType={},overall=com.atlassian.jira.plugin.devstatus.summary.beans.BuildOverallBean@79f64534[failedBuildCount=0,successfulBuildCount=0,unknownBuildCount=0,count=0,lastUpdated=<null>,lastUpdatedTimestamp=<null>]], review=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@780f02[byInstanceType={},overall=com.atlassian.jira.plugin.devstatus.summary.beans.ReviewsOverallBean@6d38812b[dueDate=<null>,overDue=false,state=<null>,stateCount=0,count=0,lastUpdated=<null>,lastUpdatedTimestamp=<null>]], deployment-environment=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@71d2454a[byInstanceType={},overall=com.atlassian.jira.plugin.devstatus.summary.beans.DeploymentOverallBean@3a4671a1[showProjects=false,successfulCount=0,topEnvironments=[],count=0,lastUpdated=<null>,lastUpdatedTimestamp=<null>]], repository=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@7a0669f4[byInstanceType={},overall=com.atlassian.jira.plugin.devstatus.summary.beans.CommitOverallBean@531e8e46[count=0,lastUpdated=<null>,lastUpdatedTimestamp=<null>]], branch=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@3005697a[byInstanceType={},overall=com.atlassian.jira.plugin.devstatus.summary.beans.BranchOverallBean@39c7cda2[count=0,lastUpdated=<null>,lastUpdatedTimestamp=<null>]]},configErrors=[],errors=[]], devSummaryJson={\"cachedValue\":{\"errors\":[],\"configErrors\":[],\"summary\":{\"pullrequest\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"stateCount\":0,\"state\":\"OPEN\",\"details\":{\"openCount\":0,\"mergedCount\":0,\"declinedCount\":0,\"total\":0},\"open\":true},\"byInstanceType\":{}},\"build\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"failedBuildCount\":0,\"successfulBuildCount\":0,\"unknownBuildCount\":0},\"byInstanceType\":{}},\"review\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"stateCount\":0,\"state\":null,\"dueDate\":null,\"overDue\":false,\"completed\":false},\"byInstanceType\":{}},\"deployment-environment\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"topEnvironments\":[],\"showProjects\":false,\"successfulCount\":0},\"byInstanceType\":{}},\"repository\":{\"overall\":{\"count\":0,\"lastUpdated\":null},\"byInstanceType\":{}},\"branch\":{\"overall\":{\"count\":0,\"lastUpdated\":null},\"byInstanceType\":{}}}},\"isStale\":false}}", "aggregateprogress": { "progress": 0, "total": 0 }, "customfield_10200": null, "customfield_10201": null, "customfield_10400": null, "customfield_10202": null, "environment": null, "duedate": null, "progress": { "progress": 0, "total": 0 }, "comment": { "comments": [], "maxResults": 0, "total": 0, "startAt": 0 }, "votes": { "self": "http://192.168.1.100:8080/rest/api/2/issue/D35-1/votes", "votes": 0, "hasVoted": false }, "worklog": { "startAt": 0, "maxResults": 20, "total": 0, "worklogs": [] }, "archivedby": null } }, "timestamp": 1725871538608}
总结一下
Jira的Webhook定义及使用都非常灵活,几乎可以Jira侧不进行二次开发来实现与第三方系统对接的所有功能。如果再结合Scriptrunner可以实现与第三方系统之间双向通信,来实现复杂的业务逻辑,这些都是不需要通Jira的二次开发来实现的功能。