You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
529 lines
16 KiB
529 lines
16 KiB
package command |
|
|
|
import ( |
|
"context" |
|
"fmt" |
|
"runtime/debug" |
|
|
|
"go-common/app/tool/saga/model" |
|
"go-common/app/tool/saga/service/notification" |
|
"go-common/library/log" |
|
|
|
ggitlab "github.com/xanzy/go-gitlab" |
|
) |
|
|
|
func (c *Command) runTryMerge(ctx context.Context, event *model.HookComment, repo *model.Repo) (err error) { |
|
var ( |
|
ok bool |
|
canMerge bool |
|
projID = int(event.MergeRequest.SourceProjectID) |
|
mrIID = int(event.MergeRequest.IID) |
|
wip = event.MergeRequest.WorkInProgress |
|
noteID int |
|
taskInfo = &model.TaskInfo{ |
|
Event: event, |
|
Repo: repo, |
|
} |
|
) |
|
log.Info("runTryMerge start ... MRIID: %d, Repo Config: %+v", mrIID, repo.Config) |
|
|
|
if ok, err = c.dao.ExistMRIID(ctx, mrIID); err != nil || ok { |
|
return |
|
} |
|
if noteID, err = c.gitlab.CreateMRNote(projID, mrIID, "<pre>SAGA 开始执行,请大佬稍后......</pre>"); err != nil { |
|
return |
|
} |
|
taskInfo.NoteID = noteID |
|
|
|
// 1, check wip |
|
if wip { |
|
c.gitlab.UpdateMRNote(projID, mrIID, noteID, "<pre>警告:当前MR处于WIP状态,请待开发结束后再merge!</pre>") |
|
return |
|
} |
|
// 2, check labels |
|
if ok, err = c.checkLabels(projID, mrIID, noteID, repo); err != nil || !ok { |
|
return |
|
} |
|
|
|
// 3, check merge status |
|
if canMerge, err = c.checkMergeStatus(projID, mrIID, noteID); err != nil || !canMerge { |
|
return |
|
} |
|
|
|
// 4, check pipeline status |
|
if repo.Config.RelatePipeline { |
|
if repo.Config.DelayMerge { |
|
if ok, _, err = c.checkPipeline(projID, mrIID, noteID, 0, model.QueryProcessing); err != nil || !ok { |
|
return |
|
} |
|
} else { |
|
if ok, _, err = c.checkPipeline(projID, mrIID, noteID, 0, model.QuerySuccess); err != nil || !ok { |
|
return |
|
} |
|
} |
|
} |
|
|
|
// 5, check path auth |
|
if ok, err = c.checkAllPathAuth(taskInfo); err != nil || !ok { |
|
return |
|
} |
|
|
|
// 6, show current mr queue info |
|
c.showMRQueueInfo(ctx, taskInfo) |
|
|
|
if err = c.dao.PushMergeTask(ctx, model.TaskStatusWaiting, taskInfo); err != nil { |
|
return |
|
} |
|
if err = c.dao.AddMRIID(ctx, mrIID, int(repo.Config.LockTimeout)); err != nil { |
|
return |
|
} |
|
log.Info("runTryMerge merge task 已加入 waiting 任务列队中... MRIID: %d", mrIID) |
|
return |
|
} |
|
|
|
func (c *Command) execMergeTask(taskInfo *model.TaskInfo) (err error) { |
|
var ( |
|
ctx = context.TODO() |
|
projID = int(taskInfo.Event.MergeRequest.SourceProjectID) |
|
mrIID = int(taskInfo.Event.MergeRequest.IID) |
|
sourceBranch = taskInfo.Event.MergeRequest.SourceBranch |
|
pipeline = &ggitlab.Pipeline{} |
|
noteID = taskInfo.NoteID |
|
mergeInfo = &model.MergeInfo{ |
|
ProjID: projID, |
|
MRIID: mrIID, |
|
URL: taskInfo.Event.ObjectAttributes.URL, |
|
AuthBranches: taskInfo.Repo.Config.AuthBranches, |
|
SourceBranch: taskInfo.Event.MergeRequest.SourceBranch, |
|
TargetBranch: taskInfo.Event.MergeRequest.TargetBranch, |
|
AuthorID: int(taskInfo.Event.MergeRequest.AuthorID), |
|
UserName: taskInfo.Event.User.UserName, |
|
MinReviewer: taskInfo.Repo.Config.MinReviewer, |
|
LockTimeout: taskInfo.Repo.Config.LockTimeout, |
|
Title: taskInfo.Event.MergeRequest.Title, |
|
Description: taskInfo.Event.MergeRequest.Description, |
|
} |
|
) |
|
mergeInfo.NoteID = noteID |
|
// 从等待任务列队移除 |
|
if err = c.dao.DeleteMergeTask(ctx, model.TaskStatusWaiting, taskInfo); err != nil { |
|
return |
|
} |
|
// 加入到正在执行任务列队 |
|
if err = c.dao.PushMergeTask(ctx, model.TaskStatusRunning, taskInfo); err != nil { |
|
return |
|
} |
|
|
|
if taskInfo.Repo.Config.RelatePipeline { |
|
if taskInfo.Repo.Config.DelayMerge { |
|
if err = c.HookDelayMerge(projID, sourceBranch, mergeInfo); err != nil { |
|
return |
|
} |
|
return |
|
} |
|
|
|
if err = c.gitlab.UpdateMRNote(projID, mrIID, noteID, "<pre>SAGA 提示:为了保证合进主干后能正常编译,正在重跑pipeline,等待时间取决于pipeline运行时间!请耐心等待!</pre>"); err != nil { |
|
return |
|
} |
|
if pipeline, err = c.retryPipeline(taskInfo.Event); err != nil { |
|
return |
|
} |
|
|
|
mergeInfo.PipelineID = pipeline.ID |
|
if err = c.dao.SetMergeInfo(ctx, projID, sourceBranch, mergeInfo); err != nil { |
|
return |
|
} |
|
} else { |
|
if err = c.HookMerge(projID, sourceBranch, mergeInfo); err != nil { |
|
return |
|
} |
|
} |
|
return |
|
} |
|
|
|
func (c *Command) retryPipeline(event *model.HookComment) (pipeline *ggitlab.Pipeline, err error) { |
|
var ( |
|
trigger *ggitlab.PipelineTrigger |
|
triggers []*ggitlab.PipelineTrigger |
|
projID = int(event.MergeRequest.SourceProjectID) |
|
sourceBranch = event.MergeRequest.SourceBranch |
|
) |
|
if triggers, err = c.gitlab.Triggers(projID); err != nil { |
|
return |
|
} |
|
if len(triggers) == 0 { |
|
log.Info("No triggers were found for project %d, try to create it now.", projID) |
|
if trigger, err = c.gitlab.CreateTrigger(projID); err != nil { |
|
return |
|
} |
|
triggers = []*ggitlab.PipelineTrigger{trigger} |
|
} |
|
trigger = triggers[0] |
|
if trigger.Owner == nil || trigger.Owner.ID == 0 { |
|
log.Info("Legacy trigger (without owner), take ownership now.") |
|
if trigger, err = c.gitlab.TakeOwnership(projID, trigger.ID); err != nil { |
|
return |
|
} |
|
} |
|
if pipeline, err = c.gitlab.TriggerPipeline(projID, sourceBranch, trigger.Token); err != nil { |
|
return |
|
} |
|
return |
|
} |
|
|
|
// HookPipeline ... |
|
func (c *Command) HookPipeline(projID int, branch string, pipelineID int) (err error) { |
|
var ( |
|
ok bool |
|
canMerge bool |
|
mergeInfo *model.MergeInfo |
|
) |
|
defer func() { |
|
if x := recover(); x != nil { |
|
log.Error("HookPipeline: %+v %s", x, debug.Stack()) |
|
} |
|
}() |
|
|
|
if ok, mergeInfo, err = c.dao.MergeInfo(context.TODO(), projID, branch); err != nil || !ok { |
|
return |
|
} |
|
log.Info("HookPipeline projID: %d, MRIID: %d, branch: %s, pipelineId: %d", projID, mergeInfo.MRIID, branch, mergeInfo.PipelineID) |
|
if pipelineID < mergeInfo.PipelineID { |
|
return |
|
} |
|
|
|
defer func() { |
|
if err = c.resetMergeStatus(projID, mergeInfo.MRIID, branch, true); err != nil { |
|
log.Error("resetMergeStatus MRIID: %d, error: %+v", mergeInfo.MRIID, err) |
|
} |
|
}() |
|
|
|
// 1, check pipeline id |
|
if ok, _, err = c.checkPipeline(projID, mergeInfo.MRIID, mergeInfo.NoteID, mergeInfo.PipelineID, model.QueryID); err != nil || !ok { |
|
return |
|
} |
|
// 2, check pipeline status |
|
if ok, _, err = c.checkPipeline(projID, mergeInfo.MRIID, mergeInfo.NoteID, 0, model.QuerySuccess); err != nil || !ok { |
|
return |
|
} |
|
// 3, check merge status |
|
if canMerge, err = c.checkMergeStatus(projID, mergeInfo.MRIID, mergeInfo.NoteID); err != nil || !canMerge { |
|
return |
|
} |
|
|
|
log.Info("HookPipeline acceptMerge ... MRIID: %d", mergeInfo.MRIID) |
|
if ok, err = c.acceptMerge(mergeInfo); err != nil || !ok { |
|
return |
|
} |
|
return |
|
} |
|
|
|
// HookMerge ... |
|
func (c *Command) HookMerge(projID int, branch string, mergeInfo *model.MergeInfo) (err error) { |
|
var ( |
|
ok bool |
|
canMerge bool |
|
) |
|
defer func() { |
|
if x := recover(); x != nil { |
|
log.Error("HookMerge: %+v %s", x, debug.Stack()) |
|
} |
|
}() |
|
defer func() { |
|
if err = c.resetMergeStatus(projID, mergeInfo.MRIID, branch, true); err != nil { |
|
log.Error("resetMergeStatus MRIID: %d, error: %+v", mergeInfo.MRIID, err) |
|
} |
|
}() |
|
|
|
log.Info("HookMerge projID: %d, MRIID: %d, branch: %s", projID, mergeInfo.MRIID, branch) |
|
if canMerge, err = c.checkMergeStatus(projID, mergeInfo.MRIID, mergeInfo.NoteID); err != nil || !canMerge { |
|
return |
|
} |
|
|
|
log.Info("HookMerge acceptMerge ... MRIID: %d", mergeInfo.MRIID) |
|
if ok, err = c.acceptMerge(mergeInfo); err != nil || !ok { |
|
return |
|
} |
|
return |
|
} |
|
|
|
// HookDelayMerge ... |
|
func (c *Command) HookDelayMerge(projID int, branch string, mergeInfo *model.MergeInfo) (err error) { |
|
var ( |
|
ctx = context.TODO() |
|
ok bool |
|
noteID = mergeInfo.NoteID |
|
mrIID = mergeInfo.MRIID |
|
pipelineID int |
|
status string |
|
) |
|
defer func() { |
|
if x := recover(); x != nil { |
|
log.Error("HookDelayMerge: %+v %s", x, debug.Stack()) |
|
} |
|
}() |
|
|
|
//if ok, pipelineID, err = c.checkPipeline(projID, mrIID, noteID, 0, model.QuerySuccessRmNote); err != nil { |
|
//return |
|
//} |
|
if pipelineID, status, err = c.gitlab.MRPipelineStatus(projID, mrIID); err != nil { |
|
return |
|
} |
|
if status == model.PipelineSuccess || status == model.PipelineSkipped { |
|
ok = true |
|
} else if status != model.PipelineRunning && status != model.PipelinePending { |
|
comment := fmt.Sprintf("<pre>警告:pipeline状态异常,请确保pipeline状态正常后再执行merge操作!</pre>") |
|
err = c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment) |
|
return |
|
} |
|
|
|
log.Info("HookDelayMerge projID: %d, MRIID: %d, branch: %s, pipeline status: %t", projID, mergeInfo.MRIID, branch, ok) |
|
if ok { |
|
if err = c.HookMerge(projID, branch, mergeInfo); err != nil { |
|
return |
|
} |
|
} else { |
|
mergeInfo.PipelineID = pipelineID |
|
if err = c.dao.SetMergeInfo(ctx, projID, branch, mergeInfo); err != nil { |
|
return |
|
} |
|
} |
|
return |
|
} |
|
|
|
func (c *Command) acceptMerge(mergeInfo *model.MergeInfo) (ok bool, err error) { |
|
var ( |
|
comment string |
|
author string |
|
canMerge bool |
|
state string |
|
authorID = mergeInfo.AuthorID |
|
username = mergeInfo.UserName |
|
projID = mergeInfo.ProjID |
|
mrIID = mergeInfo.MRIID |
|
url = mergeInfo.URL |
|
sourceBranch = mergeInfo.SourceBranch |
|
targetBranch = mergeInfo.TargetBranch |
|
noteID = mergeInfo.NoteID |
|
content = mergeInfo.Title |
|
) |
|
if author, err = c.gitlab.UserName(authorID); err != nil { |
|
return |
|
} |
|
if canMerge, err = c.checkMergeStatus(projID, mrIID, noteID); err != nil { |
|
return |
|
} |
|
if !canMerge { |
|
go notification.WechatAuthor(c.dao, author, url, sourceBranch, targetBranch, comment) |
|
return |
|
} |
|
|
|
if len(mergeInfo.Description) > 0 { |
|
content = content + "\n\n" + mergeInfo.Description |
|
} |
|
mergeMSG := fmt.Sprintf("Merge branch [%s] into [%s] by [%s]\n%s", sourceBranch, targetBranch, username, content) |
|
if state, err = c.gitlab.AcceptMR(projID, mrIID, mergeMSG); err != nil || state != model.MRStateMerged { |
|
if err != nil { |
|
comment = fmt.Sprintf("<pre>[%s]尝试合并失败,当前状态不允许合并,请查看上方merge按钮旁的提示!</pre>", username) |
|
} else { |
|
comment = fmt.Sprintf("<pre>[%s]尝试合并失败,请检查当前状态或同步目标分支代码后再试!</pre>", username) |
|
} |
|
go notification.WechatAuthor(c.dao, author, url, sourceBranch, targetBranch, comment) |
|
c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment) |
|
return |
|
} |
|
ok = true |
|
comment = fmt.Sprintf("<pre>[%s]尝试合并成功!</pre>", username) |
|
go notification.WechatAuthor(c.dao, author, url, sourceBranch, targetBranch, comment) |
|
c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment) |
|
return |
|
} |
|
|
|
func (c *Command) resetMergeStatus(projID int, MRIID int, branch string, taskRunning bool) (err error) { |
|
var ( |
|
ctx = context.TODO() |
|
) |
|
|
|
log.Info("resetMergeStatus projID: %d, MRIID: %d start", projID, MRIID) |
|
if err = c.dao.UnLock(ctx, fmt.Sprintf(model.SagaRepoLockKey, projID)); err != nil { |
|
log.Error("UnLock error: %+v", err) |
|
} |
|
if err = c.dao.DeleteMergeInfo(ctx, projID, branch); err != nil { |
|
log.Error("DeleteMergeInfo error: %+v", err) |
|
} |
|
if err = c.dao.DeleteMRIID(ctx, MRIID); err != nil { |
|
log.Error("Delete MRIID :%d, error: %+v", MRIID, err) |
|
} |
|
if taskRunning { |
|
if err = c.DeleteRunningTask(projID, MRIID); err != nil { |
|
log.Error("DeleteRunningTask: %+v", err) |
|
} |
|
} |
|
log.Info("resetMergeStatus projID: %d, MRIID: %d end!", projID, MRIID) |
|
return |
|
} |
|
|
|
// DeleteRunningTask ... |
|
func (c *Command) DeleteRunningTask(projID int, mrID int) (err error) { |
|
var ( |
|
ctx = context.TODO() |
|
taskInfos []*model.TaskInfo |
|
) |
|
|
|
if _, taskInfos, err = c.dao.MergeTasks(ctx, model.TaskStatusRunning); err != nil { |
|
return |
|
} |
|
for _, taskInfo := range taskInfos { |
|
pID := int(taskInfo.Event.MergeRequest.SourceProjectID) |
|
mID := int(taskInfo.Event.MergeRequest.IID) |
|
if pID == projID && mID == mrID { |
|
// 从正在运行的任务列队中移除 |
|
err = c.dao.DeleteMergeTask(ctx, model.TaskStatusRunning, taskInfo) |
|
return |
|
} |
|
} |
|
return |
|
} |
|
|
|
func (c *Command) checkMergeStatus(projID int, mrIID int, noteID int) (canMerge bool, err error) { |
|
var ( |
|
wip bool |
|
state string |
|
status string |
|
comment string |
|
) |
|
if wip, state, status, err = c.gitlab.MergeStatus(projID, mrIID); err != nil { |
|
return |
|
} |
|
if wip { |
|
comment = "<pre>SAGA 尝试合并失败,当前MR是一项正在进行的工作!若已完成请先点击“Resolve WIP status”按钮处理后再+merge!</pre>" |
|
} else if state != model.MergeStateOpened { |
|
comment = "<pre>SAGA 尝试合并失败,当前MR已经关闭或者已经合并!</pre>" |
|
} else if status != model.MergeStatusOk { |
|
comment = "<pre>SAGA 尝试合并失败,请先解决合并冲突!</pre>" |
|
} else { |
|
canMerge = true |
|
} |
|
|
|
if len(comment) > 0 { |
|
c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment) |
|
} |
|
|
|
return |
|
} |
|
|
|
// checkLabels ios or android need checkout label when release app stage |
|
func (c *Command) checkLabels(projID int, mrIID int, noteID int, repo *model.Repo) (ok bool, err error) { |
|
var ( |
|
labels []string |
|
comment = fmt.Sprintf("<pre>警告:SAGA 无法执行+merge,发版阶段只允许合入指定label的MR!</pre>") |
|
) |
|
if len(repo.Config.AllowLabel) <= 0 { |
|
ok = true |
|
return |
|
} |
|
|
|
if labels, err = c.gitlab.MergeLabels(projID, mrIID); err != nil { |
|
return |
|
} |
|
|
|
log.Info("checkMrLabels MRIID: %d, labels: %+v", mrIID, labels) |
|
for _, label := range labels { |
|
if label == repo.Config.AllowLabel { |
|
ok = true |
|
return |
|
} |
|
} |
|
if err = c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment); err != nil { |
|
return |
|
} |
|
return |
|
} |
|
|
|
func (c *Command) checkPipeline(projID int, mrIID int, noteID int, lastPipelineID int, queryStatus model.QueryStatus) (ok bool, pipelineID int, err error) { |
|
var status string |
|
if pipelineID, status, err = c.gitlab.MRPipelineStatus(projID, mrIID); err != nil { |
|
return |
|
} |
|
log.Info("checkPipeline MRIID: %d, queryStatus: %d, pipeline status: %s", mrIID, queryStatus, status) |
|
|
|
// query pipeline id index |
|
if queryStatus == model.QueryID { |
|
if pipelineID > lastPipelineID { |
|
comment := fmt.Sprintf("<pre>警告:SAGA 检测到重新提交代码了,+merge中断!请重新review代码!</pre>") |
|
err = c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment) |
|
return |
|
} |
|
ok = true |
|
return |
|
} |
|
|
|
// query process status |
|
if queryStatus == model.QueryProcessing { |
|
if status == model.PipelineRunning || status == model.PipelinePending { |
|
comment := fmt.Sprintf("<pre>警告:pipeline正在运行中,暂不能立即merge,待pipeline运行通过后会自动执行merge操作!</pre>") |
|
err = c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment) |
|
ok = true |
|
return |
|
} else if status == model.PipelineSuccess || status == model.PipelineSkipped { |
|
ok = true |
|
return |
|
} |
|
comment := fmt.Sprintf("<pre>警告:pipeline状态异常,请确保pipeline状态正常后再执行merge操作!</pre>") |
|
err = c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment) |
|
return |
|
} |
|
|
|
// query success status |
|
if queryStatus == model.QuerySuccess { |
|
if status != model.PipelineSuccess { |
|
comment := fmt.Sprintf("<pre>警告:SAGA 无法执行+merge,pipeline还未成功,请大佬先让pipeline执行通过!</pre>") |
|
err = c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment) |
|
ok = false |
|
return |
|
} |
|
} |
|
|
|
ok = true |
|
return |
|
} |
|
|
|
// showMRQueueInfo ... |
|
func (c *Command) showMRQueueInfo(ctx context.Context, taskInfo *model.TaskInfo) (err error) { |
|
var ( |
|
mrIID = int(taskInfo.Event.MergeRequest.IID) |
|
projID = int(taskInfo.Event.MergeRequest.SourceProjectID) |
|
noteID = taskInfo.NoteID |
|
taskInfos []*model.TaskInfo |
|
comment string |
|
waitNum int |
|
runningNum int |
|
) |
|
|
|
if _, taskInfos, err = c.dao.MergeTasks(ctx, model.TaskStatusWaiting); err != nil { |
|
return |
|
} |
|
for _, waitTaskInfo := range taskInfos { |
|
if waitTaskInfo.Event.ProjectID == taskInfo.Event.ProjectID { |
|
waitNum++ |
|
} |
|
} |
|
|
|
if _, taskInfos, err = c.dao.MergeTasks(ctx, model.TaskStatusRunning); err != nil { |
|
return |
|
} |
|
for _, runningTaskInfo := range taskInfos { |
|
if runningTaskInfo.Event.ProjectID == taskInfo.Event.ProjectID { |
|
runningNum++ |
|
} |
|
} |
|
|
|
if waitNum > 0 { |
|
comment = fmt.Sprintf("<pre>SAGA 提示:当前还有 [%d] 个 MR 等待合并,请大佬耐心等待!</pre>", waitNum) |
|
c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment) |
|
} else if runningNum > 0 { |
|
comment = fmt.Sprintf("<pre>SAGA 提示:当前还有 [%d] 个 MR 正在执行,请大佬耐心等待!</pre>", runningNum) |
|
c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment) |
|
} |
|
return |
|
}
|
|
|