diff --git a/server/plugin/command.go b/server/plugin/command.go index 6f632583d..06e321a42 100644 --- a/server/plugin/command.go +++ b/server/plugin/command.go @@ -27,6 +27,8 @@ const ( featurePullReviews = "pull_reviews" featureStars = "stars" featureReleases = "releases" + featureWorkflowFailure = "workflow_failure" + featureWorkflowSuccess = "workflow_success" featureDiscussions = "discussions" featureDiscussionComments = "discussion_comments" ) @@ -48,6 +50,8 @@ var validFeatures = map[string]bool{ featurePullReviews: true, featureStars: true, featureReleases: true, + featureWorkflowFailure: true, + featureWorkflowSuccess: true, featureDiscussions: true, featureDiscussionComments: true, } @@ -901,7 +905,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, 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, 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 65b305484..ee2d67b79 100644 --- a/server/plugin/subscriptions.go +++ b/server/plugin/subscriptions.go @@ -123,6 +123,10 @@ func (s *Subscription) Stars() bool { return strings.Contains(s.Features.String(), featureStars) } +func (s *Subscription) Workflows() bool { + return strings.Contains(s.Features.String(), featureWorkflowFailure) || strings.Contains(s.Features.String(), featureWorkflowSuccess) +} + 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 ae041dc94..74eb68062 100644 --- a/server/plugin/template.go +++ b/server/plugin/template.go @@ -113,6 +113,16 @@ func init() { return commit.GetCommitter() } + funcMap["workflowJobFailedStep"] = func(steps []*github.TaskStep) string { + for _, step := range steps { + if step.GetConclusion() == workflowJobFail { + return step.GetName() + } + } + + return "" + } + masterTemplate = template.Must(template.New("master").Funcs(funcMap).Parse("")) // The user template links to the corresponding GitHub user. If the GitHub user is a known @@ -158,6 +168,11 @@ func init() { `[#{{.GetNumber}} {{.GetTitle}}]({{.GetHTMLURL}})`, )) + // The workflow job links to the corresponding workflow. + template.Must(masterTemplate.New("workflowJob").Parse( + `[{{.GetName}}]({{.GetHTMLURL}})`, + )) + // The release links to the corresponding release. template.Must(masterTemplate.New("release").Parse( `[{{.GetTagName}}]({{.GetHTMLURL}})`, @@ -412,6 +427,8 @@ Assignees: {{range $i, $el := .Assignees -}} {{- if $i}}, {{end}}{{template "use " * `issue_comments` - includes new issue comments\n" + " * `issue_creations` - includes new issues only \n" + " * `pull_reviews` - includes pull request reviews\n" + + " * `workflow_failure` - includes workflow job failure\n" + + " * `workflow_success` - includes workflow job success\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" + @@ -437,6 +454,11 @@ Assignees: {{range $i, $el := .Assignees -}} {{- if $i}}, {{end}}{{template "use {{- end }} by {{template "user" .GetSender}} It now has **{{.GetRepo.GetStargazersCount}}** stars.`)) + template.Must(masterTemplate.New("newWorkflowJob").Funcs(funcMap).Parse(` +{{template "repo" .GetRepo}} {{.GetWorkflowJob.GetWorkflowName}} workflow {{if eq .GetWorkflowJob.GetConclusion "success"}}succeeded{{else}}failed{{end}} (triggered by {{template "user" .GetSender}}) +{{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("newReleaseEvent").Funcs(funcMap).Parse(` {{template "repo" .GetRepo}} {{template "user" .GetSender}} {{- if eq .GetAction "created" }} created a release {{template "release" .GetRelease}} diff --git a/server/plugin/template_test.go b/server/plugin/template_test.go index e66947ea3..cc8e59bf0 100644 --- a/server/plugin/template_test.go +++ b/server/plugin/template_test.go @@ -1477,6 +1477,80 @@ func TestGitHubUsernameRegex(t *testing.T) { } } +func TestWorkflowJobNotification(t *testing.T) { + t.Run("failed", func(t *testing.T) { + expected := ` +[\[mattermost-plugin-github\]](https://github.com/mattermost/mattermost-plugin-github) mock-workflow-name workflow failed (triggered by [panda](https://github.com/panda)) +Job failed: [mock-workflow-job](https://github.com/mattermost/mattermost-plugin-github/actions/runs/12345/job/67890) +Step failed: mock-job-2 +Commit: https://github.com/mattermost/mattermost-plugin-github/commit/1234567890` + + actual, err := renderTemplate("newWorkflowJob", &github.WorkflowJobEvent{ + Repo: &repo, + Sender: &user, + Action: sToP(actionCompleted), + WorkflowJob: &github.WorkflowJob{ + Conclusion: sToP("failure"), + Name: sToP("mock-workflow-job"), + HeadSHA: sToP("1234567890"), + HTMLURL: sToP("https://github.com/mattermost/mattermost-plugin-github/actions/runs/12345/job/67890"), + WorkflowName: sToP("mock-workflow-name"), + Steps: []*github.TaskStep{ + { + Name: sToP("mock-job-1"), + Conclusion: sToP("success"), + }, + { + Name: sToP("mock-job-2"), + Conclusion: sToP("failure"), + }, + { + Name: sToP("mock-job-3"), + Conclusion: sToP("success"), + }, + }, + }, + }) + 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) mock-workflow-name workflow succeeded (triggered by [panda](https://github.com/panda)) +Commit: https://github.com/mattermost/mattermost-plugin-github/commit/1234567890` + + actual, err := renderTemplate("newWorkflowJob", &github.WorkflowJobEvent{ + Repo: &repo, + Sender: &user, + Action: sToP(actionCompleted), + WorkflowJob: &github.WorkflowJob{ + Conclusion: sToP("success"), + Name: sToP("mock-workflow-job"), + HeadSHA: sToP("1234567890"), + HTMLURL: sToP("https://github.com/mattermost/mattermost-plugin-github/actions/runs/12345/job/67890"), + WorkflowName: sToP("mock-workflow-name"), + Steps: []*github.TaskStep{ + { + Name: sToP("mock-job-1"), + Conclusion: sToP("success"), + }, + { + Name: sToP("mock-job-2"), + Conclusion: sToP("success"), + }, + { + Name: sToP("mock-job-3"), + Conclusion: sToP("success"), + }, + }, + }, + }) + require.NoError(t, err) + require.Equal(t, expected, actual) + }) +} + func sToP(s string) *string { return &s } diff --git a/server/plugin/webhook.go b/server/plugin/webhook.go index f91962f45..e7243fa31 100644 --- a/server/plugin/webhook.go +++ b/server/plugin/webhook.go @@ -28,9 +28,13 @@ const ( actionLabeled = "labeled" actionAssigned = "assigned" - actionCreated = "created" - actionDeleted = "deleted" - actionEdited = "edited" + actionCreated = "created" + actionDeleted = "deleted" + actionEdited = "edited" + actionCompleted = "completed" + + workflowJobFail = "failure" + workflowJobSuccess = "success" postPropGithubRepo = "gh_repo" postPropGithubObjectID = "gh_object_id" @@ -282,6 +286,11 @@ func (p *Plugin) handleWebhook(w http.ResponseWriter, r *http.Request) { handler = func() { p.postStarEvent(event) } + case *github.WorkflowJobEvent: + repo = event.GetRepo() + handler = func() { + p.postWorkflowJobEvent(event) + } case *github.ReleaseEvent: repo = event.GetRepo() handler = func() { @@ -1350,6 +1359,47 @@ func (p *Plugin) postStarEvent(event *github.StarEvent) { } } +func (p *Plugin) postWorkflowJobEvent(event *github.WorkflowJobEvent) { + if event.GetAction() != actionCompleted { + 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 { + return + } + + repo := event.GetRepo() + subs := p.GetSubscribedChannelsForRepository(repo) + + if len(subs) == 0 { + return + } + + newWorkflowJobMessage, err := renderTemplate("newWorkflowJob", event) + if err != nil { + p.client.Log.Warn("Failed to render template", "Error", err.Error()) + return + } + + for _, sub := range subs { + if !sub.Workflows() { + continue + } + + post := &model.Post{ + UserId: p.BotUserID, + Type: "custom_git_workflow_job", + Message: newWorkflowJobMessage, + 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,