diff --git a/contribs/github-bot/Makefile b/contribs/github-bot/Makefile new file mode 100644 index 00000000000..aee43149b7a --- /dev/null +++ b/contribs/github-bot/Makefile @@ -0,0 +1,16 @@ +GNOROOT_DIR ?= $(abspath $(lastword $(MAKEFILE_LIST))/../../../) + +rundep := go run -modfile ../../misc/devdeps/go.mod +golangci_lint := $(rundep) github.com/golangci/golangci-lint/cmd/golangci-lint + +.PHONY: install +install: + go install . + +.PHONY: lint +lint: + $(golangci_lint) --config ../../.github/golangci.yml run ./... + +.PHONY: test +test: + go test $(GOTEST_FLAGS) -v ./... diff --git a/contribs/github-bot/internal/conditions/author.go b/contribs/github-bot/internal/conditions/author.go index 9052f781bd5..19be8298849 100644 --- a/contribs/github-bot/internal/conditions/author.go +++ b/contribs/github-bot/internal/conditions/author.go @@ -58,3 +58,24 @@ func (a *authorInTeam) IsMet(pr *github.PullRequest, details treeprint.Tree) boo func AuthorInTeam(gh *client.GitHub, team string) Condition { return &authorInTeam{gh: gh, team: team} } + +type authorAssociationIs struct { + assoc string +} + +var _ Condition = &authorAssociationIs{} + +func (a *authorAssociationIs) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + detail := fmt.Sprintf("Pull request author has author_association: %q", a.assoc) + + return utils.AddStatusNode(pr.GetAuthorAssociation() == a.assoc, detail, details) +} + +// AuthorAssociationIs asserts that the author of the PR has the given value for +// the GitHub "author association" field, on the PR. +// +// See https://docs.github.com/en/graphql/reference/enums#commentauthorassociation +// for a list of possible values and descriptions. +func AuthorAssociationIs(association string) Condition { + return &authorAssociationIs{assoc: association} +} diff --git a/contribs/github-bot/internal/conditions/author_test.go b/contribs/github-bot/internal/conditions/author_test.go index c5836f1ea76..24d3c51859e 100644 --- a/contribs/github-bot/internal/conditions/author_test.go +++ b/contribs/github-bot/internal/conditions/author_test.go @@ -91,3 +91,30 @@ func TestAuthorInTeam(t *testing.T) { }) } } + +func TestAuthorAssociationIs(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + association string + associationWant string + isMet bool + }{ + {"has", "MEMBER", "MEMBER", true}, + {"hasNot", "COLLABORATOR", "MEMBER", false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{ + AuthorAssociation: github.String(testCase.association), + } + details := treeprint.New() + condition := AuthorAssociationIs(testCase.associationWant) + + assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet)) + }) + } +} diff --git a/contribs/github-bot/internal/conditions/draft.go b/contribs/github-bot/internal/conditions/draft.go index 2c263f2ae75..5d554b4b484 100644 --- a/contribs/github-bot/internal/conditions/draft.go +++ b/contribs/github-bot/internal/conditions/draft.go @@ -10,7 +10,7 @@ import ( // Draft Condition. type draft struct{} -var _ Condition = &baseBranch{} +var _ Condition = &draft{} func (*draft) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { return utils.AddStatusNode(pr.GetDraft(), "This pull request is a draft", details) diff --git a/contribs/github-bot/internal/config/config.go b/contribs/github-bot/internal/config/config.go index f80fc86cb11..83219c04d1d 100644 --- a/contribs/github-bot/internal/config/config.go +++ b/contribs/github-bot/internal/config/config.go @@ -54,6 +54,19 @@ func Config(gh *client.GitHub) ([]AutomaticCheck, []ManualCheck) { If: c.Label("don't merge"), Then: r.Never(), }, + { + Description: "Pending initial approval by a review team member (and label matches review triage state)", + If: c.Not(c.AuthorInTeam(gh, "tech-staff")), + Then: r. + If(r.Or(r.ReviewByOrgMembers(gh, 1), r.Draft())). + // Either there was a first approval from a member, and we + // assert that the label for triage-pending is removed... + Then(r.Not(r.Label(gh, "review/triage-pending", r.LabelRemove))). + // Or there was not, and we apply the triage pending label. + // The requirement should always fail, to mark the PR is not + // ready to be merged. + Else(r.And(r.Label(gh, "review/triage-pending", r.LabelApply), r.Never())), + }, } manual := []ManualCheck{ diff --git a/contribs/github-bot/internal/requirements/boolean.go b/contribs/github-bot/internal/requirements/boolean.go index 6b441c92f80..7d9eea2f0d9 100644 --- a/contribs/github-bot/internal/requirements/boolean.go +++ b/contribs/github-bot/internal/requirements/boolean.go @@ -96,3 +96,69 @@ func (n *not) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { func Not(req Requirement) Requirement { return ¬{req} } + +// IfCondition executes the condition, and based on the result then runs Then +// or Else. +type IfCondition struct { + cond Requirement + then Requirement + els Requirement +} + +var _ Requirement = &IfCondition{} + +func (i *IfCondition) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { + if i.then == nil { + i.then = Always() + } + ifBranch := details.AddBranch("") + condBranch := ifBranch.AddBranch("") + + var ( + target Requirement + targetName string + ) + + if i.cond.IsSatisfied(pr, condBranch) { + condBranch.SetValue(fmt.Sprintf("%s Condition", utils.Success)) + target, targetName = i.then, "Then" + } else { + condBranch.SetValue(fmt.Sprintf("%s Condition", utils.Fail)) + target, targetName = i.els, "Else" + } + + targBranch := ifBranch.AddBranch("") + if target == nil || target.IsSatisfied(pr, targBranch) { + ifBranch.SetValue(fmt.Sprintf("%s If", utils.Success)) + targBranch.SetValue(fmt.Sprintf("%s %s", utils.Success, targetName)) + return true + } else { + ifBranch.SetValue(fmt.Sprintf("%s If", utils.Fail)) + targBranch.SetValue(fmt.Sprintf("%s %s", utils.Fail, targetName)) + return false + } +} + +// If returns a conditional requirement, which runs Then if cond evaluates +// successfully, or Else otherwise. +// +// Then / Else are optional, and always evaluate to true by default. +func If(cond Requirement) *IfCondition { + return &IfCondition{cond: cond} +} + +func (i *IfCondition) Then(then Requirement) *IfCondition { + if i.then != nil { + panic("'Then' is already set") + } + i.then = then + return i +} + +func (i *IfCondition) Else(els Requirement) *IfCondition { + if i.els != nil { + panic("'Else' is already set") + } + i.els = els + return i +} diff --git a/contribs/github-bot/internal/requirements/boolean_test.go b/contribs/github-bot/internal/requirements/boolean_test.go index 0043a44985c..dfa829ede61 100644 --- a/contribs/github-bot/internal/requirements/boolean_test.go +++ b/contribs/github-bot/internal/requirements/boolean_test.go @@ -94,3 +94,80 @@ func TestNot(t *testing.T) { }) } } + +func TestIfCond(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + req Requirement + isSatisfied bool + }{ + {"if always", If(Always()), true}, + {"if never", If(Never()), true}, + {"if always then always", If(Always()).Then(Always()), true}, + {"if never else always", If(Never()).Else(Always()), true}, + {"if always then never", If(Always()).Then(Never()), false}, + {"if never else never", If(Never()).Else(Never()), false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{} + details := treeprint.New() + + actual := testCase.req.IsSatisfied(pr, details) + assert.Equal(t, testCase.isSatisfied, actual, + "requirement should have a satisfied status: %t", testCase.isSatisfied) + assert.True(t, + utils.TestNodeStatus(t, testCase.isSatisfied, details.(*treeprint.Node).Nodes[0]), + "requirement details should have a status: %t", testCase.isSatisfied) + }) + } +} + +type reqFunc func(*github.PullRequest, treeprint.Tree) bool + +func (r reqFunc) IsSatisfied(gh *github.PullRequest, details treeprint.Tree) bool { + return r(gh, details) +} + +func TestIfCond_ConditionalExecution(t *testing.T) { + t.Run("executeThen", func(t *testing.T) { + thenExec, elseExec := 0, 0 + If(Always()). + Then(reqFunc(func(*github.PullRequest, treeprint.Tree) bool { + thenExec++ + return true + })). + Else(reqFunc(func(*github.PullRequest, treeprint.Tree) bool { + elseExec++ + return true + })).IsSatisfied(nil, treeprint.New()) + assert.Equal(t, 1, thenExec, "Then should be executed 1 time") + assert.Equal(t, 0, elseExec, "Else should be executed 0 time") + }) + t.Run("executeElse", func(t *testing.T) { + thenExec, elseExec := 0, 0 + If(Never()). + Then(reqFunc(func(*github.PullRequest, treeprint.Tree) bool { + thenExec++ + return true + })). + Else(reqFunc(func(*github.PullRequest, treeprint.Tree) bool { + elseExec++ + return true + })).IsSatisfied(nil, treeprint.New()) + assert.Equal(t, 0, thenExec, "Then should be executed 0 time") + assert.Equal(t, 1, elseExec, "Else should be executed 1 time") + }) +} + +func TestIfCond_NoRepeats(t *testing.T) { + assert.Panics(t, func() { + If(Always()).Then(Always()).Then(Always()) + }, "two Then should panic") + assert.Panics(t, func() { + If(Always()).Else(Always()).Else(Always()) + }, "two Else should panic") +} diff --git a/contribs/github-bot/internal/requirements/draft.go b/contribs/github-bot/internal/requirements/draft.go new file mode 100644 index 00000000000..675ffa02090 --- /dev/null +++ b/contribs/github-bot/internal/requirements/draft.go @@ -0,0 +1,21 @@ +package requirements + +import ( + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +// Draft Condition. +type draft struct{} + +var _ Requirement = &draft{} + +func (*draft) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { + return utils.AddStatusNode(pr.GetDraft(), "This pull request is a draft", details) +} + +func Draft() Requirement { + return &draft{} +} diff --git a/contribs/github-bot/internal/requirements/draft_test.go b/contribs/github-bot/internal/requirements/draft_test.go new file mode 100644 index 00000000000..ded4917b808 --- /dev/null +++ b/contribs/github-bot/internal/requirements/draft_test.go @@ -0,0 +1,34 @@ +package requirements + +import ( + "fmt" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/google/go-github/v64/github" + "github.com/stretchr/testify/assert" + "github.com/xlab/treeprint" +) + +func TestDraft(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + isMet bool + }{ + {"draft is true", true}, + {"draft is false", false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{Draft: &testCase.isMet} + details := treeprint.New() + req := Draft() + + assert.Equal(t, req.IsSatisfied(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet)) + }) + } +} diff --git a/contribs/github-bot/internal/requirements/label.go b/contribs/github-bot/internal/requirements/label.go index d1ee475db92..783c7a94ae8 100644 --- a/contribs/github-bot/internal/requirements/label.go +++ b/contribs/github-bot/internal/requirements/label.go @@ -10,10 +10,23 @@ import ( "github.com/xlab/treeprint" ) +// LabelAction controls what to do with the given label. +type LabelAction byte + +const ( + // LabelApply will place the label on the PR if it doesn't exist. + LabelApply = iota + // LabelRemove will remove the label from the PR if it exists. + LabelRemove + // LabelIgnore always leaves the label on the PR as-is, without modifying it. + LabelIgnore +) + // Label Requirement. type label struct { - gh *client.GitHub - name string + gh *client.GitHub + name string + action LabelAction } var _ Requirement = &label{} @@ -21,33 +34,58 @@ var _ Requirement = &label{} func (l *label) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { detail := fmt.Sprintf("This label is applied to pull request: %s", l.name) + found := false // Check if label was already applied to PR. for _, label := range pr.Labels { if l.name == label.GetName() { - return utils.AddStatusNode(true, detail, details) + found = true + break } } - // If in a dry run, skip applying the label. - if l.gh.DryRun { - return utils.AddStatusNode(false, detail, details) + // If in a dry run, or no action expected, skip applying the label. + if l.gh.DryRun || + l.action == LabelIgnore || + (l.action == LabelApply && found) || + (l.action == LabelRemove && !found) { + return utils.AddStatusNode(found, detail, details) } - // If label not already applied, apply it. - if _, _, err := l.gh.Client.Issues.AddLabelsToIssue( - l.gh.Ctx, - l.gh.Owner, - l.gh.Repo, - pr.GetNumber(), - []string{l.name}, - ); err != nil { - l.gh.Logger.Errorf("Unable to add label %s to PR %d: %v", l.name, pr.GetNumber(), err) + switch l.action { + case LabelApply: + // If label not already applied, apply it. + if _, _, err := l.gh.Client.Issues.AddLabelsToIssue( + l.gh.Ctx, + l.gh.Owner, + l.gh.Repo, + pr.GetNumber(), + []string{l.name}, + ); err != nil { + l.gh.Logger.Errorf("Unable to add label %s to PR %d: %v", l.name, pr.GetNumber(), err) + return utils.AddStatusNode(false, detail, details) + } + return utils.AddStatusNode(true, detail, details) + case LabelRemove: + // If label not already applied, apply it. + if _, err := l.gh.Client.Issues.RemoveLabelForIssue( + l.gh.Ctx, + l.gh.Owner, + l.gh.Repo, + pr.GetNumber(), + l.name, + ); err != nil { + l.gh.Logger.Errorf("Unable to remove label %s from PR %d: %v", l.name, pr.GetNumber(), err) + return utils.AddStatusNode(true, detail, details) + } return utils.AddStatusNode(false, detail, details) + default: + panic(fmt.Sprintf("invalid LabelAction value: %d", l.action)) } - - return utils.AddStatusNode(true, detail, details) } -func Label(gh *client.GitHub, name string) Requirement { - return &label{gh, name} +// Label asserts that the label with the given name is not applied to the PR. +// +// If it's not a dry run, the label will be applied to the PR. +func Label(gh *client.GitHub, name string, action LabelAction) Requirement { + return &label{gh, name, action} } diff --git a/contribs/github-bot/internal/requirements/label_test.go b/contribs/github-bot/internal/requirements/label_test.go index 631bff9e64b..e59fa821dc3 100644 --- a/contribs/github-bot/internal/requirements/label_test.go +++ b/contribs/github-bot/internal/requirements/label_test.go @@ -25,11 +25,11 @@ func TestLabel(t *testing.T) { } for _, testCase := range []struct { - name string - pattern string - labels []*github.Label - dryRun bool - exists bool + name string + labelName string + prLabels []*github.Label + dryRun bool + exists bool }{ {"empty label list", "label", []*github.Label{}, false, false}, {"empty label list with dry-run", "user", []*github.Label{}, true, false}, @@ -60,9 +60,9 @@ func TestLabel(t *testing.T) { DryRun: testCase.dryRun, } - pr := &github.PullRequest{Labels: testCase.labels} + pr := &github.PullRequest{Labels: testCase.prLabels} details := treeprint.New() - requirement := Label(gh, testCase.pattern) + requirement := Label(gh, testCase.labelName, LabelApply) assert.True(t, requirement.IsSatisfied(pr, details) || testCase.dryRun, "requirement should have a satisfied status: true") assert.True(t, utils.TestLastNodeStatus(t, true, details) || testCase.dryRun, "requirement details should have a status: true") @@ -70,3 +70,60 @@ func TestLabel(t *testing.T) { }) } } + +func TestLabel_LabelRemove(t *testing.T) { + t.Parallel() + + labels := []*github.Label{ + {Name: github.String("notTheRightOne")}, + {Name: github.String("label")}, + {Name: github.String("anotherOne")}, + } + + for _, testCase := range []struct { + name string + labelName string + prLabels []*github.Label + dryRun bool + shouldRequest bool + result bool + }{ + {"empty label list", "label", []*github.Label{}, false, false, false}, + {"empty label list with dry-run", "label", []*github.Label{}, true, false, false}, + {"label list contains label", "label", labels, false, true, false}, + {"label list contains label with dry-run", "label", labels, true, false, true}, + {"label list doesn't contain label", "label2", labels, false, false, false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + requested := false + mockedHTTPClient := mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{ + Pattern: "/repos/issues/0/labels/label", + Method: "GET", + }, + http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { + requested = true + }), + ), + ) + + gh := &client.GitHub{ + Client: github.NewClient(mockedHTTPClient), + Ctx: context.Background(), + Logger: logger.NewNoopLogger(), + DryRun: testCase.dryRun, + } + + pr := &github.PullRequest{Labels: testCase.prLabels} + details := treeprint.New() + requirement := Label(gh, testCase.labelName, LabelRemove) + + assert.Equal(t, testCase.result, requirement.IsSatisfied(pr, details), "result of IsSatisfied should match expectation") + assert.True(t, utils.TestLastNodeStatus(t, testCase.result, details), "requirement details should have a status: %t", testCase.result) + assert.Equal(t, testCase.shouldRequest, requested, "IsSatisfied should have requested to delete item") + }) + } +} diff --git a/contribs/github-bot/internal/requirements/reviewer.go b/contribs/github-bot/internal/requirements/reviewer.go index aa3914d4c4a..3c0d4f98510 100644 --- a/contribs/github-bot/internal/requirements/reviewer.go +++ b/contribs/github-bot/internal/requirements/reviewer.go @@ -16,6 +16,8 @@ type reviewByUser struct { user string } +const approvedState = "APPROVED" + var _ Requirement = &reviewByUser{} func (r *reviewByUser) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { @@ -65,7 +67,7 @@ func (r *reviewByUser) IsSatisfied(pr *github.PullRequest, details treeprint.Tre for _, review := range reviews { if review.GetUser().GetLogin() == r.user { r.gh.Logger.Debugf("User %s already reviewed PR %d with state %s", r.user, pr.GetNumber(), review.GetState()) - return utils.AddStatusNode(review.GetState() == "APPROVED", detail, details) + return utils.AddStatusNode(review.GetState() == approvedState, detail, details) } } r.gh.Logger.Debugf("User %s has not reviewed PR %d yet", r.user, pr.GetNumber()) @@ -131,16 +133,16 @@ func (r *reviewByTeamMembers) IsSatisfied(pr *github.PullRequest, details treepr return utils.AddStatusNode(false, detail, details) } - for _, review := range reviews { - teamMembers, err := r.gh.ListTeamMembers(r.team) - if err != nil { - r.gh.Logger.Errorf(err.Error()) - continue - } + teamMembers, err := r.gh.ListTeamMembers(r.team) + if err != nil { + r.gh.Logger.Errorf(err.Error()) + return utils.AddStatusNode(false, detail, details) + } + for _, review := range reviews { for _, member := range teamMembers { if review.GetUser().GetLogin() == member.GetLogin() { - if review.GetState() == "APPROVED" { + if review.GetState() == approvedState { approved += 1 } r.gh.Logger.Debugf("Member %s from team %s already reviewed PR %d with state %s (%d/%d required approval(s))", member.GetLogin(), r.team, pr.GetNumber(), review.GetState(), approved, r.count) @@ -154,3 +156,41 @@ func (r *reviewByTeamMembers) IsSatisfied(pr *github.PullRequest, details treepr func ReviewByTeamMembers(gh *client.GitHub, team string, count uint) Requirement { return &reviewByTeamMembers{gh, team, count} } + +type reviewByOrgMembers struct { + gh *client.GitHub + count uint +} + +var _ Requirement = &reviewByOrgMembers{} + +func (r *reviewByOrgMembers) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { + detail := fmt.Sprintf("At least %d user(s) of the organization approved the pull request", r.count) + + // Check how many members of this team already approved this PR. + approved := uint(0) + reviews, err := r.gh.ListPRReviews(pr.GetNumber()) + if err != nil { + r.gh.Logger.Errorf("unable to check number of reviews on this PR: %v", err) + return utils.AddStatusNode(false, detail, details) + } + + for _, review := range reviews { + if review.GetAuthorAssociation() == "MEMBER" { + if review.GetState() == approvedState { + approved++ + } + r.gh.Logger.Debugf( + "Member %s already reviewed PR %d with state %s (%d/%d required approval(s))", + review.GetUser().GetLogin(), pr.GetNumber(), review.GetState(), + approved, r.count, + ) + } + } + + return utils.AddStatusNode(approved >= r.count, detail, details) +} + +func ReviewByOrgMembers(gh *client.GitHub, count uint) Requirement { + return &reviewByOrgMembers{gh, count} +} diff --git a/contribs/github-bot/internal/requirements/reviewer_test.go b/contribs/github-bot/internal/requirements/reviewer_test.go index 16c50e13743..98f68384d8f 100644 --- a/contribs/github-bot/internal/requirements/reviewer_test.go +++ b/contribs/github-bot/internal/requirements/reviewer_test.go @@ -213,3 +213,68 @@ func TestReviewByTeamMembers(t *testing.T) { }) } } + +func TestReviewByOrgMembers(t *testing.T) { + t.Parallel() + + reviews := []*github.PullRequestReview{ + { + User: &github.User{Login: github.String("user1")}, + State: github.String("APPROVED"), + AuthorAssociation: github.String("MEMBER"), + }, { + User: &github.User{Login: github.String("user2")}, + State: github.String("APPROVED"), + AuthorAssociation: github.String("COLLABORATOR"), + }, { + User: &github.User{Login: github.String("user3")}, + State: github.String("APPROVED"), + AuthorAssociation: github.String("MEMBER"), + }, { + User: &github.User{Login: github.String("user4")}, + State: github.String("REQUEST_CHANGES"), + AuthorAssociation: github.String("MEMBER"), + }, { + User: &github.User{Login: github.String("user5")}, + State: github.String("REQUEST_CHANGES"), + AuthorAssociation: github.String("NONE"), + }, + } + + for _, testCase := range []struct { + name string + count uint + isSatisfied bool + }{ + {"2/3 org members approved", 3, false}, + {"2/2 org members approved", 2, true}, + {"2/1 org members approved", 1, true}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + mockedHTTPClient := mock.NewMockedHTTPClient( + mock.WithRequestMatchPages( + mock.EndpointPattern{ + Pattern: "/repos/pulls/0/reviews", + Method: "GET", + }, + reviews, + ), + ) + + gh := &client.GitHub{ + Client: github.NewClient(mockedHTTPClient), + Ctx: context.Background(), + Logger: logger.NewNoopLogger(), + } + + pr := &github.PullRequest{} + details := treeprint.New() + requirement := ReviewByOrgMembers(gh, testCase.count) + + assert.Equal(t, requirement.IsSatisfied(pr, details), testCase.isSatisfied, fmt.Sprintf("requirement should have a satisfied status: %t", testCase.isSatisfied)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isSatisfied, details), fmt.Sprintf("requirement details should have a status: %t", testCase.isSatisfied)) + }) + } +} diff --git a/contribs/github-bot/internal/utils/testing.go b/contribs/github-bot/internal/utils/testing.go index 3c7f7bfef88..839bde831dd 100644 --- a/contribs/github-bot/internal/utils/testing.go +++ b/contribs/github-bot/internal/utils/testing.go @@ -9,8 +9,13 @@ import ( func TestLastNodeStatus(t *testing.T, success bool, details treeprint.Tree) bool { t.Helper() + return TestNodeStatus(t, success, details.FindLastNode()) +} + +func TestNodeStatus(t *testing.T, success bool, details treeprint.Tree) bool { + t.Helper() - detail := details.FindLastNode().(*treeprint.Node).Value.(string) + detail := details.(*treeprint.Node).Value.(string) status := Fail if success {