Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion server/plugin/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -62,6 +64,8 @@ var validFeatures = map[string]bool{
featureReleases: true,
featureWorkflowFailure: true,
featureWorkflowSuccess: true,
featureWorkflowRunFailure: true,
featureWorkflowRunSuccess: true,
featureDiscussions: true,
featureDiscussionComments: true,
}
Expand Down Expand Up @@ -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:\"<labelname>\". 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:\"<labelname>\". 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{
Expand Down
8 changes: 8 additions & 0 deletions server/plugin/subscriptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
14 changes: 11 additions & 3 deletions server/plugin/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
Expand Down Expand Up @@ -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:<labelname>` - 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" +
Expand Down Expand Up @@ -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}}
Expand All @@ -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}}):
Expand Down
102 changes: 102 additions & 0 deletions server/plugin/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
23 changes: 23 additions & 0 deletions server/plugin/test_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
67 changes: 63 additions & 4 deletions server/plugin/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading