Skip to content

Commit 4f11514

Browse files
MM-67473 Adding support to workflow run success/failures (#975)
* WIP: Adding support for workflow run events * Removing unused values * Refactoring for simplicity and removing redundant functions * Fixing linter error * Added missing subscription filter checks * Added tests, improving workflow handler flow * Fixing org repo order in text fixture * Updated test assertions and added cancelled and timed_out cases
1 parent 883ad46 commit 4f11514

7 files changed

Lines changed: 410 additions & 8 deletions

File tree

server/plugin/command.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ const (
3535
featureReleases = "releases"
3636
featureWorkflowFailure = "workflow_failure"
3737
featureWorkflowSuccess = "workflow_success"
38+
featureWorkflowRunFailure = "workflow_run_failure"
39+
featureWorkflowRunSuccess = "workflow_run_success"
3840
featureDiscussions = "discussions"
3941
featureDiscussionComments = "discussion_comments"
4042
)
@@ -62,6 +64,8 @@ var validFeatures = map[string]bool{
6264
featureReleases: true,
6365
featureWorkflowFailure: true,
6466
featureWorkflowSuccess: true,
67+
featureWorkflowRunFailure: true,
68+
featureWorkflowRunSuccess: true,
6569
featureDiscussions: true,
6670
featureDiscussionComments: true,
6771
}
@@ -1137,7 +1141,7 @@ func getAutocompleteData(config *Configuration) *model.AutocompleteData {
11371141

11381142
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")
11391143
subscriptionsAdd.AddTextArgument("Owner/repo to subscribe to", "[owner/repo]", "")
1140-
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)
1144+
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)
11411145

11421146
if config.GitHubOrg != "" {
11431147
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{

server/plugin/subscriptions.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,14 @@ func (s *Subscription) Workflows() bool {
147147
return strings.Contains(s.Features.String(), featureWorkflowFailure) || strings.Contains(s.Features.String(), featureWorkflowSuccess)
148148
}
149149

150+
func (s *Subscription) WorkflowRunFailures() bool {
151+
return strings.Contains(s.Features.String(), featureWorkflowRunFailure)
152+
}
153+
154+
func (s *Subscription) WorkflowRunSuccesses() bool {
155+
return strings.Contains(s.Features.String(), featureWorkflowRunSuccess)
156+
}
157+
150158
func (s *Subscription) Release() bool {
151159
return strings.Contains(s.Features.String(), featureReleases)
152160
}

server/plugin/template.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ func init() {
120120

121121
funcMap["workflowJobFailedStep"] = func(steps []*github.TaskStep) string {
122122
for _, step := range steps {
123-
if step.GetConclusion() == workflowJobFail {
123+
if step.GetConclusion() == workflowConclusionFailure {
124124
return step.GetName()
125125
}
126126
}
@@ -453,6 +453,8 @@ Reviewers: {{range $i, $el := .RequestedReviewers -}} {{- if $i}}, {{end}}{{temp
453453
" * `pull_reviews` - includes pull request reviews\n" +
454454
" * `workflow_failure` - includes workflow job failure\n" +
455455
" * `workflow_success` - includes workflow job success\n" +
456+
" * `workflow_run_failure` - includes workflow run failures (failures, cancellations, timeouts)\n" +
457+
" * `workflow_run_success` - includes workflow run successes\n" +
456458
" * `releases` - includes release created and deleted\n" +
457459
" * `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" +
458460
" * `discussions` - includes new discussions\n" +
@@ -489,6 +491,12 @@ It now has **{{.GetRepo.GetStargazersCount}}** stars.`))
489491
{{if eq .GetWorkflowJob.GetConclusion "failure"}}Job failed: {{template "workflowJob" .GetWorkflowJob}}
490492
Step failed: {{.GetWorkflowJob.Steps | workflowJobFailedStep}}
491493
{{end}}Commit: {{.GetRepo.GetHTMLURL}}/commit/{{.GetWorkflowJob.GetHeadSHA}}`))
494+
495+
template.Must(masterTemplate.New("workflowRunCompleted").Funcs(funcMap).Parse(`
496+
{{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}}
497+
Branch: ` + "`" + `{{.GetWorkflowRun.GetHeadBranch}}` + "`" + ` | Run [#{{.GetWorkflowRun.GetRunNumber}}]({{.GetWorkflowRun.GetHTMLURL}}) | Triggered by {{template "user" .GetSender}}
498+
Commit: {{.GetRepo.GetHTMLURL}}/commit/{{.GetWorkflowRun.GetHeadSHA}}`))
499+
492500
template.Must(masterTemplate.New("newReleaseEvent").Funcs(funcMap).Parse(`
493501
{{template "repo" .GetRepo}} {{template "user" .GetSender}}
494502
{{- if eq .GetAction "created" }} created a release {{template "release" .GetRelease}}
@@ -505,8 +513,8 @@ Step failed: {{.GetWorkflowJob.Steps | workflowJobFailedStep}}
505513
`))
506514

507515
template.Must(masterTemplate.New("newDiscussionComment").Funcs(funcMap).Parse(`
508-
{{template "repo" .GetRepo}}
509-
{{- if eq .GetAction "created" }} New comment
516+
{{template "repo" .GetRepo}}
517+
{{- if eq .GetAction "created" }} New comment
510518
{{- else if eq .GetAction "edited" }} Comment edited
511519
{{- else if eq .GetAction "deleted" }} Comment deleted
512520
{{- end }} by {{template "user" .GetSender}} on discussion [#{{.GetDiscussion.GetNumber}} {{.GetDiscussion.GetTitle}}]({{.GetDiscussion.GetHTMLURL}}):

server/plugin/template_test.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1578,6 +1578,108 @@ Commit: https://github.com/mattermost/mattermost-plugin-github/commit/1234567890
15781578
})
15791579
}
15801580

1581+
func TestWorkflowRunNotification(t *testing.T) {
1582+
t.Run("failed", func(t *testing.T) {
1583+
expected := `
1584+
[\[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:
1585+
Branch: ` + "`main`" + ` | Run [#42](https://github.com/mattermost/mattermost-plugin-github/actions/runs/99999) | Triggered by [panda](https://github.com/panda)
1586+
Commit: https://github.com/mattermost/mattermost-plugin-github/commit/abc1234567`
1587+
1588+
actual, err := renderTemplate("workflowRunCompleted", &github.WorkflowRunEvent{
1589+
Repo: &repo,
1590+
Sender: &user,
1591+
Action: sToP(actionCompleted),
1592+
Workflow: &github.Workflow{
1593+
Name: sToP("CI Pipeline"),
1594+
},
1595+
WorkflowRun: &github.WorkflowRun{
1596+
Conclusion: sToP("failure"),
1597+
HeadBranch: sToP("main"),
1598+
HeadSHA: sToP("abc1234567"),
1599+
HTMLURL: sToP("https://github.com/mattermost/mattermost-plugin-github/actions/runs/99999"),
1600+
RunNumber: iToP(42),
1601+
},
1602+
})
1603+
require.NoError(t, err)
1604+
require.Equal(t, expected, actual)
1605+
})
1606+
1607+
t.Run("success", func(t *testing.T) {
1608+
expected := `
1609+
[\[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:
1610+
Branch: ` + "`main`" + ` | Run [#42](https://github.com/mattermost/mattermost-plugin-github/actions/runs/99999) | Triggered by [panda](https://github.com/panda)
1611+
Commit: https://github.com/mattermost/mattermost-plugin-github/commit/abc1234567`
1612+
1613+
actual, err := renderTemplate("workflowRunCompleted", &github.WorkflowRunEvent{
1614+
Repo: &repo,
1615+
Sender: &user,
1616+
Action: sToP(actionCompleted),
1617+
Workflow: &github.Workflow{
1618+
Name: sToP("CI Pipeline"),
1619+
},
1620+
WorkflowRun: &github.WorkflowRun{
1621+
Conclusion: sToP("success"),
1622+
HeadBranch: sToP("main"),
1623+
HeadSHA: sToP("abc1234567"),
1624+
HTMLURL: sToP("https://github.com/mattermost/mattermost-plugin-github/actions/runs/99999"),
1625+
RunNumber: iToP(42),
1626+
},
1627+
})
1628+
require.NoError(t, err)
1629+
require.Equal(t, expected, actual)
1630+
})
1631+
1632+
t.Run("cancelled", func(t *testing.T) {
1633+
expected := `
1634+
[\[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:
1635+
Branch: ` + "`main`" + ` | Run [#42](https://github.com/mattermost/mattermost-plugin-github/actions/runs/99999) | Triggered by [panda](https://github.com/panda)
1636+
Commit: https://github.com/mattermost/mattermost-plugin-github/commit/abc1234567`
1637+
1638+
actual, err := renderTemplate("workflowRunCompleted", &github.WorkflowRunEvent{
1639+
Repo: &repo,
1640+
Sender: &user,
1641+
Action: sToP(actionCompleted),
1642+
Workflow: &github.Workflow{
1643+
Name: sToP("CI Pipeline"),
1644+
},
1645+
WorkflowRun: &github.WorkflowRun{
1646+
Conclusion: sToP("cancelled"),
1647+
HeadBranch: sToP("main"),
1648+
HeadSHA: sToP("abc1234567"),
1649+
HTMLURL: sToP("https://github.com/mattermost/mattermost-plugin-github/actions/runs/99999"),
1650+
RunNumber: iToP(42),
1651+
},
1652+
})
1653+
require.NoError(t, err)
1654+
require.Equal(t, expected, actual)
1655+
})
1656+
1657+
t.Run("timed_out", func(t *testing.T) {
1658+
expected := `
1659+
[\[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:
1660+
Branch: ` + "`main`" + ` | Run [#42](https://github.com/mattermost/mattermost-plugin-github/actions/runs/99999) | Triggered by [panda](https://github.com/panda)
1661+
Commit: https://github.com/mattermost/mattermost-plugin-github/commit/abc1234567`
1662+
1663+
actual, err := renderTemplate("workflowRunCompleted", &github.WorkflowRunEvent{
1664+
Repo: &repo,
1665+
Sender: &user,
1666+
Action: sToP(actionCompleted),
1667+
Workflow: &github.Workflow{
1668+
Name: sToP("CI Pipeline"),
1669+
},
1670+
WorkflowRun: &github.WorkflowRun{
1671+
Conclusion: sToP("timed_out"),
1672+
HeadBranch: sToP("main"),
1673+
HeadSHA: sToP("abc1234567"),
1674+
HTMLURL: sToP("https://github.com/mattermost/mattermost-plugin-github/actions/runs/99999"),
1675+
RunNumber: iToP(42),
1676+
},
1677+
})
1678+
require.NoError(t, err)
1679+
require.Equal(t, expected, actual)
1680+
})
1681+
}
1682+
15811683
func sToP(s string) *string {
15821684
return &s
15831685
}

server/plugin/test_utils.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,29 @@ func GetMockDiscussionEvent(repo, org, sender string) *github.DiscussionEvent {
427427
}
428428
}
429429

430+
func GetMockWorkflowRunEvent(action, conclusion, repo, org, sender string) *github.WorkflowRunEvent {
431+
return &github.WorkflowRunEvent{
432+
Action: github.String(action),
433+
Repo: &github.Repository{
434+
Name: github.String(repo),
435+
Owner: &github.User{Login: github.String(org)},
436+
FullName: github.String(fmt.Sprintf("%s/%s", org, repo)),
437+
HTMLURL: github.String(fmt.Sprintf("%s%s/%s", GithubBaseURL, org, repo)),
438+
},
439+
Sender: &github.User{Login: github.String(sender)},
440+
Workflow: &github.Workflow{
441+
Name: github.String("CI Pipeline"),
442+
},
443+
WorkflowRun: &github.WorkflowRun{
444+
Conclusion: github.String(conclusion),
445+
HeadBranch: github.String("main"),
446+
HeadSHA: github.String("abc1234567"),
447+
HTMLURL: github.String(fmt.Sprintf("%s%s/%s/actions/runs/99999", GithubBaseURL, org, repo)),
448+
RunNumber: github.Int(42),
449+
},
450+
}
451+
}
452+
430453
func GetMockDiscussionCommentEvent(repo, org, action, sender string) *github.DiscussionCommentEvent {
431454
return &github.DiscussionCommentEvent{
432455
Action: &action,

server/plugin/webhook.go

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,10 @@ const (
3838
actionEdited = "edited"
3939
actionCompleted = "completed"
4040

41-
workflowJobFail = "failure"
42-
workflowJobSuccess = "success"
41+
workflowConclusionFailure = "failure"
42+
workflowConclusionSuccess = "success"
43+
workflowConclusionCancelled = "cancelled"
44+
workflowConclusionTimedOut = "timed_out"
4345

4446
postPropGithubRepo = "gh_repo"
4547
postPropGithubObjectID = "gh_object_id"
@@ -301,6 +303,11 @@ func (p *Plugin) handleWebhook(w http.ResponseWriter, r *http.Request) {
301303
handler = func() {
302304
p.postWorkflowJobEvent(event)
303305
}
306+
case *github.WorkflowRunEvent:
307+
repo = event.GetRepo()
308+
handler = func() {
309+
p.postWorkflowRunEvent(event)
310+
}
304311
case *github.ReleaseEvent:
305312
repo = event.GetRepo()
306313
handler = func() {
@@ -1463,8 +1470,7 @@ func (p *Plugin) postWorkflowJobEvent(event *github.WorkflowJobEvent) {
14631470
return
14641471
}
14651472

1466-
// Create a post only when the workflow job is completed and has either failed or succeeded
1467-
if event.GetWorkflowJob().GetConclusion() != workflowJobFail && event.GetWorkflowJob().GetConclusion() != workflowJobSuccess {
1473+
if event.GetWorkflowJob().GetConclusion() != workflowConclusionFailure && event.GetWorkflowJob().GetConclusion() != workflowConclusionSuccess {
14681474
return
14691475
}
14701476

@@ -1499,6 +1505,59 @@ func (p *Plugin) postWorkflowJobEvent(event *github.WorkflowJobEvent) {
14991505
}
15001506
}
15011507

1508+
func (p *Plugin) postWorkflowRunEvent(event *github.WorkflowRunEvent) {
1509+
if event.GetAction() != actionCompleted {
1510+
return
1511+
}
1512+
1513+
conclusion := event.GetWorkflowRun().GetConclusion()
1514+
isSuccess := conclusion == workflowConclusionSuccess
1515+
isFailure := conclusion == workflowConclusionFailure ||
1516+
conclusion == workflowConclusionCancelled ||
1517+
conclusion == workflowConclusionTimedOut
1518+
1519+
if !isSuccess && !isFailure {
1520+
return
1521+
}
1522+
1523+
repo := event.GetRepo()
1524+
subs := p.GetSubscribedChannelsForRepository(repo)
1525+
if len(subs) == 0 {
1526+
return
1527+
}
1528+
1529+
workflowRunMessage, err := renderTemplate("workflowRunCompleted", event)
1530+
if err != nil {
1531+
p.client.Log.Warn("Failed to render template", "Error", err.Error())
1532+
return
1533+
}
1534+
1535+
for _, sub := range subs {
1536+
if (isFailure && !sub.WorkflowRunFailures()) || (isSuccess && !sub.WorkflowRunSuccesses()) {
1537+
continue
1538+
}
1539+
1540+
if p.excludeConfigOrgMember(event.GetSender(), sub) {
1541+
continue
1542+
}
1543+
1544+
if p.shouldDenyEventDueToNotOrgMember(event.GetSender(), sub) {
1545+
continue
1546+
}
1547+
1548+
post := &model.Post{
1549+
UserId: p.BotUserID,
1550+
Type: "custom_git_workflow_run",
1551+
Message: workflowRunMessage,
1552+
ChannelId: sub.ChannelID,
1553+
}
1554+
1555+
if err = p.client.Post.CreatePost(post); err != nil {
1556+
p.client.Log.Warn("Error webhook post", "Post", post, "Error", err.Error())
1557+
}
1558+
}
1559+
}
1560+
15021561
func (p *Plugin) makeBotPost(message, postType string) *model.Post {
15031562
return &model.Post{
15041563
UserId: p.BotUserID,

0 commit comments

Comments
 (0)