From 200cb6140d9929130e4aacf49464bd544765befe Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Tue, 4 Feb 2025 19:35:47 -0800
Subject: [PATCH] Fix commit status events (#33320) (#33493)

Fix #32873
Fix #33201
~Fix #33244~
~Fix #33302~

depends on ~#33396~
backport #33320
---
 options/locale/locale_en-US.ini               |  2 ++
 routers/web/repo/setting/webhook.go           |  1 +
 services/forms/repo_form.go                   |  1 +
 services/webhook/dingtalk.go                  |  6 ++++++
 services/webhook/discord.go                   |  6 ++++++
 services/webhook/feishu.go                    |  6 ++++++
 services/webhook/general.go                   | 12 ++++++++++++
 services/webhook/matrix.go                    |  7 +++++++
 services/webhook/msteams.go                   | 14 ++++++++++++++
 services/webhook/packagist.go                 |  4 ++++
 services/webhook/payloader.go                 |  3 +++
 services/webhook/slack.go                     |  6 ++++++
 services/webhook/telegram.go                  |  6 ++++++
 services/webhook/wechatwork.go                |  6 ++++++
 templates/repo/settings/webhook/settings.tmpl | 11 +++++++++++
 15 files changed, 91 insertions(+)

diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 26c617f7bb..fd05ae7614 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -2325,6 +2325,8 @@ settings.event_fork = Fork
 settings.event_fork_desc = Repository forked.
 settings.event_wiki = Wiki
 settings.event_wiki_desc = Wiki page created, renamed, edited or deleted.
+settings.event_statuses = Statuses
+settings.event_statuses_desc = Commit Status updated from the API.
 settings.event_release = Release
 settings.event_release_desc = Release published, updated or deleted in a repository.
 settings.event_push = Push
diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go
index 8d548c4e3d..c519b4117f 100644
--- a/routers/web/repo/setting/webhook.go
+++ b/routers/web/repo/setting/webhook.go
@@ -184,6 +184,7 @@ func ParseHookEvent(form forms.WebhookForm) *webhook_module.HookEvent {
 			Wiki:                     form.Wiki,
 			Repository:               form.Repository,
 			Package:                  form.Package,
+			Status:                   form.Status,
 		},
 		BranchFilter: form.BranchFilter,
 	}
diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go
index 7647c74e46..f558133aeb 100644
--- a/services/forms/repo_form.go
+++ b/services/forms/repo_form.go
@@ -263,6 +263,7 @@ type WebhookForm struct {
 	Wiki                     bool
 	Repository               bool
 	Package                  bool
+	Status                   bool
 	Active                   bool
 	BranchFilter             string `binding:"GlobPattern"`
 	AuthorizationHeader      string
diff --git a/services/webhook/dingtalk.go b/services/webhook/dingtalk.go
index 992b8c566f..3ea8f50764 100644
--- a/services/webhook/dingtalk.go
+++ b/services/webhook/dingtalk.go
@@ -170,6 +170,12 @@ func (dc dingtalkConvertor) Package(p *api.PackagePayload) (DingtalkPayload, err
 	return createDingtalkPayload(text, text, "view package", p.Package.HTMLURL), nil
 }
 
+func (dc dingtalkConvertor) Status(p *api.CommitStatusPayload) (DingtalkPayload, error) {
+	text, _ := getStatusPayloadInfo(p, noneLinkFormatter, true)
+
+	return createDingtalkPayload(text, text, "Status Changed", p.TargetURL), nil
+}
+
 func createDingtalkPayload(title, text, singleTitle, singleURL string) DingtalkPayload {
 	return DingtalkPayload{
 		MsgType: "actionCard",
diff --git a/services/webhook/discord.go b/services/webhook/discord.go
index 30d930062e..43e5e533bf 100644
--- a/services/webhook/discord.go
+++ b/services/webhook/discord.go
@@ -265,6 +265,12 @@ func (d discordConvertor) Package(p *api.PackagePayload) (DiscordPayload, error)
 	return d.createPayload(p.Sender, text, "", p.Package.HTMLURL, color), nil
 }
 
+func (d discordConvertor) Status(p *api.CommitStatusPayload) (DiscordPayload, error) {
+	text, color := getStatusPayloadInfo(p, noneLinkFormatter, false)
+
+	return d.createPayload(p.Sender, text, "", p.TargetURL, color), nil
+}
+
 func newDiscordRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
 	meta := &DiscordMeta{}
 	if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
diff --git a/services/webhook/feishu.go b/services/webhook/feishu.go
index 4e6aebc39d..639118d2a5 100644
--- a/services/webhook/feishu.go
+++ b/services/webhook/feishu.go
@@ -166,6 +166,12 @@ func (fc feishuConvertor) Package(p *api.PackagePayload) (FeishuPayload, error)
 	return newFeishuTextPayload(text), nil
 }
 
+func (fc feishuConvertor) Status(p *api.CommitStatusPayload) (FeishuPayload, error) {
+	text, _ := getStatusPayloadInfo(p, noneLinkFormatter, true)
+
+	return newFeishuTextPayload(text), nil
+}
+
 func newFeishuRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
 	var pc payloadConvertor[FeishuPayload] = feishuConvertor{}
 	return newJSONRequest(pc, w, t, true)
diff --git a/services/webhook/general.go b/services/webhook/general.go
index dde43bb349..91bf68600f 100644
--- a/services/webhook/general.go
+++ b/services/webhook/general.go
@@ -307,6 +307,18 @@ func getPackagePayloadInfo(p *api.PackagePayload, linkFormatter linkFormatter, w
 	return text, color
 }
 
+func getStatusPayloadInfo(p *api.CommitStatusPayload, linkFormatter linkFormatter, withSender bool) (text string, color int) {
+	refLink := linkFormatter(p.TargetURL, p.Context+"["+p.SHA+"]:"+p.Description)
+
+	text = fmt.Sprintf("Commit Status changed: %s", refLink)
+	color = greenColor
+	if withSender {
+		text += fmt.Sprintf(" by %s", linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName))
+	}
+
+	return text, color
+}
+
 // ToHook convert models.Webhook to api.Hook
 // This function is not part of the convert package to prevent an import cycle
 func ToHook(repoLink string, w *webhook_model.Webhook) (*api.Hook, error) {
diff --git a/services/webhook/matrix.go b/services/webhook/matrix.go
index 96dfa139ac..ec21712837 100644
--- a/services/webhook/matrix.go
+++ b/services/webhook/matrix.go
@@ -244,6 +244,13 @@ func (m matrixConvertor) Package(p *api.PackagePayload) (MatrixPayload, error) {
 	return m.newPayload(text)
 }
 
+func (m matrixConvertor) Status(p *api.CommitStatusPayload) (MatrixPayload, error) {
+	refLink := htmlLinkFormatter(p.TargetURL, p.Context+"["+p.SHA+"]:"+p.Description)
+	text := fmt.Sprintf("Commit Status changed: %s", refLink)
+
+	return m.newPayload(text)
+}
+
 var urlRegex = regexp.MustCompile(`<a [^>]*?href="([^">]*?)">(.*?)</a>`)
 
 func getMessageBody(htmlText string) string {
diff --git a/services/webhook/msteams.go b/services/webhook/msteams.go
index 1ae7c4f931..485f695be2 100644
--- a/services/webhook/msteams.go
+++ b/services/webhook/msteams.go
@@ -303,6 +303,20 @@ func (m msteamsConvertor) Package(p *api.PackagePayload) (MSTeamsPayload, error)
 	), nil
 }
 
