feat(webhook): add reviewer name to MS Teams review request notifications (#38289)

Include the requested reviewer's username (along with their full name in
parentheses, if available) and render the `Repository` and `Pull
request` fields as clickable links in Microsoft Teams webhook
notifications.

Fixes: https://github.com/go-gitea/gitea/issues/38270

## Screenshots

<img width="1246" height="651" alt="image"
src="https://github.com/user-attachments/assets/7299ce10-c6d4-4c89-a05a-a258d72c00e5"
/>

---------

Signed-off-by: Shudhanshu Singh <sudhanshuwriterblc@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
Shudhanshu Singh
2026-07-06 18:30:58 +00:00
committed by GitHub
co-authored by wxiaoguang
parent a46e331637
commit 582217a0da
2 changed files with 72 additions and 33 deletions
+21 -8
View File
@@ -187,6 +187,18 @@ func (m msteamsConvertor) IssueComment(p *api.IssueCommentPayload) (MSTeamsPaylo
func (m msteamsConvertor) PullRequest(p *api.PullRequestPayload) (MSTeamsPayload, error) { func (m msteamsConvertor) PullRequest(p *api.PullRequestPayload) (MSTeamsPayload, error) {
title, _, extraMarkdown, color := getPullRequestPayloadInfo(p, noneLinkFormatter, false) title, _, extraMarkdown, color := getPullRequestPayloadInfo(p, noneLinkFormatter, false)
facts := []*MSTeamsFact{
{"Pull request:", fmt.Sprintf("[#%d](%s)", p.PullRequest.Index, p.PullRequest.HTMLURL)},
}
if (p.Action == api.HookIssueReviewRequested || p.Action == api.HookIssueReviewRequestRemoved) && p.RequestedReviewer != nil {
reviewerName := p.RequestedReviewer.UserName
if p.RequestedReviewer.FullName != "" {
reviewerName += " (" + p.RequestedReviewer.FullName + ")"
}
facts = append(facts, &MSTeamsFact{"Requested Reviewer:", reviewerName})
}
return createMSTeamsPayload( return createMSTeamsPayload(
p.Repository, p.Repository,
p.Sender, p.Sender,
@@ -194,7 +206,7 @@ func (m msteamsConvertor) PullRequest(p *api.PullRequestPayload) (MSTeamsPayload
extraMarkdown, extraMarkdown,
p.PullRequest.HTMLURL, p.PullRequest.HTMLURL,
color, color,
&MSTeamsFact{"Pull request #:", strconv.FormatInt(p.PullRequest.ID, 10)}, facts...,
), nil ), nil
} }
@@ -231,7 +243,7 @@ func (m msteamsConvertor) Review(p *api.PullRequestPayload, event webhook_module
text, text,
p.PullRequest.HTMLURL, p.PullRequest.HTMLURL,
color, color,
&MSTeamsFact{"Pull request #:", strconv.FormatInt(p.PullRequest.ID, 10)}, &MSTeamsFact{"Pull request:", fmt.Sprintf("[#%d](%s)", p.PullRequest.Index, p.PullRequest.HTMLURL)},
), nil ), nil
} }
@@ -271,7 +283,6 @@ func (m msteamsConvertor) Wiki(p *api.WikiPayload) (MSTeamsPayload, error) {
"", "",
p.Repository.HTMLURL+"/wiki/"+url.PathEscape(p.Page), p.Repository.HTMLURL+"/wiki/"+url.PathEscape(p.Page),
color, color,
&MSTeamsFact{"Repository:", p.Repository.FullName},
), nil ), nil
} }
@@ -346,16 +357,18 @@ func (msteamsConvertor) WorkflowJob(p *api.WorkflowJobPayload) (MSTeamsPayload,
), nil ), nil
} }
func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTarget string, color int, fact *MSTeamsFact) MSTeamsPayload { func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTarget string, color int, extraFacts ...*MSTeamsFact) MSTeamsPayload {
facts := make([]MSTeamsFact, 0, 2) facts := make([]MSTeamsFact, 0, len(extraFacts)+1)
if r != nil { if r != nil {
facts = append(facts, MSTeamsFact{ facts = append(facts, MSTeamsFact{
Name: "Repository:", Name: "Repository:",
Value: r.FullName, Value: fmt.Sprintf("[%s](%s)", r.FullName, r.HTMLURL),
}) })
} }
if fact != nil { for _, f := range extraFacts {
facts = append(facts, *fact) if f != nil {
facts = append(facts, *f)
}
} }
return MSTeamsPayload{ return MSTeamsPayload{
+51 -25
View File
@@ -4,6 +4,7 @@
package webhook package webhook
import ( import (
"fmt"
"testing" "testing"
webhook_model "gitea.dev/models/webhook" webhook_model "gitea.dev/models/webhook"
@@ -31,7 +32,7 @@ func TestMSTeamsPayload(t *testing.T) {
assert.Len(t, pl.Sections[0].Facts, 2) assert.Len(t, pl.Sections[0].Facts, 2)
for _, fact := range pl.Sections[0].Facts { for _, fact := range pl.Sections[0].Facts {
if fact.Name == "Repository:" { if fact.Name == "Repository:" {
assert.Equal(t, p.Repo.FullName, fact.Value) assert.Equal(t, fmt.Sprintf("[%s](%s)", p.Repo.FullName, p.Repo.HTMLURL), fact.Value)
} else if fact.Name == "branch:" { } else if fact.Name == "branch:" {
assert.Equal(t, "test", fact.Value) assert.Equal(t, "test", fact.Value)
} else { } else {
@@ -57,7 +58,7 @@ func TestMSTeamsPayload(t *testing.T) {
assert.Len(t, pl.Sections[0].Facts, 2) assert.Len(t, pl.Sections[0].Facts, 2)
for _, fact := range pl.Sections[0].Facts { for _, fact := range pl.Sections[0].Facts {
if fact.Name == "Repository:" { if fact.Name == "Repository:" {
assert.Equal(t, p.Repo.FullName, fact.Value) assert.Equal(t, fmt.Sprintf("[%s](%s)", p.Repo.FullName, p.Repo.HTMLURL), fact.Value)
} else if fact.Name == "branch:" { } else if fact.Name == "branch:" {
assert.Equal(t, "test", fact.Value) assert.Equal(t, "test", fact.Value)
} else { } else {
@@ -83,7 +84,7 @@ func TestMSTeamsPayload(t *testing.T) {
assert.Len(t, pl.Sections[0].Facts, 2) assert.Len(t, pl.Sections[0].Facts, 2)
for _, fact := range pl.Sections[0].Facts { for _, fact := range pl.Sections[0].Facts {
if fact.Name == "Repository:" { if fact.Name == "Repository:" {
assert.Equal(t, p.Repo.FullName, fact.Value) assert.Equal(t, fmt.Sprintf("[%s](%s)", p.Repo.FullName, p.Repo.HTMLURL), fact.Value)
} else if fact.Name == "Forkee:" { } else if fact.Name == "Forkee:" {
assert.Equal(t, p.Forkee.FullName, fact.Value) assert.Equal(t, p.Forkee.FullName, fact.Value)
} else { } else {
@@ -109,7 +110,7 @@ func TestMSTeamsPayload(t *testing.T) {
assert.Len(t, pl.Sections[0].Facts, 2) assert.Len(t, pl.Sections[0].Facts, 2)
for _, fact := range pl.Sections[0].Facts { for _, fact := range pl.Sections[0].Facts {
if fact.Name == "Repository:" { if fact.Name == "Repository:" {
assert.Equal(t, p.Repo.FullName, fact.Value) assert.Equal(t, fmt.Sprintf("[%s](%s)", p.Repo.FullName, p.Repo.HTMLURL), fact.Value)
} else if fact.Name == "Commit count:" { } else if fact.Name == "Commit count:" {
assert.Equal(t, "2", fact.Value) assert.Equal(t, "2", fact.Value)
} else { } else {
@@ -136,7 +137,7 @@ func TestMSTeamsPayload(t *testing.T) {
assert.Len(t, pl.Sections[0].Facts, 2) assert.Len(t, pl.Sections[0].Facts, 2)
for _, fact := range pl.Sections[0].Facts { for _, fact := range pl.Sections[0].Facts {
if fact.Name == "Repository:" { if fact.Name == "Repository:" {
assert.Equal(t, p.Repository.FullName, fact.Value) assert.Equal(t, fmt.Sprintf("[%s](%s)", p.Repository.FullName, p.Repository.HTMLURL), fact.Value)
} else if fact.Name == "Issue #:" { } else if fact.Name == "Issue #:" {
assert.Equal(t, "2", fact.Value) assert.Equal(t, "2", fact.Value)
} else { } else {
@@ -159,7 +160,7 @@ func TestMSTeamsPayload(t *testing.T) {
assert.Len(t, pl.Sections[0].Facts, 2) assert.Len(t, pl.Sections[0].Facts, 2)
for _, fact := range pl.Sections[0].Facts { for _, fact := range pl.Sections[0].Facts {
if fact.Name == "Repository:" { if fact.Name == "Repository:" {
assert.Equal(t, p.Repository.FullName, fact.Value) assert.Equal(t, fmt.Sprintf("[%s](%s)", p.Repository.FullName, p.Repository.HTMLURL), fact.Value)
} else if fact.Name == "Issue #:" { } else if fact.Name == "Issue #:" {
assert.Equal(t, "2", fact.Value) assert.Equal(t, "2", fact.Value)
} else { } else {
@@ -185,7 +186,7 @@ func TestMSTeamsPayload(t *testing.T) {
assert.Len(t, pl.Sections[0].Facts, 2) assert.Len(t, pl.Sections[0].Facts, 2)
for _, fact := range pl.Sections[0].Facts { for _, fact := range pl.Sections[0].Facts {
if fact.Name == "Repository:" { if fact.Name == "Repository:" {
assert.Equal(t, p.Repository.FullName, fact.Value) assert.Equal(t, fmt.Sprintf("[%s](%s)", p.Repository.FullName, p.Repository.HTMLURL), fact.Value)
} else if fact.Name == "Issue #:" { } else if fact.Name == "Issue #:" {
assert.Equal(t, "2", fact.Value) assert.Equal(t, "2", fact.Value)
} else { } else {
@@ -211,9 +212,9 @@ func TestMSTeamsPayload(t *testing.T) {
assert.Len(t, pl.Sections[0].Facts, 2) assert.Len(t, pl.Sections[0].Facts, 2)
for _, fact := range pl.Sections[0].Facts { for _, fact := range pl.Sections[0].Facts {
if fact.Name == "Repository:" { if fact.Name == "Repository:" {
assert.Equal(t, p.Repository.FullName, fact.Value) assert.Equal(t, fmt.Sprintf("[%s](%s)", p.Repository.FullName, p.Repository.HTMLURL), fact.Value)
} else if fact.Name == "Pull request #:" { } else if fact.Name == "Pull request:" {
assert.Equal(t, "12", fact.Value) assert.Equal(t, fmt.Sprintf("[#%d](%s)", p.PullRequest.Index, p.PullRequest.HTMLURL), fact.Value)
} else { } else {
t.Fail() t.Fail()
} }
@@ -223,6 +224,34 @@ func TestMSTeamsPayload(t *testing.T) {
assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.PotentialAction[0].Targets[0].URI) assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.PotentialAction[0].Targets[0].URI)
}) })
t.Run("PullRequestReviewRequest", func(t *testing.T) {
p := pullRequestTestPayload()
p.Action = api.HookIssueReviewRequested
p.RequestedReviewer = &api.User{
UserName: "reviewer1",
FullName: "Reviewer One",
}
pl, err := mc.PullRequest(p)
require.NoError(t, err)
assert.Len(t, pl.Sections[0].Facts, 3)
var hasReviewer bool
for _, fact := range pl.Sections[0].Facts {
if fact.Name == "Repository:" {
assert.Equal(t, fmt.Sprintf("[%s](%s)", p.Repository.FullName, p.Repository.HTMLURL), fact.Value)
} else if fact.Name == "Pull request:" {
assert.Equal(t, fmt.Sprintf("[#%d](%s)", p.PullRequest.Index, p.PullRequest.HTMLURL), fact.Value)
} else if fact.Name == "Requested Reviewer:" {
assert.Equal(t, "reviewer1 (Reviewer One)", fact.Value)
hasReviewer = true
} else {
t.Fail()
}
}
assert.True(t, hasReviewer)
})
t.Run("PullRequestComment", func(t *testing.T) { t.Run("PullRequestComment", func(t *testing.T) {
p := pullRequestCommentTestPayload() p := pullRequestCommentTestPayload()
@@ -237,7 +266,7 @@ func TestMSTeamsPayload(t *testing.T) {
assert.Len(t, pl.Sections[0].Facts, 2) assert.Len(t, pl.Sections[0].Facts, 2)
for _, fact := range pl.Sections[0].Facts { for _, fact := range pl.Sections[0].Facts {
if fact.Name == "Repository:" { if fact.Name == "Repository:" {
assert.Equal(t, p.Repository.FullName, fact.Value) assert.Equal(t, fmt.Sprintf("[%s](%s)", p.Repository.FullName, p.Repository.HTMLURL), fact.Value)
} else if fact.Name == "Issue #:" { } else if fact.Name == "Issue #:" {
assert.Equal(t, "12", fact.Value) assert.Equal(t, "12", fact.Value)
} else { } else {
@@ -264,9 +293,9 @@ func TestMSTeamsPayload(t *testing.T) {
assert.Len(t, pl.Sections[0].Facts, 2) assert.Len(t, pl.Sections[0].Facts, 2)
for _, fact := range pl.Sections[0].Facts { for _, fact := range pl.Sections[0].Facts {
if fact.Name == "Repository:" { if fact.Name == "Repository:" {
assert.Equal(t, p.Repository.FullName, fact.Value) assert.Equal(t, fmt.Sprintf("[%s](%s)", p.Repository.FullName, p.Repository.HTMLURL), fact.Value)
} else if fact.Name == "Pull request #:" { } else if fact.Name == "Pull request:" {
assert.Equal(t, "12", fact.Value) assert.Equal(t, fmt.Sprintf("[#%d](%s)", p.PullRequest.Index, p.PullRequest.HTMLURL), fact.Value)
} else { } else {
t.Fail() t.Fail()
} }
@@ -290,7 +319,7 @@ func TestMSTeamsPayload(t *testing.T) {
assert.Len(t, pl.Sections[0].Facts, 1) assert.Len(t, pl.Sections[0].Facts, 1)
for _, fact := range pl.Sections[0].Facts { for _, fact := range pl.Sections[0].Facts {
if fact.Name == "Repository:" { if fact.Name == "Repository:" {
assert.Equal(t, p.Repository.FullName, fact.Value) assert.Equal(t, fmt.Sprintf("[%s](%s)", p.Repository.FullName, p.Repository.HTMLURL), fact.Value)
} else { } else {
t.Fail() t.Fail()
} }
@@ -336,17 +365,14 @@ func TestMSTeamsPayload(t *testing.T) {
assert.Len(t, pl.Sections, 1) assert.Len(t, pl.Sections, 1)
assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
assert.Empty(t, pl.Sections[0].Text) assert.Empty(t, pl.Sections[0].Text)
assert.Len(t, pl.Sections[0].Facts, 2) assert.Len(t, pl.Sections[0].Facts, 1)
for _, fact := range pl.Sections[0].Facts { for _, fact := range pl.Sections[0].Facts {
if fact.Name == "Repository:" { if fact.Name == "Repository:" {
assert.Equal(t, p.Repository.FullName, fact.Value) assert.Equal(t, fmt.Sprintf("[%s](%s)", p.Repository.FullName, p.Repository.HTMLURL), fact.Value)
} else { } else {
t.Fail() t.Fail()
} }
} }
assert.Len(t, pl.PotentialAction, 1)
assert.Len(t, pl.PotentialAction[0].Targets, 1)
assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.PotentialAction[0].Targets[0].URI)
p.Action = api.HookWikiEdited p.Action = api.HookWikiEdited
pl, err = mc.Wiki(p) pl, err = mc.Wiki(p)
@@ -357,10 +383,10 @@ func TestMSTeamsPayload(t *testing.T) {
assert.Len(t, pl.Sections, 1) assert.Len(t, pl.Sections, 1)
assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
assert.Empty(t, pl.Sections[0].Text) assert.Empty(t, pl.Sections[0].Text)
assert.Len(t, pl.Sections[0].Facts, 2) assert.Len(t, pl.Sections[0].Facts, 1)
for _, fact := range pl.Sections[0].Facts { for _, fact := range pl.Sections[0].Facts {
if fact.Name == "Repository:" { if fact.Name == "Repository:" {
assert.Equal(t, p.Repository.FullName, fact.Value) assert.Equal(t, fmt.Sprintf("[%s](%s)", p.Repository.FullName, p.Repository.HTMLURL), fact.Value)
} else { } else {
t.Fail() t.Fail()
} }
@@ -378,10 +404,10 @@ func TestMSTeamsPayload(t *testing.T) {
assert.Len(t, pl.Sections, 1) assert.Len(t, pl.Sections, 1)
assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle) assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
assert.Empty(t, pl.Sections[0].Text) assert.Empty(t, pl.Sections[0].Text)
assert.Len(t, pl.Sections[0].Facts, 2) assert.Len(t, pl.Sections[0].Facts, 1)
for _, fact := range pl.Sections[0].Facts { for _, fact := range pl.Sections[0].Facts {
if fact.Name == "Repository:" { if fact.Name == "Repository:" {
assert.Equal(t, p.Repository.FullName, fact.Value) assert.Equal(t, fmt.Sprintf("[%s](%s)", p.Repository.FullName, p.Repository.HTMLURL), fact.Value)
} else { } else {
t.Fail() t.Fail()
} }
@@ -405,7 +431,7 @@ func TestMSTeamsPayload(t *testing.T) {
assert.Len(t, pl.Sections[0].Facts, 2) assert.Len(t, pl.Sections[0].Facts, 2)
for _, fact := range pl.Sections[0].Facts { for _, fact := range pl.Sections[0].Facts {
if fact.Name == "Repository:" { if fact.Name == "Repository:" {
assert.Equal(t, p.Repository.FullName, fact.Value) assert.Equal(t, fmt.Sprintf("[%s](%s)", p.Repository.FullName, p.Repository.HTMLURL), fact.Value)
} else if fact.Name == "Tag:" { } else if fact.Name == "Tag:" {
assert.Equal(t, "v1.0", fact.Value) assert.Equal(t, "v1.0", fact.Value)
} else { } else {