aboutsummaryrefslogtreecommitdiffstats
path: root/services/webhook
diff options
context:
space:
mode:
Diffstat (limited to 'services/webhook')
-rw-r--r--services/webhook/dingtalk.go6
-rw-r--r--services/webhook/discord.go6
-rw-r--r--services/webhook/feishu.go42
-rw-r--r--services/webhook/feishu_test.go6
-rw-r--r--services/webhook/general.go31
-rw-r--r--services/webhook/matrix.go6
-rw-r--r--services/webhook/msteams.go14
-rw-r--r--services/webhook/notifier.go128
-rw-r--r--services/webhook/packagist.go4
-rw-r--r--services/webhook/payloader.go6
-rw-r--r--services/webhook/slack.go6
-rw-r--r--services/webhook/telegram.go6
-rw-r--r--services/webhook/wechatwork.go6
13 files changed, 178 insertions, 89 deletions
diff --git a/services/webhook/dingtalk.go b/services/webhook/dingtalk.go
index ce907bf0cb..5bbc610fe5 100644
--- a/services/webhook/dingtalk.go
+++ b/services/webhook/dingtalk.go
@@ -176,6 +176,12 @@ func (dc dingtalkConvertor) Status(p *api.CommitStatusPayload) (DingtalkPayload,
return createDingtalkPayload(text, text, "Status Changed", p.TargetURL), nil
}
+func (dingtalkConvertor) WorkflowRun(p *api.WorkflowRunPayload) (DingtalkPayload, error) {
+ text, _ := getWorkflowRunPayloadInfo(p, noneLinkFormatter, true)
+
+ return createDingtalkPayload(text, text, "Workflow Run", p.WorkflowRun.HTMLURL), nil
+}
+
func (dingtalkConvertor) WorkflowJob(p *api.WorkflowJobPayload) (DingtalkPayload, error) {
text, _ := getWorkflowJobPayloadInfo(p, noneLinkFormatter, true)
diff --git a/services/webhook/discord.go b/services/webhook/discord.go
index 7b68430508..0426964181 100644
--- a/services/webhook/discord.go
+++ b/services/webhook/discord.go
@@ -278,6 +278,12 @@ func (d discordConvertor) Status(p *api.CommitStatusPayload) (DiscordPayload, er
return d.createPayload(p.Sender, text, "", p.TargetURL, color), nil
}
+func (d discordConvertor) WorkflowRun(p *api.WorkflowRunPayload) (DiscordPayload, error) {
+ text, color := getWorkflowRunPayloadInfo(p, noneLinkFormatter, false)
+
+ return d.createPayload(p.Sender, text, "", p.WorkflowRun.HTMLURL, color), nil
+}
+
func (d discordConvertor) WorkflowJob(p *api.WorkflowJobPayload) (DiscordPayload, error) {
text, color := getWorkflowJobPayloadInfo(p, noneLinkFormatter, false)
diff --git a/services/webhook/feishu.go b/services/webhook/feishu.go
index 274aaf90b3..b6ee80c44c 100644
--- a/services/webhook/feishu.go
+++ b/services/webhook/feishu.go
@@ -5,9 +5,13 @@ package webhook
import (
"context"
+ "crypto/hmac"
+ "crypto/sha256"
+ "encoding/base64"
"fmt"
"net/http"
"strings"
+ "time"
webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/git"
@@ -16,10 +20,12 @@ import (
)
type (
- // FeishuPayload represents
+ // FeishuPayload represents the payload for Feishu webhook
FeishuPayload struct {
- MsgType string `json:"msg_type"` // text / post / image / share_chat / interactive / file /audio / media
- Content struct {
+ Timestamp int64 `json:"timestamp,omitempty"` // Unix timestamp for signature verification
+ Sign string `json:"sign,omitempty"` // Signature for verification
+ MsgType string `json:"msg_type"` // text / post / image / share_chat / interactive / file /audio / media
+ Content struct {
Text string `json:"text"`
} `json:"content"`
}
@@ -172,15 +178,41 @@ func (fc feishuConvertor) Status(p *api.CommitStatusPayload) (FeishuPayload, err
return newFeishuTextPayload(text), nil
}
+func (feishuConvertor) WorkflowRun(p *api.WorkflowRunPayload) (FeishuPayload, error) {
+ text, _ := getWorkflowRunPayloadInfo(p, noneLinkFormatter, true)
+
+ return newFeishuTextPayload(text), nil
+}
+
func (feishuConvertor) WorkflowJob(p *api.WorkflowJobPayload) (FeishuPayload, error) {
text, _ := getWorkflowJobPayloadInfo(p, noneLinkFormatter, true)
return newFeishuTextPayload(text), nil
}
+// feishuGenSign generates a signature for Feishu webhook
+// https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot
+func feishuGenSign(secret string, timestamp int64) string {
+ // key="{timestamp}\n{secret}", then hmac-sha256, then base64 encode
+ stringToSign := fmt.Sprintf("%d\n%s", timestamp, secret)
+ h := hmac.New(sha256.New, []byte(stringToSign))
+ return base64.StdEncoding.EncodeToString(h.Sum(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)
+ payload, err := newPayload(feishuConvertor{}, []byte(t.PayloadContent), t.EventType)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ // Add timestamp and signature if secret is provided
+ if w.Secret != "" {
+ timestamp := time.Now().Unix()
+ payload.Timestamp = timestamp
+ payload.Sign = feishuGenSign(w.Secret, timestamp)
+ }
+
+ return prepareJSONRequest(payload, w, t, false /* no default headers */)
}
func init() {
diff --git a/services/webhook/feishu_test.go b/services/webhook/feishu_test.go
index c4249bdb30..7e200ea132 100644
--- a/services/webhook/feishu_test.go
+++ b/services/webhook/feishu_test.go
@@ -168,6 +168,7 @@ func TestFeishuJSONPayload(t *testing.T) {
URL: "https://feishu.example.com/",
Meta: `{}`,
HTTPMethod: "POST",
+ Secret: "secret",
}
task := &webhook_model.HookTask{
HookID: hook.ID,
@@ -183,10 +184,13 @@ func TestFeishuJSONPayload(t *testing.T) {
assert.Equal(t, "POST", req.Method)
assert.Equal(t, "https://feishu.example.com/", req.URL.String())
- assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
var body FeishuPayload
err = json.NewDecoder(req.Body).Decode(&body)
assert.NoError(t, err)
assert.Equal(t, "[test/repo:test] \r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", body.Content.Text)
+ assert.Equal(t, feishuGenSign(hook.Secret, body.Timestamp), body.Sign)
+
+ // a separate sign test, the result is generated by official python code, so the algo must be correct
+ assert.Equal(t, "rWZ84lcag1x9aBFhn1gtV4ZN+4gme3pilfQNMk86vKg=", feishuGenSign("a", 1))
}
diff --git a/services/webhook/general.go b/services/webhook/general.go
index 251659e75e..be457e46f5 100644
--- a/services/webhook/general.go
+++ b/services/webhook/general.go
@@ -327,6 +327,37 @@ func getStatusPayloadInfo(p *api.CommitStatusPayload, linkFormatter linkFormatte
return text, color
}
+func getWorkflowRunPayloadInfo(p *api.WorkflowRunPayload, linkFormatter linkFormatter, withSender bool) (text string, color int) {
+ description := p.WorkflowRun.Conclusion
+ if description == "" {
+ description = p.WorkflowRun.Status
+ }
+ refLink := linkFormatter(p.WorkflowRun.HTMLURL, fmt.Sprintf("%s(#%d)", p.WorkflowRun.DisplayTitle, p.WorkflowRun.ID)+"["+base.ShortSha(p.WorkflowRun.HeadSha)+"]:"+description)
+
+ text = fmt.Sprintf("Workflow Run %s: %s", p.Action, refLink)
+ switch description {
+ case "waiting":
+ color = orangeColor
+ case "queued":
+ color = orangeColorLight
+ case "success":
+ color = greenColor
+ case "failure":
+ color = redColor
+ case "cancelled":
+ color = yellowColor
+ case "skipped":
+ color = purpleColor
+ default:
+ color = greyColor
+ }
+ if withSender {
+ text += " by " + linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)
+ }
+
+ return text, color
+}
+
func getWorkflowJobPayloadInfo(p *api.WorkflowJobPayload, linkFormatter linkFormatter, withSender bool) (text string, color int) {
description := p.WorkflowJob.Conclusion
if description == "" {
diff --git a/services/webhook/matrix.go b/services/webhook/matrix.go
index 5bc7ba097e..3e9163f78c 100644
--- a/services/webhook/matrix.go
+++ b/services/webhook/matrix.go
@@ -252,6 +252,12 @@ func (m matrixConvertor) Status(p *api.CommitStatusPayload) (MatrixPayload, erro
return m.newPayload(text)
}
+func (m matrixConvertor) WorkflowRun(p *api.WorkflowRunPayload) (MatrixPayload, error) {
+ text, _ := getWorkflowRunPayloadInfo(p, htmlLinkFormatter, true)
+
+ return m.newPayload(text)
+}
+
func (m matrixConvertor) WorkflowJob(p *api.WorkflowJobPayload) (MatrixPayload, error) {
text, _ := getWorkflowJobPayloadInfo(p, htmlLinkFormatter, true)
diff --git a/services/webhook/msteams.go b/services/webhook/msteams.go
index 07d28c3867..450a544b42 100644
--- a/services/webhook/msteams.go
+++ b/services/webhook/msteams.go
@@ -318,6 +318,20 @@ func (m msteamsConvertor) Status(p *api.CommitStatusPayload) (MSTeamsPayload, er
), nil
}
+func (msteamsConvertor) WorkflowRun(p *api.WorkflowRunPayload) (MSTeamsPayload, error) {
+ title, color := getWorkflowRunPayloadInfo(p, noneLinkFormatter, false)
+
+ return createMSTeamsPayload(
+ p.Repo,
+ p.Sender,
+ title,
+ "",
+ p.WorkflowRun.HTMLURL,
+ color,
+ &MSTeamsFact{"WorkflowRun:", p.WorkflowRun.DisplayTitle},
+ ), nil
+}
+
func (msteamsConvertor) WorkflowJob(p *api.WorkflowJobPayload) (MSTeamsPayload, error) {
title, color := getWorkflowJobPayloadInfo(p, noneLinkFormatter, false)
diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go
index 9e3f21de29..dc44460860 100644
--- a/services/webhook/notifier.go
+++ b/services/webhook/notifier.go
@@ -5,10 +5,8 @@ package webhook
import (
"context"
- "fmt"
actions_model "code.gitea.io/gitea/models/actions"
- "code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/organization"
@@ -18,6 +16,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/repository"
@@ -956,72 +955,61 @@ func (*webhookNotifier) WorkflowJobStatusUpdate(ctx context.Context, repo *repo_
org = convert.ToOrganization(ctx, organization.OrgFromUser(repo.Owner))
}
- err := job.LoadAttributes(ctx)
+ status, _ := convert.ToActionsStatus(job.Status)
+
+ convertedJob, err := convert.ToActionWorkflowJob(ctx, repo, task, job)
if err != nil {
- log.Error("Error loading job attributes: %v", err)
+ log.Error("ToActionWorkflowJob: %v", err)
return
}
- jobIndex := 0
- jobs, err := actions_model.GetRunJobsByRunID(ctx, job.RunID)
+ if err := PrepareWebhooks(ctx, source, webhook_module.HookEventWorkflowJob, &api.WorkflowJobPayload{
+ Action: status,
+ WorkflowJob: convertedJob,
+ Organization: org,
+ Repo: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}),
+ Sender: convert.ToUser(ctx, sender, nil),
+ }); err != nil {
+ log.Error("PrepareWebhooks: %v", err)
+ }
+}
+
+func (*webhookNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, run *actions_model.ActionRun) {
+ source := EventSource{
+ Repository: repo,
+ Owner: repo.Owner,
+ }
+
+ var org *api.Organization
+ if repo.Owner.IsOrganization() {
+ org = convert.ToOrganization(ctx, organization.OrgFromUser(repo.Owner))
+ }
+
+ status := convert.ToWorkflowRunAction(run.Status)
+
+ gitRepo, err := gitrepo.OpenRepository(ctx, repo)
if err != nil {
- log.Error("Error loading getting run jobs: %v", err)
+ log.Error("OpenRepository: %v", err)
return
}
- for i, j := range jobs {
- if j.ID == job.ID {
- jobIndex = i
- break
- }
- }
+ defer gitRepo.Close()
- status, conclusion := toActionStatus(job.Status)
- var runnerID int64
- var runnerName string
- var steps []*api.ActionWorkflowStep
+ convertedWorkflow, err := convert.GetActionWorkflow(ctx, gitRepo, repo, run.WorkflowID)
+ if err != nil {
+ log.Error("GetActionWorkflow: %v", err)
+ return
+ }
- if task != nil {
- runnerID = task.RunnerID
- if runner, ok, _ := db.GetByID[actions_model.ActionRunner](ctx, runnerID); ok {
- runnerName = runner.Name
- }
- for i, step := range task.Steps {
- stepStatus, stepConclusion := toActionStatus(job.Status)
- steps = append(steps, &api.ActionWorkflowStep{
- Name: step.Name,
- Number: int64(i),
- Status: stepStatus,
- Conclusion: stepConclusion,
- StartedAt: step.Started.AsTime().UTC(),
- CompletedAt: step.Stopped.AsTime().UTC(),
- })
- }
+ convertedRun, err := convert.ToActionWorkflowRun(ctx, repo, run)
+ if err != nil {
+ log.Error("ToActionWorkflowRun: %v", err)
+ return
}
- if err := PrepareWebhooks(ctx, source, webhook_module.HookEventWorkflowJob, &api.WorkflowJobPayload{
- Action: status,
- WorkflowJob: &api.ActionWorkflowJob{
- ID: job.ID,
- // missing api endpoint for this location
- URL: fmt.Sprintf("%s/actions/runs/%d/jobs/%d", repo.APIURL(), job.RunID, job.ID),
- HTMLURL: fmt.Sprintf("%s/jobs/%d", job.Run.HTMLURL(), jobIndex),
- RunID: job.RunID,
- // Missing api endpoint for this location, artifacts are available under a nested url
- RunURL: fmt.Sprintf("%s/actions/runs/%d", repo.APIURL(), job.RunID),
- Name: job.Name,
- Labels: job.RunsOn,
- RunAttempt: job.Attempt,
- HeadSha: job.Run.CommitSHA,
- HeadBranch: git.RefName(job.Run.Ref).BranchName(),
- Status: status,
- Conclusion: conclusion,
- RunnerID: runnerID,
- RunnerName: runnerName,
- Steps: steps,
- CreatedAt: job.Created.AsTime().UTC(),
- StartedAt: job.Started.AsTime().UTC(),
- CompletedAt: job.Stopped.AsTime().UTC(),
- },
+ if err := PrepareWebhooks(ctx, source, webhook_module.HookEventWorkflowRun, &api.WorkflowRunPayload{
+ Action: status,
+ Workflow: convertedWorkflow,
+ WorkflowRun: convertedRun,
Organization: org,
Repo: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}),
Sender: convert.ToUser(ctx, sender, nil),
@@ -1029,29 +1017,3 @@ func (*webhookNotifier) WorkflowJobStatusUpdate(ctx context.Context, repo *repo_
log.Error("PrepareWebhooks: %v", err)
}
}
-
-func toActionStatus(status actions_model.Status) (string, string) {
- var action string
- var conclusion string
- switch status {
- // This is a naming conflict of the webhook between Gitea and GitHub Actions
- case actions_model.StatusWaiting:
- action = "queued"
- case actions_model.StatusBlocked:
- action = "waiting"
- case actions_model.StatusRunning:
- action = "in_progress"
- }
- if status.IsDone() {
- action = "completed"
- switch status {
- case actions_model.StatusSuccess:
- conclusion = "success"
- case actions_model.StatusCancelled:
- conclusion = "cancelled"
- case actions_model.StatusFailure:
- conclusion = "failure"
- }
- }
- return action, conclusion
-}
diff --git a/services/webhook/packagist.go b/services/webhook/packagist.go
index 8829d95da6..e6a00b0293 100644
--- a/services/webhook/packagist.go
+++ b/services/webhook/packagist.go
@@ -114,6 +114,10 @@ func (pc packagistConvertor) Status(_ *api.CommitStatusPayload) (PackagistPayloa
return PackagistPayload{}, nil
}
+func (pc packagistConvertor) WorkflowRun(_ *api.WorkflowRunPayload) (PackagistPayload, error) {
+ return PackagistPayload{}, nil
+}
+
func (pc packagistConvertor) WorkflowJob(_ *api.WorkflowJobPayload) (PackagistPayload, error) {
return PackagistPayload{}, nil
}
diff --git a/services/webhook/payloader.go b/services/webhook/payloader.go
index adb7243fb1..b607bf3250 100644
--- a/services/webhook/payloader.go
+++ b/services/webhook/payloader.go
@@ -29,6 +29,7 @@ type payloadConvertor[T any] interface {
Wiki(*api.WikiPayload) (T, error)
Package(*api.PackagePayload) (T, error)
Status(*api.CommitStatusPayload) (T, error)
+ WorkflowRun(*api.WorkflowRunPayload) (T, error)
WorkflowJob(*api.WorkflowJobPayload) (T, error)
}
@@ -81,6 +82,8 @@ func newPayload[T any](rc payloadConvertor[T], data []byte, event webhook_module
return convertUnmarshalledJSON(rc.Package, data)
case webhook_module.HookEventStatus:
return convertUnmarshalledJSON(rc.Status, data)
+ case webhook_module.HookEventWorkflowRun:
+ return convertUnmarshalledJSON(rc.WorkflowRun, data)
case webhook_module.HookEventWorkflowJob:
return convertUnmarshalledJSON(rc.WorkflowJob, data)
}
@@ -92,7 +95,10 @@ func newJSONRequest[T any](pc payloadConvertor[T], w *webhook_model.Webhook, t *
if err != nil {
return nil, nil, err
}
+ return prepareJSONRequest(payload, w, t, withDefaultHeaders)
+}
+func prepareJSONRequest[T any](payload T, w *webhook_model.Webhook, t *webhook_model.HookTask, withDefaultHeaders bool) (*http.Request, []byte, error) {
body, err := json.MarshalIndent(payload, "", " ")
if err != nil {
return nil, nil, err
diff --git a/services/webhook/slack.go b/services/webhook/slack.go
index 589ef3fe9b..3d645a55d0 100644
--- a/services/webhook/slack.go
+++ b/services/webhook/slack.go
@@ -173,6 +173,12 @@ func (s slackConvertor) Status(p *api.CommitStatusPayload) (SlackPayload, error)
return s.createPayload(text, nil), nil
}
+func (s slackConvertor) WorkflowRun(p *api.WorkflowRunPayload) (SlackPayload, error) {
+ text, _ := getWorkflowRunPayloadInfo(p, SlackLinkFormatter, true)
+
+ return s.createPayload(text, nil), nil
+}
+
func (s slackConvertor) WorkflowJob(p *api.WorkflowJobPayload) (SlackPayload, error) {
text, _ := getWorkflowJobPayloadInfo(p, SlackLinkFormatter, true)
diff --git a/services/webhook/telegram.go b/services/webhook/telegram.go
index ca74eabe1c..ae195758b9 100644
--- a/services/webhook/telegram.go
+++ b/services/webhook/telegram.go
@@ -180,6 +180,12 @@ func (t telegramConvertor) Status(p *api.CommitStatusPayload) (TelegramPayload,
return createTelegramPayloadHTML(text), nil
}
+func (telegramConvertor) WorkflowRun(p *api.WorkflowRunPayload) (TelegramPayload, error) {
+ text, _ := getWorkflowRunPayloadInfo(p, htmlLinkFormatter, true)
+
+ return createTelegramPayloadHTML(text), nil
+}
+
func (telegramConvertor) WorkflowJob(p *api.WorkflowJobPayload) (TelegramPayload, error) {
text, _ := getWorkflowJobPayloadInfo(p, htmlLinkFormatter, true)
diff --git a/services/webhook/wechatwork.go b/services/webhook/wechatwork.go
index 2b19822caf..1875317406 100644
--- a/services/webhook/wechatwork.go
+++ b/services/webhook/wechatwork.go
@@ -181,6 +181,12 @@ func (wc wechatworkConvertor) Status(p *api.CommitStatusPayload) (WechatworkPayl
return newWechatworkMarkdownPayload(text), nil
}
+func (wc wechatworkConvertor) WorkflowRun(p *api.WorkflowRunPayload) (WechatworkPayload, error) {
+ text, _ := getWorkflowRunPayloadInfo(p, noneLinkFormatter, true)
+
+ return newWechatworkMarkdownPayload(text), nil
+}
+
func (wc wechatworkConvertor) WorkflowJob(p *api.WorkflowJobPayload) (WechatworkPayload, error) {
text, _ := getWorkflowJobPayloadInfo(p, noneLinkFormatter, true)