+func (m msteamsConvertor) Status(p *api.CommitStatusPayload) (MSTeamsPayload, error) {
+	title, color := getStatusPayloadInfo(p, noneLinkFormatter, false)
+
+	return createMSTeamsPayload(
+		p.Repo,
+		p.Sender,
+		title,
+		"",
+		p.TargetURL,
+		color,
+		&MSTeamsFact{"CommitStatus:", p.Context},
+	), nil
+}
+
 func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTarget string, color int, fact *MSTeamsFact) MSTeamsPayload {
 	facts := make([]MSTeamsFact, 0, 2)
 	if r != nil {
diff --git a/services/webhook/packagist.go b/services/webhook/packagist.go
index e66895832b..6864fc822a 100644
--- a/services/webhook/packagist.go
+++ b/services/webhook/packagist.go
@@ -110,6 +110,10 @@ func (pc packagistConvertor) Package(_ *api.PackagePayload) (PackagistPayload, e
 	return PackagistPayload{}, nil
 }
 
+func (pc packagistConvertor) Status(_ *api.CommitStatusPayload) (PackagistPayload, error) {
+	return PackagistPayload{}, nil
+}
+
 func newPackagistRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
 	meta := &PackagistMeta{}
 	if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
diff --git a/services/webhook/payloader.go b/services/webhook/payloader.go
index ab280a25b6..c29ad8ac92 100644
--- a/services/webhook/payloader.go
+++ b/services/webhook/payloader.go
@@ -28,6 +28,7 @@ type payloadConvertor[T any] interface {
 	Release(*api.ReleasePayload) (T, error)
 	Wiki(*api.WikiPayload) (T, error)
 	Package(*api.PackagePayload) (T, error)
+	Status(*api.CommitStatusPayload) (T, error)
 }
 
 func convertUnmarshalledJSON[T, P any](convert func(P) (T, error), data []byte) (t T, err error) {
@@ -77,6 +78,8 @@ func newPayload[T any](rc payloadConvertor[T], data []byte, event webhook_module
 		return convertUnmarshalledJSON(rc.Wiki, data)
 	case webhook_module.HookEventPackage:
 		return convertUnmarshalledJSON(rc.Package, data)
+	case webhook_module.HookEventStatus:
+		return convertUnmarshalledJSON(rc.Status, data)
 	}
 	return t, fmt.Errorf("newPayload unsupported event: %s", event)
 }
diff --git a/services/webhook/slack.go b/services/webhook/slack.go
index e0e938d53d..a5edb830f3 100644
--- a/services/webhook/slack.go
+++ b/services/webhook/slack.go
@@ -167,6 +167,12 @@ func (s slackConvertor) Package(p *api.PackagePayload) (SlackPayload, error) {
 	return s.createPayload(text, nil), nil
 }
 
+func (s slackConvertor) Status(p *api.CommitStatusPayload) (SlackPayload, error) {
+	text, _ := getStatusPayloadInfo(p, SlackLinkFormatter, true)
+
+	return s.createPayload(text, nil), nil
+}
+
 // Push implements payloadConvertor Push method
 func (s slackConvertor) Push(p *api.PushPayload) (SlackPayload, error) {
 	// n new commits
diff --git a/services/webhook/telegram.go b/services/webhook/telegram.go
index 6fbf995801..485e2d990b 100644
--- a/services/webhook/telegram.go
+++ b/services/webhook/telegram.go
@@ -174,6 +174,12 @@ func (t telegramConvertor) Package(p *api.PackagePayload) (TelegramPayload, erro
 	return createTelegramPayloadHTML(text), nil
 }
 
+func (t telegramConvertor) Status(p *api.CommitStatusPayload) (TelegramPayload, error) {
+	text, _ := getStatusPayloadInfo(p, htmlLinkFormatter, true)
+
+	return createTelegramPayloadHTML(text), nil
+}
+
 func createTelegramPayloadHTML(msgHTML string) TelegramPayload {
 	// https://core.telegram.org/bots/api#formatting-options
 	return TelegramPayload{
diff --git a/services/webhook/wechatwork.go b/services/webhook/wechatwork.go
index 44e0ff7de5..1c834b4020 100644
--- a/services/webhook/wechatwork.go
+++ b/services/webhook/wechatwork.go
@@ -175,6 +175,12 @@ func (wc wechatworkConvertor) Package(p *api.PackagePayload) (WechatworkPayload,
 	return newWechatworkMarkdownPayload(text), nil
 }
 
+func (wc wechatworkConvertor) Status(p *api.CommitStatusPayload) (WechatworkPayload, error) {
+	text, _ := getStatusPayloadInfo(p, noneLinkFormatter, true)
+
+	return newWechatworkMarkdownPayload(text), nil
+}
+
 func newWechatworkRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
 	var pc payloadConvertor[WechatworkPayload] = wechatworkConvertor{}
 	return newJSONRequest(pc, w, t, true)
diff --git a/templates/repo/settings/webhook/settings.tmpl b/templates/repo/settings/webhook/settings.tmpl
index cf3b0eb053..b31003f2f1 100644
--- a/templates/repo/settings/webhook/settings.tmpl
+++ b/templates/repo/settings/webhook/settings.tmpl
@@ -109,6 +109,17 @@
 			</div>
 		</div>
 
+		<!-- Status -->
+		<div class="seven wide column">
+			<div class="field">
+				<div class="ui checkbox">
+					<input name="status" type="checkbox" {{if .Webhook.HookEvents.Get "status"}}checked{{end}}>
+					<label>{{ctx.Locale.Tr "repo.settings.event_statuses"}}</label>
+					<span class="help">{{ctx.Locale.Tr "repo.settings.event_statuses_desc"}}</span>
+				</div>
+			</div>
+		</div>
+
 		<!-- Issue Events -->
 		<div class="fourteen wide column">
 			<label>{{ctx.Locale.Tr "repo.settings.event_header_issue"}}</label>