业务的简称为demo,微服务架构。N多个微服务。服务命名:业务简称-应用名称-类型(demo-hello-service)。特性分支开发,版本分支发布。每个需求(任务/故事)对应一个特性分支。每个发布(release)对应一个版本分支。
1.需求与代码管理
Jira作为需求和缺陷管理,采用Scrum开发方法,jira中的项目名称与业务简称一致(demo)。Gitlab作为版本控制系统,每个Group对应一个业务,每个微服务对应一个代码库。
需求与代码关联:在jira中创建一个任务/故事,关联模块后自动在该模块创建一个以ISSUE(任务/故事)ID的特性分支。此时的模块等同于每个微服务的项目(代码库)名称。以下面图中为例:我们在demo项目中创建了一个模块demo-hello-service,其实对应的就是Gitlab代码库中demo组的demo-hello-service服务。
特性分支:创建好每个模块后,就可以实现需求与代码关联。例如:我们在Jira项目demo中创建一个问题,类型为故事(不受限制可为其他),重点是需要将改故事关联到模块(只有关联到模块,我们才能通过接口得知哪个问题关联的哪个代码库)。
版本分支:当特性分支开发完成以及测试验证完成后,基于主干分支创建一个版本分支,然后将所有的特性分支合并到版本分支。此时可以通过Jira中创建一个发布版本,然后问题关联发布版本(此动作表示该特性分支已经通过验证,可以合并)。自动完成版本分支的创建和特性分支到版本分支的合并请求。
2. 配置过程
需求与代码库关联,主要用到的工具链为: Jira + GitLab + Jenkins。Jira负责创建需求,配置webhook。Jenkins负责接收Jira webhook请求,然后通过接口实现GitLab项目分支创建。
特性分支自动化:当我们在jira上面创建了问题,此时会通过Jira的webhook触发对应的Jenkins作业,该Jenkins作业通过解析Jira webhook传递的数据,找到问题名称和模块名称。调用GitlabAPI 项目查询接口,根据模块名称找到代码库。调用GitLabAPI 分支创建接口,根据问题名称基于主干分支创建一个特性分支。任务结束。
版本分支自动化:Jira创建发布版本,Issue关联版本。自动在gitlab代码库基于master创建版本分支,并开启特性分支到版本分支的合并请求。
2.1 准备工作
在Jenkins, 创建一个Pipeline 作业并配置GenericTrigger 触发器,接收JiraWebhook数据。projectKey 参数表示Jira项目名称,webHookData 参数为Jira webhook的所有数据。token 是触发器的触发token,这里默认采用的作业名称(作业名称要唯一)。
triggers {
GenericTrigger( causeString: 'Trigger By Jira Server -->>>>> Generic Cause',
genericRequestVariables: [[key: 'projectKey', regexpFilter: '']],
genericVariables: [[defaultValue: '', key: 'webHookData', regexpFilter: '', value: '$']],
printContributedVariables: true,
printPostContent: true,
regexpFilterExpression: '',
regexpFilterText: '',
silentResponse: true,
token: "${JOB_NAME}"
)
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
在Jira项目中配置Webhook,勾选触发事件填写触发URL。http://jenkins.idevops.site/generic-webhook-trigger/invoke?token=demo-jira-service&projectKey=${project.key} (这个地址是jenkins Generictrigger生成的,这里不做过多的介绍)
Jira webhook数据参考, 这些参数可以在Jenkinsfile中通过readJSON格式化,然后获取值。
response = readJSON text: """${webHookData}"""
println(response)
//获取webhook的事件类型
env.eventType = response["webhookEvent"]
- 1.
- 2.
- 3.
- 4.
- 5.
{
"timestamp":1603087582648,
"webhookEvent":"jira:issue_created",
"issue_event_type_name":"issue_created",
"user":Object{...},
"issue":{
"id":"10500",
"self":"http://192.168.1.200:8050/rest/api/2/issue/10500",
"key":"DEMO-2",
"fields":{
"issuetype":{
"self":"http://192.168.1.200:8050/rest/api/2/issuetype/10001",
"id":"10001",
"description":"",
"iconUrl":"http://192.168.1.200:8050/images/icons/issuetypes/story.svg",
"name":"故事",
"subtask":false
},
"components":[
{
"self":"http://192.168.1.200:8050/rest/api/2/component/10200",
"id":"10200",
"name":"demo-hello-service",
"description":"demo-hello-service应用"
}
],
"timespent":null,
"timeoriginalestimate":null,
"description":null,
...
...
...
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
2.2 封装GitLab接口
Gitlab接口文档:https://docs.gitlab.com/ce/api/README.html
共享库:src/org/devops/gitlab.groovy
package org.devops
//封装HTTP请求
def HttpReq(reqType,reqUrl,reqBody){
def gitServer = "http://gitlab.idevops.site/api/v4"
withCredentials([string(credentialsId: 'gitlab-token', variable: 'gitlabToken')]) {
result = httpRequest customHeaders: [[maskValue: true, name: 'PRIVATE-TOKEN', value: "${gitlabToken}"]],
httpMode: reqType,
contentType: "APPLICATION_JSON",
consoleLogResponseBody: true,
ignoreSslErrors: true,
requestBody: reqBody,
url: "${gitServer}/${reqUrl}"
//quiet: true
}
return result
}
//更新文件内容
def UpdateRepoFile(projectId,filePath,fileContent){
apiUrl = "projects/${projectId}/repository/files/${filePath}"
reqBody = """{"branch": "master","encoding":"base64", "content": "${fileContent}", "commit_message": "update a new file"}"""
response = HttpReq('PUT',apiUrl,reqBody)
println(response)
}
//获取文件内容
def GetRepoFile(projectId,filePath){
apiUrl = "projects/${projectId}/repository/files/${filePath}/raw?ref=master"
response = HttpReq('GET',apiUrl,'')
return response.content
}
//创建仓库文件
def CreateRepoFile(projectId,filePath,fileContent){
apiUrl = "projects/${projectId}/repository/files/${filePath}"
reqBody = """{"branch": "master","encoding":"base64", "content": "${fileContent}", "commit_message": "create a new file"}"""
response = HttpReq('POST',apiUrl,reqBody)
println(response)
}
//更改提交状态
def ChangeCommitStatus(projectId,commitSha,status){
commitApi = "projects/${projectId}/statuses/${commitSha}?state=${status}"
response = HttpReq('POST',commitApi,'')
println(response)
return response
}
//获取项目ID
def GetProjectID(repoName='',projectName){
projectApi = "projects?search=${projectName}"
response = HttpReq('GET',projectApi,'')
def result = readJSON text: """${response.content}"""
for (repo in result){
// println(repo['path_with_namespace'])
if (repo['path'] == "${projectName}"){
repoId = repo['id']
println(repoId)
}
}
return repoId
}
//删除分支
def DeleteBranch(projectId,branchName){
apiUrl = "/projects/${projectId}/repository/branches/${branchName}"
response = HttpReq("DELETE",apiUrl,'').content
println(response)
}
//创建分支
def CreateBranch(projectId,refBranch,newBranch){
try {
branchApi = "projects/${projectId}/repository/branches?branch=${newBranch}&ref=${refBranch}"
response = HttpReq("POST",branchApi,'').content
branchInfo = readJSON text: """${response}"""
} catch(e){
println(e)
} //println(branchInfo)
}
//创建合并请求
def CreateMr(projectId,sourceBranch,targetBranch,title,assigneeUser=""){
try {
def mrUrl = "projects/${projectId}/merge_requests"
def reqBody = """{"source_branch":"${sourceBranch}", "target_branch": "${targetBranch}","title":"${title}","assignee_id":"${assigneeUser}"}"""
response = HttpReq("POST",mrUrl,reqBody).content
return response
} catch(e){
println(e)
}
}
//搜索分支
def SearchProjectBranches(projectId,searchKey){
def branchUrl = "projects/${projectId}/repository/branches?search=${searchKey}"
response = HttpReq("GET",branchUrl,'').content
def branchInfo = readJSON text: """${response}"""
def branches = [:]
branches[projectId] = []
if(branchInfo.size() ==0){
return branches
} else {
for (branch in branchInfo){
//println(branch)
branches[projectId] += ["branchName":branch["name"],
"commitMes":branch["commit"]["message"],
"commitId":branch["commit"]["id"],
"merged": branch["merged"],
"createTime": branch["commit"]["created_at"]]
}
return branches
}
}
//允许合并
def AcceptMr(projectId,mergeId){
def apiUrl = "projects/${projectId}/merge_requests/${mergeId}/merge"
HttpReq('PUT',apiUrl,'')
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
- 81.
- 82.
- 83.
- 84.
- 85.
- 86.
- 87.
- 88.
- 89.
- 90.
- 91.
- 92.
- 93.
- 94.
- 95.
- 96.
- 97.
- 98.
- 99.
- 100.
- 101.
- 102.
- 103.
- 104.
- 105.
- 106.
- 107.
- 108.
- 109.
- 110.
- 111.
- 112.
- 113.
- 114.
- 115.
- 116.
- 117.
- 118.
- 119.
- 120.
- 121.
- 122.
- 123.
- 124.
- 125.
- 126.
- 127.
2.3 共享库配置
演示效果:上传了两个小视频,可以扫描进入视频号查看。