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, "
SAGA 开始执行,请大佬稍后......
"); err != nil { return } taskInfo.NoteID = noteID // 1, check wip if wip { c.gitlab.UpdateMRNote(projID, mrIID, noteID, "
警告:当前MR处于WIP状态,请待开发结束后再merge!
") 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, "
SAGA 提示:为了保证合进主干后能正常编译,正在重跑pipeline,等待时间取决于pipeline运行时间!请耐心等待!
"); 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("
警告:pipeline状态异常,请确保pipeline状态正常后再执行merge操作!
") 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("
[%s]尝试合并失败,当前状态不允许合并,请查看上方merge按钮旁的提示!
", username) } else { comment = fmt.Sprintf("
[%s]尝试合并失败,请检查当前状态或同步目标分支代码后再试!
", username) } go notification.WechatAuthor(c.dao, author, url, sourceBranch, targetBranch, comment) c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment) return } ok = true comment = fmt.Sprintf("
[%s]尝试合并成功!
", 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 = "
SAGA 尝试合并失败,当前MR是一项正在进行的工作!若已完成请先点击“Resolve WIP status”按钮处理后再+merge!
" } else if state != model.MergeStateOpened { comment = "
SAGA 尝试合并失败,当前MR已经关闭或者已经合并!
" } else if status != model.MergeStatusOk { comment = "
SAGA 尝试合并失败,请先解决合并冲突!
" } 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("
警告:SAGA 无法执行+merge,发版阶段只允许合入指定label的MR!
") ) 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("
警告:SAGA 检测到重新提交代码了,+merge中断!请重新review代码!
") 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("
警告:pipeline正在运行中,暂不能立即merge,待pipeline运行通过后会自动执行merge操作!
") 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("
警告:pipeline状态异常,请确保pipeline状态正常后再执行merge操作!
") err = c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment) return } // query success status if queryStatus == model.QuerySuccess { if status != model.PipelineSuccess { comment := fmt.Sprintf("
警告:SAGA 无法执行+merge,pipeline还未成功,请大佬先让pipeline执行通过!
") 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("
SAGA 提示:当前还有 [%d] 个 MR 等待合并,请大佬耐心等待!
", waitNum) c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment) } else if runningNum > 0 { comment = fmt.Sprintf("
SAGA 提示:当前还有 [%d] 个 MR 正在执行,请大佬耐心等待!
", runningNum) c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment) } return }