Commit b60d012b authored by Simon Latapie's avatar Simon Latapie
Browse files

new acceptance implementation: use a pseudo graph traversal

Changing the acceptance workflow should now be simpler

- Documentation acceptance graphs transitions have simpler checks
- Define a list of MR States, representing the acceptance graphs nodes
- Define state transitions and use a simple algorightm to find the state
  of an MR
parent 91307996
......@@ -3,7 +3,6 @@ package main
import (
"fmt"
"regexp"
"time"
)
// Check is an internal representation of a check
......@@ -41,129 +40,6 @@ func (check *Check) Markdown() string {
return ":x: " + check.baseString()
}
// CheckStatus checks the MR status
func (mr *MergeRequest) CheckStatus() Check {
return Check{
"MR should be considered mergeable by Gitlab",
mr.MergeStatus == MergeStatusCanBeMerged,
fmt.Sprintf("MR internal status is '%s'", mr.MergeStatus),
nil,
}
}
// CheckInactivity checks MR "full" inactivity: no vote, no discussion
func (mr *MergeRequest) CheckInactivity(threshold time.Duration) Check {
description := fmt.Sprintf("No activity on MR (no review, no vote) and last update is longer than %s", threshold)
lastUpdate, err := mr.GetLatestVersionTime()
if err != nil {
return Check{description, false, "", err}
}
logMRVerbose(mr.IID, "Latest MR Version Time:", lastUpdate)
timeSinceLastUpdate := time.Since(lastUpdate)
if timeSinceLastUpdate < threshold {
return Check{description, false, fmt.Sprintf("Last update is %s", timeSinceLastUpdate.Round(time.Minute)), nil}
}
_, votes, _, err := mr.DevScore(false)
if err != nil {
return Check{description, false, "", err}
}
if votes > 0 {
return Check{description, false, fmt.Sprintf("Dev votes have been found (%d)", votes), nil}
}
notes, err := mr.GetAllNotes(true)
if err != nil {
return Check{description, false, "", err}
}
if len(*notes) > 0 {
return Check{description, false, fmt.Sprintf("Discussion threads have been found (%d)", len(*notes)), nil}
}
return Check{description, true, fmt.Sprintf("No dev vote or thread has been found, and time since last update is %s", timeSinceLastUpdate.Round(time.Minute)), nil}
}
// CheckDevScore checks the Developer Votes score.
// threshold is the minimal time before validation
// strict means the score should be strictly positive to be validated
func (mr *MergeRequest) CheckDevScore(threshold time.Duration, strict bool) Check {
description := fmt.Sprintf("Developers Vote Score is high enough for at least %s", threshold)
if strict {
description += ", with a minimum of one vote"
}
score, votes, lastSignChange, err := mr.DevScore(strict)
if err != nil {
return Check{description, false, "", err}
}
if votes == 0 {
if strict {
return Check{description, false, "No Developer vote has been found", nil}
}
// if not strict and there is no vote (developer case), consider the check to be ok.
return Check{description, true, "No Developer vote has been found, score is considered zero", nil}
}
var success bool
var reason string
lastSignChangeStr := lastSignChange.String()
if lastSignChange.IsZero() {
lastSignChangeStr = "never"
}
if strict {
if score <= 0 {
success = false
reason = fmt.Sprintf("Score (%d) is not strictly positive", score)
} else if time.Since(lastSignChange) < threshold {
success = false
reason = fmt.Sprintf("Score (%d) has not been strictly positive long enough (last sign change: %s)", score, lastSignChangeStr)
} else {
success = true
reason = fmt.Sprintf("Score (%d) has been strictly positive long enough (last sign change: %s)", score, lastSignChangeStr)
}
} else {
if score < 0 {
success = false
reason = fmt.Sprintf("Score (%d) is strictly negative", score)
} else if time.Since(lastSignChange) < threshold {
success = false
reason = fmt.Sprintf("Score (%d) has not been positive long enough (last sign change: %s)", score, lastSignChangeStr)
} else {
success = true
reason = fmt.Sprintf("Score (%d) has been positive long enough (last sign change: %s)", score, lastSignChangeStr)
}
}
return Check{description, success, reason, nil}
}
// CheckDiscussions checks all threads in MR have been resolved
// threshold is the minimal time before validation
func (mr *MergeRequest) CheckDiscussions(threshold time.Duration) Check {
description := fmt.Sprintf("All Threads should be resolved for at least %s", threshold)
notes, err := mr.GetAllNotes(true)
if err != nil {
return Check{description, false, "", err}
}
lastResolved := time.Time{}
for _, note := range *notes {
if !note.Resolved {
return Check{description, false, fmt.Sprintf("A thread is still [unresolved](#note_%d)", note.ID), nil}
}
if note.Author.ID != mr.Author.ID && note.ResolvedBy != nil && note.ResolvedBy.ID == mr.Author.ID {
// NOTE: should we do something else ? A "Invalid" label on the MR ?
return Check{description, false, fmt.Sprintf("A thread not started by the MR Author has been closed by the MR Author: [thread](#note_%d)", note.ID), nil}
}
// resolved + legit => take updatetime into account
if note.UpdatedAt.After(lastResolved) {
lastResolved = note.UpdatedAt
}
}
if time.Since(lastResolved) < threshold {
return Check{description, false, fmt.Sprintf("Threads have not been resolved for enough time (last resolved: %s)", lastResolved), nil}
}
return Check{
description,
true,
"No unresolved thread has been found",
nil,
}
}
// IsWelcomeMessagePresent detects if the bot already wrote a Welcome message
func (mr *MergeRequest) IsWelcomeMessagePresent() (bool, error) {
notes, err := mr.GetAllNotes(false)
......@@ -191,27 +67,9 @@ func (mr *MergeRequest) GenerateWelcomeMessage() (string, error) {
message += fmt.Sprintln("When all the following conditions are fulfilled, your MergeRequest will be reviewed by the Team:")
message += fmt.Sprintln("- the check pipeline pass")
message += fmt.Sprintln("- the MR is considered as 'mergeable' by gitlab")
authorIsDev, err := mr.IsAuthorDevOrHigher()
if err != nil {
return "", err
}
message += fmt.Sprintln("")
message += fmt.Sprintln("<details><summary>Click here for more details about the acceptance conditions</summary><p>")
message += fmt.Sprintln("You can find more details about the acceptance process [here](", acceptanceDocURL, ").")
message += fmt.Sprintln("")
message += fmt.Sprintln("### Acceptance conditions")
message += fmt.Sprintln("Your MergeRequest will be accepted if:")
if authorIsDev {
message += fmt.Sprintln("- There is no activity on this MergeRequest for at least ", mrInactiveHours, "h")
message += fmt.Sprintln("")
message += fmt.Sprintln("*OR*")
message += fmt.Sprintln("")
message += fmt.Sprintln("- All discussions have been considered resolved (and closed) for at least ", mrDevDiscussionHours, "h")
message += fmt.Sprintln("- The developers vote balance (", ":"+VoteUp+":", "/", ":"+VoteDown+":", ") is positive for at least", mrDevScoreHours, "h")
} else {
message += fmt.Sprintln("- All discussions have been considered resolved (and closed) for at least ", mrOtherDiscussionHours, "h")
message += fmt.Sprintln("- The developers vote balance (", ":"+VoteUp+":", "/", ":"+VoteDown+":", ") is strictly positive for at least", mrOtherScoreHours, "h")
}
message += fmt.Sprintln("</p></details>")
// Add messagetype in metadata
meta, err := generateMetadataString(Metadata{MessageType: messageTypeWelcome})
if err != nil {
......@@ -243,68 +101,3 @@ func (mr *MergeRequest) GetAllNotesReferingUser(user User) (*[]Note, error) {
}
return &notes, nil
}
// CheckPipelines checks last pipeline of MR is successful
func (mr *MergeRequest) CheckPipelines() Check {
description := "Last pipeline should be successful"
headPipeline, err := mr.GetHeadPipeline()
if err != nil {
return Check{description, false, "", err}
}
if headPipeline == nil {
return Check{description, false, "No pipeline has been found", nil}
}
return Check{
description,
headPipeline.Status == PipelineStatusSuccess,
fmt.Sprintf("Last Pipeline (%d) has status %s", headPipeline.ID, headPipeline.Status),
nil,
}
}
// CheckAuthorIsDev checks if the author has a developer access or higher
// This is an optional check that is used to validate the "Inactivity" scenario
func (mr *MergeRequest) CheckAuthorIsDev() Check {
description := "Check whether MR author has developer access or higher"
authorIsDev, err := mr.IsAuthorDevOrHigher()
if err != nil {
return Check{description, false, "", err}
}
if authorIsDev {
return Check{description, true, "Author has developer access or higher", nil}
}
return Check{description, false, "Author does NOT have developer access or higher", nil}
}
// Analyze generates a the analysis report of the MR
// A "report" is a list of checks
func (mr *MergeRequest) Analyze() []Check {
checks := []Check{}
// MANDATORY checks
checks = append(checks, mr.CheckStatus())
checks = append(checks, mr.CheckPipelines())
// Checks that depend on author role (>= developer or not)
devCheck := mr.CheckAuthorIsDev()
if devCheck.Error != nil {
// Unable to determine the author role, aborting.
checks = append(checks, devCheck)
return checks
}
if devCheck.Success {
// Author is >= developer
checks = append(checks, devCheck)
// Special Case: inactivity on a developer MR
inactiveCheck := mr.CheckInactivity(time.Duration(mrInactiveHours) * time.Hour)
if inactiveCheck.Success {
checks = append(checks, inactiveCheck)
return checks
}
logMRVerbose(mr.IID, "[OPTIONAL] ", inactiveCheck.String())
checks = append(checks, mr.CheckDevScore(time.Duration(mrDevScoreHours)*time.Hour, false))
checks = append(checks, mr.CheckDiscussions(time.Duration(mrDevDiscussionHours)*time.Hour))
} else {
checks = append(checks, mr.CheckDevScore(time.Duration(mrOtherScoreHours)*time.Hour, true))
checks = append(checks, mr.CheckDiscussions(time.Duration(mrOtherDiscussionHours)*time.Hour))
}
return checks
}
......@@ -15,8 +15,10 @@ stateDiagram
Submitted: CR Submitted
InReview: In Review
[*] --> Submitted: A VLC developer submitted
Submitted --> Rejected: CI NOK\nor\nnot mergeable
Submitted --> Reviewable: CI OK\nand\nmergeable
Submitted --> Rejected: NOT mergeable by gitlab
Submitted --> PipelineReady: mergeable by gitlab
PipelineReady --> Rejected: CI NOK
PipelineReady --> Reviewable: CI OK
Reviewable --> InReview: Thread opened\nor\nAnother Developer voted
Reviewable --> Accepted: No vote\nNo thread opened\nduring 72h
InReview --> Acceptable: All threads resolved\nand\nScore >= 0
......@@ -34,8 +36,10 @@ stateDiagram
Submitted: CR Submitted
InReview: In Review
[*] --> Submitted: A VLC developer submitted
Submitted --> Rejected: CI NOK\nor\nnot mergeable
Submitted --> Reviewable: CI OK\nand\nmergeable
Submitted --> Rejected: NOT mergeable by gitlab
Submitted --> PipelineReady: mergeable by gitlab
PipelineReady --> Rejected: CI NOK
PipelineReady --> Reviewable: CI OK
Reviewable --> InReview: Thread opened\nor\nAnother Developer voted
InReview --> Acceptable: All threads resolved\nand\nlen(votes) > 0\nand\n Score > 0
Acceptable --> Accepted: Wait for 72h
......
package main
import (
"fmt"
"time"
)
const (
InitialState = GraphNode("Initial")
RejectedState = GraphNode("Rejected")
PipelineReadyState = GraphNode("PipelineReadyState")
ReviewableState = GraphNode("Reviewable")
InReviewState = GraphNode("InReview")
AcceptableState = GraphNode("Acceptable")
AcceptedState = GraphNode("Accepted")
NoneState = GraphNode("None")
MaxTransitions = 100 // Max transitions on graph - avoid graph infinite loop
)
type GraphNode string
type GraphEdge struct {
From GraphNode
Condition func(*MergeRequest) Check
To GraphNode
ToOnFalse GraphNode
}
func SimpleEdge(from GraphNode, condition func(*MergeRequest) Check, to GraphNode) GraphEdge {
return GraphEdge{from, condition, to, NoneState}
}
// FindMRGraphState computes the Node where the MR should be.
// returns the final GraphNode, and the list of checks performed
// if the GraphNode is NoneState, something gone wrong.
func FindMRGraphState(mr *MergeRequest, graph []GraphEdge) (GraphNode, []Check) {
checks := []Check{}
state := InitialState
iteration := 0
for ; iteration < MaxTransitions; iteration++ {
transit := false
for _, edge := range graph {
if edge.From == state {
logMRVerbose(mr.IID, "found a graph edge for state ", state)
check := edge.Condition(mr)
logMR(mr.IID, check.String())
checks = append(checks, check)
if check.Error != nil {
return NoneState, checks
}
if check.Success {
state = edge.To
logMRVerbose(mr.IID, "transit to state ", state)
transit = true
break
} else if edge.ToOnFalse != NoneState && state != edge.ToOnFalse {
state = edge.ToOnFalse
logMRVerbose(mr.IID, "transit to state ", state)
transit = true
break
}
}
}
if !transit {
logMRVerbose(mr.IID, "No transition from state ", state, ", this is the final state")
break
}
}
if iteration == MaxTransitions {
// We should never been there: this means your Graph is not deterministic.
panic("Infinite loop in graph traversal!")
}
return state, checks
}
func UserAcceptanceGraph() []GraphEdge {
graph := []GraphEdge{}
graph = append(graph, GraphEdge{InitialState, CheckMergeable, PipelineReadyState, RejectedState})
graph = append(graph, GraphEdge{PipelineReadyState, CheckPipelines, ReviewableState, RejectedState})
graph = append(graph, SimpleEdge(ReviewableState, CheckVoteOrThread, InReviewState))
graph = append(graph, SimpleEdge(InReviewState, CheckAllThreadResolvedAndDevScoreStrict, AcceptableState))
graph = append(graph, SimpleEdge(AcceptableState, CheckIsNotActiveAnymoreUser, AcceptedState))
// The other edges (Acceptable -> In Review, Accepted -> In Review, etc.) will never be trigger for now:
// Homer will never reach the Acceptable/In Review states - votes or discussions will not meet the requirements
return graph
}
func DeveloperAcceptanceGraph() []GraphEdge {
graph := []GraphEdge{}
graph = append(graph, GraphEdge{InitialState, CheckMergeable, PipelineReadyState, RejectedState})
graph = append(graph, GraphEdge{PipelineReadyState, CheckPipelines, ReviewableState, RejectedState})
graph = append(graph, SimpleEdge(ReviewableState, CheckVoteOrThread, InReviewState))
graph = append(graph, SimpleEdge(ReviewableState, CheckNoActivity, AcceptedState))
graph = append(graph, SimpleEdge(InReviewState, CheckAllThreadResolvedAndDevScoreNonStrict, AcceptableState))
graph = append(graph, SimpleEdge(AcceptableState, CheckIsNotActiveAnymoreDeveloper, AcceptedState))
// The other edges (Acceptable -> In Review, Accepted -> In Review, etc.) will never be trigger for now:
// Homer will never reach the Acceptable/In Review states - votes or discussions will not meet the requirements
return graph
}
// CheckMergeable checks the MR status is considered mergeable
func CheckMergeable(mr *MergeRequest) Check {
return Check{
"MR should be considered mergeable by Gitlab",
mr.MergeStatus == MergeStatusCanBeMerged,
fmt.Sprintf("MR internal status is '%s'", mr.MergeStatus),
nil,
}
}
// CheckPipelines checks last pipeline of MR is successful
func CheckPipelines(mr *MergeRequest) Check {
description := "Last pipeline should be successful"
headPipeline, err := mr.GetHeadPipeline()
if err != nil {
return Check{description, false, "", err}
}
if headPipeline == nil {
return Check{description, false, "No pipeline has been found", nil}
}
return Check{
description,
headPipeline.Status == PipelineStatusSuccess,
fmt.Sprintf("Last Pipeline (%d) has status %s", headPipeline.ID, headPipeline.Status),
nil,
}
}
func CheckVoteOrThread(mr *MergeRequest) Check {
description := "MergeRequest should have at least one review and/or vote"
// strictly or not, we only need the number of votes here
_, votes, _, err := mr.DevScore(false)
if err != nil {
return Check{description, false, "", err}
}
resolvableNotes, err := mr.GetAllNotes(true)
if err != nil {
return Check{description, false, "", err}
}
noteFound := false
for _, note := range *resolvableNotes {
if note.Author.ID != mr.Author.ID {
noteFound = true
break
}
}
return Check{
description,
votes > 0 || noteFound,
fmt.Sprintf("Votes: %d, Note found: %t", votes, noteFound),
nil,
}
}
// CheckNoActivity checks MR "full" inactivity: no vote, no discussion (opened or closed)
func CheckNoActivity(mr *MergeRequest) Check {
threshold := time.Duration(mrInactiveHours) * time.Hour
description := fmt.Sprintf("No activity on MR (no review, no vote) and last update is longer than %s", threshold)
lastUpdate, err := mr.GetLatestVersionTime()
if err != nil {
return Check{description, false, "", err}
}
logMRVerbose(mr.IID, "Latest MR Version Time:", lastUpdate)
timeSinceLastUpdate := time.Since(lastUpdate)
if timeSinceLastUpdate < threshold {
return Check{description, false, fmt.Sprintf("Last update is %s", timeSinceLastUpdate.Round(time.Minute)), nil}
}
_, votes, _, err := mr.DevScore(false)
if err != nil {
return Check{description, false, "", err}
}
if votes > 0 {
return Check{description, false, fmt.Sprintf("Dev votes have been found (%d)", votes), nil}
}
resolvableNotes, err := mr.GetAllNotes(true)
if err != nil {
return Check{description, false, "", err}
}
if len(*resolvableNotes) > 0 {
return Check{description, false, fmt.Sprintf("Discussion threads have been found (%d)", len(*resolvableNotes)), nil}
}
return Check{description, true, fmt.Sprintf("No developer vote or thread has been found, and time since last update is %s", timeSinceLastUpdate.Round(time.Minute)), nil}
}
func CheckAllThreadResolvedAndDevScoreStrict(mr *MergeRequest) Check {
description := "All threads should be resolved, have votes and score > 0"
check, _ := CheckAllThreadResolved(mr)
if check.Error != nil || !check.Success {
return check
}
score, votes, _, err := mr.DevScore(true)
if err != nil {
return Check{description, false, "", err}
}
if votes == 0 {
return Check{description, false, "No Developer vote has been found", nil}
}
if score <= 0 {
return Check{description, false, fmt.Sprintf("Score is not strictly positive (%d)", score), nil}
}
return Check{description, true, fmt.Sprintf("All threads seem resolved, developer votes (%d) found and score is strictly positive (%d)", votes, score), nil}
}
func CheckAllThreadResolvedAndDevScoreNonStrict(mr *MergeRequest) Check {
description := "All threads should be resolved, have votes and score >= 0"
check, _ := CheckAllThreadResolved(mr)
if check.Error != nil || !check.Success {
return check
}
score, _, _, err := mr.DevScore(false)
if err != nil {
return Check{description, false, "", err}
}
if score < 0 {
return Check{description, false, fmt.Sprintf("Score is not positive (%d)", score), nil}
}
return Check{description, true, fmt.Sprintf("All threads seem resolved, and developers score is positive (%d)", score), nil}
}
// CheckIsNotActiveAnymoreUser checks if a User MR has been inactive for a certain time
func CheckIsNotActiveAnymoreUser(mr *MergeRequest) Check {
return CheckIsNotActiveAnymore(mr, mrOtherDiscussionHours, mrOtherScoreHours, true)
}
// CheckIsNotActiveAnymoreDeveloper checks if a Developer MR has been inactive for a certain time
func CheckIsNotActiveAnymoreDeveloper(mr *MergeRequest) Check {
return CheckIsNotActiveAnymore(mr, mrDevDiscussionHours, mrDevScoreHours, false)
}
// CheckIsNotActiveAnymore checks if a MR has been inactiv for a certain time
func CheckIsNotActiveAnymore(mr *MergeRequest, discussionHoursThreshold int64, scoreHoursThreshold int64, strictly bool) Check {
description := fmt.Sprintf("MergeRequest should have no activity (threads/votes) for (%d/%d)h", discussionHoursThreshold, scoreHoursThreshold)
check, lastResolved := CheckAllThreadResolved(mr)
if check.Error != nil || !check.Success {
return check
}
discussionThreshold := time.Duration(discussionHoursThreshold) * time.Hour
// lastResolved can be zero time, but this has no consequence here
if since := time.Since(lastResolved); !lastResolved.IsZero() && since < discussionThreshold {
return Check{description, false, fmt.Sprintf("Discussions have been resolved, but too soon (%fh)", since.Hours()), nil}
}
_, _, lastSignChange, err := mr.DevScore(strictly)
if err != nil {
return Check{description, false, "", err}
}
voteThreshold := time.Duration(scoreHoursThreshold) * time.Hour
// lastSignChange can be zero time, in case of a Developer MR
if since := time.Since(lastSignChange); since < voteThreshold {
return Check{description, false, fmt.Sprintf("Score is strictly positive, but too soon (%fh)", since.Hours()), nil}
}
return Check{description, true, "All threads seem resolved, developer votes is strictly positive, both for enough time", nil}
}
// CheckAllThreadResolved checks all threads in MR have been resolved
// Returns check, and last resolved time (zero if none)
func CheckAllThreadResolved(mr *MergeRequest) (Check, time.Time) {
description := fmt.Sprintf("All Threads should be resolved")
notes, err := mr.GetAllNotes(true)
if err != nil {
return Check{description, false, "", err}, time.Time{}
}
lastResolved := time.Time{}
for _, note := range *notes {
if !note.Resolved {
return Check{description, false, fmt.Sprintf("A thread is still [unresolved](#note_%d)", note.ID), nil}, time.Time{}
}
if note.Author.ID != mr.Author.ID && note.ResolvedBy != nil && note.ResolvedBy.ID == mr.Author.ID {
// NOTE: should we do something else ? A "Invalid" label on the MR ?
return Check{description, false, fmt.Sprintf("A thread not started by the MR Author has been closed by the MR Author: [thread](#note_%d)", note.ID), nil}, time.Time{}
}
// resolved + legit => take updatetime into account
if note.UpdatedAt.After(lastResolved) {
lastResolved = note.UpdatedAt
}
}
return Check{description, true, "No unresolved thread has been found", nil}, lastResolved
}
......@@ -36,6 +36,7 @@ var (
mrOtherScoreHours int64
mrDevDiscussionHours int64
mrOtherDiscussionHours int64
acceptanceDocURL string
)
func logMR(mrID int64, v ...interface{}) {
......@@ -70,17 +71,13 @@ func retrieveMetadataFromString(message string, metadata *Metadata) (bool, error
}
// Markdown report
func generateReport(mr MergeRequest, checks []Check) string {
fullSuccess := true
func generateReport(mr MergeRequest, success bool, checks []Check) string {
details := fmt.Sprintln("### MR acceptance checks details")
for _, check := range checks {
details += fmt.Sprintln("* ", check.Markdown())
if !check.Success {
fullSuccess = false
}
}
report := fmt.Sprintln("### MR Acceptance result")
if fullSuccess {
if success {
report += fmt.Sprintln(":tada: This MergeRequest has been **Accepted**! Congratulations.")
} else {
report += fmt.Sprintln(":no_entry_sign: This MergeRequest has not been accepted (yet?).")
......@@ -92,7 +89,7 @@ func generateReport(mr MergeRequest, checks []Check) string {
report += fmt.Sprintln("</p></details>")
// Add a message identifier (maybe for future use)
metadata := Metadata{MessageType: messageTypeReport, ReportResult: rejectedLabel}
if fullSuccess {
if success {
metadata.ReportResult = acceptedLabel
}
if meta, err := generateMetadataString(metadata); err == nil {
......@@ -143,6 +140,7 @@ func main() {
flag.Int64Var(&mrOtherScoreHours, "mr-otherscore-hours", 72, "hours before considering a >0 vote score for a external participant MR is OK")
flag.Int64Var(&mrDevDiscussionHours, "mr-devdiscussion-hours", 24, "hours before considering resolved threads for a developer MR are OK")
flag.Int64Var(&mrOtherDiscussionHours, "mr-otherdiscussion-hours", 72, "hours before considering resolved threads for an external participant MR are OK")
flag.StringVar(&acceptanceDocURL, "acceptance-doc-url", "https://code.videolan.org/Garf/homer-bot/-/blob/master/doc/acceptance.md", "link to the Acceptance process documentation")
flag.Parse()
if projectID == 0 {
log.Println("Please provide a Project ID.")
......@@ -193,57 +191,57 @@ func main() {
}
}
}
// Analyze MR
checks := mr.Analyze()
fullSuccess := true
var failedCheck Check
var checkError error = nil
for _, check := range checks {
if !check.Success {
fullSuccess = false
failedCheck = check
}
if check.Error != nil {
checkError = check.Error
}
logMRVerbose(mr.IID, check.String())
}
if checkError != nil {
logMR(mr.IID, "ERROR! Something gone wrong during the MR analyze: ", checkError.Error())
// Do not perform any operation if an error occured
errors <- checkError
// Get MR state
authorIsDev, err := mr.IsAuthorDevOrHigher()
if err != nil {
errors <- err
return
} else if fullSuccess {
logMR(mr.IID, "SUCCESS! MergeRequest is fully compliant!")
}
var state GraphNode
var stateChecks []Check
if authorIsDev {
state, stateChecks = FindMRGraphState(&mr, DeveloperAcceptanceGraph())