diff --git a/server/plugin/command.go b/server/plugin/command.go index 9c4d273a0..9c0de4294 100644 --- a/server/plugin/command.go +++ b/server/plugin/command.go @@ -35,6 +35,8 @@ const ( featureReleases = "releases" featureWorkflowFailure = "workflow_failure" featureWorkflowSuccess = "workflow_success" + featureWorkflowRunFailure = "workflow_run_failure" + featureWorkflowRunSuccess = "workflow_run_success" featureDiscussions = "discussions" featureDiscussionComments = "discussion_comments" ) @@ -62,6 +64,8 @@ var validFeatures = map[string]bool{ featureReleases: true, featureWorkflowFailure: true, featureWorkflowSuccess: true, + featureWorkflowRunFailure: true, + featureWorkflowRunSuccess: true, featureDiscussions: true, featureDiscussionComments: true, } @@ -1137,7 +1141,7 @@ func getAutocompleteData(config *Configuration) *model.AutocompleteData { subscriptionsAdd := model.NewAutocompleteData("add", "[owner/repo] [features] [flags]", "Subscribe the current channel to receive notifications about opened pull requests and issues for an organization or repository. [features] and [flags] are optional arguments") subscriptionsAdd.AddTextArgument("Owner/repo to subscribe to", "[owner/repo]", "") - subscriptionsAdd.AddNamedTextArgument("features", "Comma-delimited list of one or more of: issues, pulls, pulls_merged, pulls_created, pushes, creates, deletes, issue_creations, issue_comments, pull_reviews, releases, workflow_success, workflow_failure, discussions, discussion_comments, label:\"\". Defaults to pulls,issues,creates,deletes", "", `/[^,-\s]+(,[^,-\s]+)*/`, false) + subscriptionsAdd.AddNamedTextArgument("features", "Comma-delimited list of one or more of: issues, pulls, pulls_merged, pulls_created, pushes, creates, deletes, issue_creations, issue_comments, pull_reviews, releases, workflow_success, workflow_failure, workflow_run_failure, workflow_run_success, discussions, discussion_comments, label:\"\". Defaults to pulls,issues,creates,deletes", "", `/[^,-\s]+(,[^,-\s]+)*/`, false) if config.GitHubOrg != "" { subscriptionsAdd.AddNamedStaticListArgument("exclude-org-member", "Events triggered by organization members will not be delivered (the organization config should be set, otherwise this flag has not effect)", false, []model.AutocompleteListItem{ diff --git a/server/plugin/subscriptions.go b/server/plugin/subscriptions.go index 2f9f10a9d..5ae6dc592 100644 --- a/server/plugin/subscriptions.go +++ b/server/plugin/subscriptions.go @@ -147,6 +147,14 @@ func (s *Subscription) Workflows() bool { return strings.Contains(s.Features.String(), featureWorkflowFailure) || strings.Contains(s.Features.String(), featureWorkflowSuccess) } +func (s *Subscription) WorkflowRunFailures() bool { + return strings.Contains(s.Features.String(), featureWorkflowRunFailure) +} + +func (s *Subscription) WorkflowRunSuccesses() bool { + return strings.Contains(s.Features.String(), featureWorkflowRunSuccess) +} + func (s *Subscription) Release() bool { return strings.Contains(s.Features.String(), featureReleases) } diff --git a/server/plugin/template.go b/server/plugin/template.go index 33e43840a..e9dc3f8e1 100644 --- a/server/plugin/template.go +++ b/server/plugin/template.go @@ -120,7 +120,7 @@ func init() { funcMap["workflowJobFailedStep"] = func(steps []*github.TaskStep) string { for _, step := range steps { - if step.GetConclusion() == workflowJobFail { + if step.GetConclusion() == workflowConclusionFailure { return step.GetName() } } @@ -453,6 +453,8 @@ Reviewers: {{range $i, $el := .RequestedReviewers -}} {{- if $i}}, {{end}}{{temp " * `pull_reviews` - includes pull request reviews\n" + " * `workflow_failure` - includes workflow job failure\n" + " * `workflow_success` - includes workflow job success\n" + + " * `workflow_run_failure` - includes workflow run failures (failures, cancellations, timeouts)\n" + + " * `workflow_run_success` - includes workflow run successes\n" + " * `releases` - includes release created and deleted\n" + " * `label:` - limit pull request and issue events to only this label. Must include `pulls` or `issues` in feature list when using a label.\n" + " * `discussions` - includes new discussions\n" + @@ -489,6 +491,12 @@ It now has **{{.GetRepo.GetStargazersCount}}** stars.`)) {{if eq .GetWorkflowJob.GetConclusion "failure"}}Job failed: {{template "workflowJob" .GetWorkflowJob}} Step failed: {{.GetWorkflowJob.Steps | workflowJobFailedStep}} {{end}}Commit: {{.GetRepo.GetHTMLURL}}/commit/{{.GetWorkflowJob.GetHeadSHA}}`)) + + template.Must(masterTemplate.New("workflowRunCompleted").Funcs(funcMap).Parse(` +{{template "repo" .GetRepo}} Workflow [{{.GetWorkflow.GetName}}]({{.GetWorkflowRun.GetHTMLURL}}) {{if eq .GetWorkflowRun.GetConclusion "success"}}succeeded :white_check_mark:{{else if eq .GetWorkflowRun.GetConclusion "failure"}}failed :x:{{else if eq .GetWorkflowRun.GetConclusion "cancelled"}}was cancelled :no_entry_sign:{{else if eq .GetWorkflowRun.GetConclusion "timed_out"}}timed out :warning:{{else}}completed with conclusion: {{.GetWorkflowRun.GetConclusion}}{{end}} +Branch: ` + "`" + `{{.GetWorkflowRun.GetHeadBranch}}` + "`" + ` | Run [#{{.GetWorkflowRun.GetRunNumber}}]({{.GetWorkflowRun.GetHTMLURL}}) | Triggered by {{template "user" .GetSender}} +Commit: {{.GetRepo.GetHTMLURL}}/commit/{{.GetWorkflowRun.GetHeadSHA}}`)) + template.Must(masterTemplate.New("newReleaseEvent").Funcs(funcMap).Parse(` {{template "repo" .GetRepo}} {{template "user" .GetSender}} {{- if eq .GetAction "created" }} created a release {{template "release" .GetRelease}} @@ -505,8 +513,8 @@ Step failed: {{.GetWorkflowJob.Steps | workflowJobFailedStep}} `)) template.Must(masterTemplate.New("newDiscussionComment").Funcs(funcMap).Parse(` -{{template "repo" .GetRepo}} -{{- if eq .GetAction "created" }} New comment +{{template "repo" .GetRepo}} +{{- if eq .GetAction "created" }} New comment {{- else if eq .GetAction "edited" }} Comment edited {{- else if eq .GetAction "deleted" }} Comment deleted {{- end }} by {{template "user" .GetSender}} on discussion [#{{.GetDiscussion.GetNumber}} {{.GetDiscussion.GetTitle}}]({{.GetDiscussion.GetHTMLURL}}): diff --git a/server/plugin/template_test.go b/server/plugin/template_test.go index ec6118590..8972a0e0b 100644 --- a/server/plugin/template_test.go +++ b/server/plugin/template_test.go @@ -1578,6 +1578,108 @@ Commit: https://github.com/mattermost/mattermost-plugin-github/commit/1234567890 }) } +func TestWorkflowRunNotification(t *testing.T) { + t.Run("failed", func(t *testing.T) { + expected := ` +[\[mattermost-plugin-github\]](https://github.com/mattermost/mattermost-plugin-github) Workflow [CI Pipeline](https://github.com/mattermost/mattermost-plugin-github/actions/runs/99999) failed :x: +Branch: ` + "`main`" + ` | Run [#42](https://github.com/mattermost/mattermost-plugin-github/actions/runs/99999) | Triggered by [panda](https://github.com/panda) +Commit: https://github.com/mattermost/mattermost-plugin-github/commit/abc1234567` + + actual, err := renderTemplate("workflowRunCompleted", &github.WorkflowRunEvent{ + Repo: &repo, + Sender: &user, + Action: sToP(actionCompleted), + Workflow: &github.Workflow{ + Name: sToP("CI Pipeline"), + }, + WorkflowRun: &github.WorkflowRun{ + Conclusion: sToP("failure"), + HeadBranch: sToP("main"), + HeadSHA: sToP("abc1234567"), + HTMLURL: sToP("https://github.com/mattermost/mattermost-plugin-github/actions/runs/99999"), + RunNumber: iToP(42), + }, + }) + require.NoError(t, err) + require.Equal(t, expected, actual) + }) + + t.Run("success", func(t *testing.T) { + expected := ` +[\[mattermost-plugin-github\]](https://github.com/mattermost/mattermost-plugin-github) Workflow [CI Pipeline](https://github.com/mattermost/mattermost-plugin-github/actions/runs/99999) succeeded :white_check_mark: +Branch: ` + "`main`" + ` | Run [#42](https://github.com/mattermost/mattermost-plugin-github/actions/runs/99999) | Triggered by [panda](https://github.com/panda) +Commit: https://github.com/mattermost/mattermost-plugin-github/commit/abc1234567` + + actual, err := renderTemplate("workflowRunCompleted", &github.WorkflowRunEvent{ + Repo: &repo, + Sender: &user, + Action: sToP(actionCompleted), + Workflow: &github.Workflow{ + Name: sToP("CI Pipeline"), + }, + WorkflowRun: &github.WorkflowRun{ + Conclusion: sToP("success"), + HeadBranch: sToP("main"), + HeadSHA: sToP("abc1234567"), + HTMLURL: sToP("https://github.com/mattermost/mattermost-plugin-github/actions/runs/99999"), + RunNumber: iToP(42), + }, + }) + require.NoError(t, err) + require.Equal(t, expected, actual) + }) + + t.Run("cancelled", func(t *testing.T) { + expected := ` +[\[mattermost-plugin-github\]](https://github.com/mattermost/mattermost-plugin-github) Workflow [CI Pipeline](https://github.com/mattermost/mattermost-plugin-github/actions/runs/99999) was cancelled :no_entry_sign: +Branch: ` + "`main`" + ` | Run [#42](https://github.com/mattermost/mattermost-plugin-github/actions/runs/99999) | Triggered by [panda](https://github.com/panda) +Commit: https://github.com/mattermost/mattermost-plugin-github/commit/abc1234567` + + actual, err := renderTemplate("workflowRunCompleted", &github.WorkflowRunEvent{ + Repo: &repo, + Sender: &user, + Action: sToP(actionCompleted), + Workflow: &github.Workflow{ + Name: sToP("CI Pipeline"), + }, + WorkflowRun: &github.WorkflowRun{ + Conclusion: sToP("cancelled"), + HeadBranch: sToP("main"), + HeadSHA: sToP("abc1234567"), + HTMLURL: sToP("https://github.com/mattermost/mattermost-plugin-github/actions/runs/99999"), + RunNumber: iToP(42), + }, + }) + require.NoError(t, err) + require.Equal(t, expected, actual) + }) + + t.Run("timed_out", func(t *testing.T) { + expected := ` +[\[mattermost-plugin-github\]](https://github.com/mattermost/mattermost-plugin-github) Workflow [CI Pipeline](https://github.com/mattermost/mattermost-plugin-github/actions/runs/99999) timed out :warning: +Branch: ` + "`main`" + ` | Run [#42](https://github.com/mattermost/mattermost-plugin-github/actions/runs/99999) | Triggered by [panda](https://github.com/panda) +Commit: https://github.com/mattermost/mattermost-plugin-github/commit/abc1234567` + + actual, err := renderTemplate("workflowRunCompleted", &github.WorkflowRunEvent{ + Repo: &repo, + Sender: &user, + Action: sToP(actionCompleted), + Workflow: &github.Workflow{ + Name: sToP("CI Pipeline"), + }, + WorkflowRun: &github.WorkflowRun{ + Conclusion: sToP("timed_out"), + HeadBranch: sToP("main"), + HeadSHA: sToP("abc1234567"), + HTMLURL: sToP("https://github.com/mattermost/mattermost-plugin-github/actions/runs/99999"), + RunNumber: iToP(42), + }, + }) + require.NoError(t, err) + require.Equal(t, expected, actual) + }) +} + func sToP(s string) *string { return &s } diff --git a/server/plugin/test_utils.go b/server/plugin/test_utils.go index cc9978e4a..49c1f6242 100644 --- a/server/plugin/test_utils.go +++ b/server/plugin/test_utils.go @@ -427,6 +427,29 @@ func GetMockDiscussionEvent(repo, org, sender string) *github.DiscussionEvent { } } +func GetMockWorkflowRunEvent(action, conclusion, repo, org, sender string) *github.WorkflowRunEvent { + return &github.WorkflowRunEvent{ + Action: github.String(action), + Repo: &github.Repository{ + Name: github.String(repo), + Owner: &github.User{Login: github.String(org)}, + FullName: github.String(fmt.Sprintf("%s/%s", org, repo)), + HTMLURL: github.String(fmt.Sprintf("%s%s/%s", GithubBaseURL, org, repo)), + }, + Sender: &github.User{Login: github.String(sender)}, + Workflow: &github.Workflow{ + Name: github.String("CI Pipeline"), + }, + WorkflowRun: &github.WorkflowRun{ + Conclusion: github.String(conclusion), + HeadBranch: github.String("main"), + HeadSHA: github.String("abc1234567"), + HTMLURL: github.String(fmt.Sprintf("%s%s/%s/actions/runs/99999", GithubBaseURL, org, repo)), + RunNumber: github.Int(42), + }, + } +} + func GetMockDiscussionCommentEvent(repo, org, action, sender string) *github.DiscussionCommentEvent { return &github.DiscussionCommentEvent{ Action: &action, diff --git a/server/plugin/webhook.go b/server/plugin/webhook.go index 90d62ac4d..5b4fed0ac 100644 --- a/server/plugin/webhook.go +++ b/server/plugin/webhook.go @@ -38,8 +38,10 @@ const ( actionEdited = "edited" actionCompleted = "completed" - workflowJobFail = "failure" - workflowJobSuccess = "success" + workflowConclusionFailure = "failure" + workflowConclusionSuccess = "success" + workflowConclusionCancelled = "cancelled" + workflowConclusionTimedOut = "timed_out" postPropGithubRepo = "gh_repo" postPropGithubObjectID = "gh_object_id" @@ -301,6 +303,11 @@ func (p *Plugin) handleWebhook(w http.ResponseWriter, r *http.Request) { handler = func() { p.postWorkflowJobEvent(event) } + case *github.WorkflowRunEvent: + repo = event.GetRepo() + handler = func() { + p.postWorkflowRunEvent(event) + } case *github.ReleaseEvent: repo = event.GetRepo() handler = func() { @@ -1438,8 +1445,7 @@ func (p *Plugin) postWorkflowJobEvent(event *github.WorkflowJobEvent) { return } - // Create a post only when the workflow job is completed and has either failed or succeeded - if event.GetWorkflowJob().GetConclusion() != workflowJobFail && event.GetWorkflowJob().GetConclusion() != workflowJobSuccess { + if event.GetWorkflowJob().GetConclusion() != workflowConclusionFailure && event.GetWorkflowJob().GetConclusion() != workflowConclusionSuccess { return } @@ -1474,6 +1480,59 @@ func (p *Plugin) postWorkflowJobEvent(event *github.WorkflowJobEvent) { } } +func (p *Plugin) postWorkflowRunEvent(event *github.WorkflowRunEvent) { + if event.GetAction() != actionCompleted { + return + } + + conclusion := event.GetWorkflowRun().GetConclusion() + isSuccess := conclusion == workflowConclusionSuccess + isFailure := conclusion == workflowConclusionFailure || + conclusion == workflowConclusionCancelled || + conclusion == workflowConclusionTimedOut + + if !isSuccess && !isFailure { + return + } + + repo := event.GetRepo() + subs := p.GetSubscribedChannelsForRepository(repo) + if len(subs) == 0 { + return + } + + workflowRunMessage, err := renderTemplate("workflowRunCompleted", event) + if err != nil { + p.client.Log.Warn("Failed to render template", "Error", err.Error()) + return + } + + for _, sub := range subs { + if (isFailure && !sub.WorkflowRunFailures()) || (isSuccess && !sub.WorkflowRunSuccesses()) { + continue + } + + if p.excludeConfigOrgMember(event.GetSender(), sub) { + continue + } + + if p.shouldDenyEventDueToNotOrgMember(event.GetSender(), sub) { + continue + } + + post := &model.Post{ + UserId: p.BotUserID, + Type: "custom_git_workflow_run", + Message: workflowRunMessage, + ChannelId: sub.ChannelID, + } + + if err = p.client.Post.CreatePost(post); err != nil { + p.client.Log.Warn("Error webhook post", "Post", post, "Error", err.Error()) + } + } +} + func (p *Plugin) makeBotPost(message, postType string) *model.Post { return &model.Post{ UserId: p.BotUserID, diff --git a/server/plugin/webhook_test.go b/server/plugin/webhook_test.go index 1f3ef60a2..03ada648d 100644 --- a/server/plugin/webhook_test.go +++ b/server/plugin/webhook_test.go @@ -1197,6 +1197,204 @@ func TestPostDiscussionEvent(t *testing.T) { } } +func TestPostWorkflowRunEvent(t *testing.T) { + mockKvStore, mockAPI, _, _, _ := GetTestSetup(t) + p := getPluginTest(mockAPI, mockKvStore) + + tests := []struct { + name string + event *github.WorkflowRunEvent + setup func() + }{ + { + name: "action is not completed, event ignored", + event: GetMockWorkflowRunEvent("in_progress", "success", MockRepo, MockOrg, MockSender), + setup: func() {}, + }, + { + name: "unsupported conclusion, event ignored", + event: GetMockWorkflowRunEvent(actionCompleted, "skipped", MockRepo, MockOrg, MockSender), + setup: func() {}, + }, + { + name: "no subscribed channels for repository", + event: GetMockWorkflowRunEvent(actionCompleted, "failure", MockRepo, MockOrg, MockSender), + setup: func() { + mockKvStore.EXPECT().Get("subscriptions", mock.MatchedBy(func(val any) bool { + _, ok := val.(**Subscriptions) + return ok + })).Return(nil).Times(1) + }, + }, + { + name: "subscription does not include workflow_run_failure feature", + event: GetMockWorkflowRunEvent(actionCompleted, "failure", MockRepo, MockOrg, MockSender), + setup: func() { + mockKvStore.EXPECT().Get("subscriptions", mock.MatchedBy(func(val any) bool { + _, ok := val.(**Subscriptions) + return ok + })).DoAndReturn(setupMockSubscriptions(map[string][]*Subscription{ + "mockorg/mockrepo": { + {ChannelID: MockChannelID, CreatorID: MockCreatorID, Features: featureStars, Repository: MockRepo}, + }, + })).Times(1) + }, + }, + { + name: "subscription does not include workflow_run_success feature", + event: GetMockWorkflowRunEvent(actionCompleted, "success", MockRepo, MockOrg, MockSender), + setup: func() { + mockKvStore.EXPECT().Get("subscriptions", mock.MatchedBy(func(val any) bool { + _, ok := val.(**Subscriptions) + return ok + })).DoAndReturn(setupMockSubscriptions(map[string][]*Subscription{ + "mockorg/mockrepo": { + {ChannelID: MockChannelID, CreatorID: MockCreatorID, Features: Features(featureWorkflowRunFailure), Repository: MockRepo}, + }, + })).Times(1) + }, + }, + { + name: "excluded org member skips subscription", + event: GetMockWorkflowRunEvent(actionCompleted, "failure", MockRepo, MockOrg, MockSender), + setup: func() { + mockKvStore.EXPECT().Get("subscriptions", mock.MatchedBy(func(val any) bool { + _, ok := val.(**Subscriptions) + return ok + })).DoAndReturn(setupMockSubscriptions(map[string][]*Subscription{ + "mockorg/mockrepo": { + { + ChannelID: MockChannelID, + CreatorID: MockCreatorID, + Features: Features(featureWorkflowRunFailure), + Repository: MockRepo, + Flags: SubscriptionFlags{ExcludeOrgMembers: true}, + }, + }, + })).Times(1) + mockKvStore.EXPECT().Get(MockCreatorID+"_githubtoken", mock.MatchedBy(func(val any) bool { + _, ok := val.(**GitHubUserInfo) + return ok + })).Return(nil).Times(1) + mockAPI.On("LogWarn", "Failed to exclude org member", "error", mock.AnythingOfType("string")) + mockAPI.On("CreatePost", mock.Anything).Return(&model.Post{}, nil).Times(1) + }, + }, + { + name: "include only org members checks membership", + event: GetMockWorkflowRunEvent(actionCompleted, "failure", MockRepo, MockOrg, MockSender), + setup: func() { + mockKvStore.EXPECT().Get("subscriptions", mock.MatchedBy(func(val any) bool { + _, ok := val.(**Subscriptions) + return ok + })).DoAndReturn(setupMockSubscriptions(map[string][]*Subscription{ + "mockorg/mockrepo": { + { + ChannelID: MockChannelID, + CreatorID: MockCreatorID, + Features: Features(featureWorkflowRunFailure), + Repository: MockRepo, + Flags: SubscriptionFlags{IncludeOnlyOrgMembers: true}, + }, + }, + })).Times(1) + mockKvStore.EXPECT().Get(MockCreatorID+"_githubtoken", mock.MatchedBy(func(val any) bool { + _, ok := val.(**GitHubUserInfo) + return ok + })).Return(nil).Times(1) + mockAPI.On("LogWarn", "Failed to get user info", "error", mock.AnythingOfType("string")) + mockAPI.On("CreatePost", mock.Anything).Return(&model.Post{}, nil).Times(1) + }, + }, + { + name: "error creating post", + event: GetMockWorkflowRunEvent(actionCompleted, "failure", MockRepo, MockOrg, MockSender), + setup: func() { + mockKvStore.EXPECT().Get("subscriptions", mock.MatchedBy(func(val any) bool { + _, ok := val.(**Subscriptions) + return ok + })).DoAndReturn(setupMockSubscriptions(map[string][]*Subscription{ + "mockorg/mockrepo": { + {ChannelID: MockChannelID, CreatorID: MockCreatorID, Features: Features(featureWorkflowRunFailure), Repository: MockRepo}, + }, + })).Times(1) + mockAPI.On("CreatePost", mock.Anything).Return(nil, &model.AppError{Message: "error creating post"}).Times(1) + mockAPI.On("LogWarn", "Error webhook post", "Post", mock.Anything, "Error", "error creating post") + }, + }, + { + name: "successful workflow run failure notification", + event: GetMockWorkflowRunEvent(actionCompleted, "failure", MockRepo, MockOrg, MockSender), + setup: func() { + mockKvStore.EXPECT().Get("subscriptions", mock.MatchedBy(func(val any) bool { + _, ok := val.(**Subscriptions) + return ok + })).DoAndReturn(setupMockSubscriptions(map[string][]*Subscription{ + "mockorg/mockrepo": { + {ChannelID: MockChannelID, CreatorID: MockCreatorID, Features: Features(featureWorkflowRunFailure), Repository: MockRepo}, + }, + })).Times(1) + mockAPI.On("CreatePost", mock.Anything).Return(&model.Post{}, nil).Times(1) + }, + }, + { + name: "successful workflow run success notification", + event: GetMockWorkflowRunEvent(actionCompleted, "success", MockRepo, MockOrg, MockSender), + setup: func() { + mockKvStore.EXPECT().Get("subscriptions", mock.MatchedBy(func(val any) bool { + _, ok := val.(**Subscriptions) + return ok + })).DoAndReturn(setupMockSubscriptions(map[string][]*Subscription{ + "mockorg/mockrepo": { + {ChannelID: MockChannelID, CreatorID: MockCreatorID, Features: Features(featureWorkflowRunSuccess), Repository: MockRepo}, + }, + })).Times(1) + mockAPI.On("CreatePost", mock.Anything).Return(&model.Post{}, nil).Times(1) + }, + }, + { + name: "successful workflow run cancelled notification", + event: GetMockWorkflowRunEvent(actionCompleted, "cancelled", MockRepo, MockOrg, MockSender), + setup: func() { + mockKvStore.EXPECT().Get("subscriptions", mock.MatchedBy(func(val any) bool { + _, ok := val.(**Subscriptions) + return ok + })).DoAndReturn(setupMockSubscriptions(map[string][]*Subscription{ + "mockorg/mockrepo": { + {ChannelID: MockChannelID, CreatorID: MockCreatorID, Features: Features(featureWorkflowRunFailure), Repository: MockRepo}, + }, + })).Times(1) + mockAPI.On("CreatePost", mock.Anything).Return(&model.Post{}, nil).Times(1) + }, + }, + { + name: "successful workflow run timed_out notification", + event: GetMockWorkflowRunEvent(actionCompleted, "timed_out", MockRepo, MockOrg, MockSender), + setup: func() { + mockKvStore.EXPECT().Get("subscriptions", mock.MatchedBy(func(val any) bool { + _, ok := val.(**Subscriptions) + return ok + })).DoAndReturn(setupMockSubscriptions(map[string][]*Subscription{ + "mockorg/mockrepo": { + {ChannelID: MockChannelID, CreatorID: MockCreatorID, Features: Features(featureWorkflowRunFailure), Repository: MockRepo}, + }, + })).Times(1) + mockAPI.On("CreatePost", mock.Anything).Return(&model.Post{}, nil).Times(1) + }, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + mockAPI.ExpectedCalls = nil + tc.setup() + + p.postWorkflowRunEvent(tc.event) + + mockAPI.AssertExpectations(t) + }) + } +} + func TestPostDiscussionCommentEvent(t *testing.T) { mockKvStore, mockAPI, _, _, _ := GetTestSetup(t) p := getPluginTest(mockAPI, mockKvStore)