From 381ca50d2f8b1ff4d7ec9de2db291a2b5a23e7ba Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Tue, 14 Nov 2023 20:56:55 +0800 Subject: [PATCH 1/2] Add workflow run webhook --- models/webhook/webhook.go | 53 -------------------------- models/webhook/webhook_list.go | 68 ++++++++++++++++++++++++++++++++++ modules/structs/hook.go | 27 ++++++++++++++ modules/structs/workflow.go | 22 +++++++++++ 4 files changed, 117 insertions(+), 53 deletions(-) create mode 100644 models/webhook/webhook_list.go create mode 100644 modules/structs/workflow.go diff --git a/models/webhook/webhook.go b/models/webhook/webhook.go index b28cea6a9cc9c..03c9160e296c1 100644 --- a/models/webhook/webhook.go +++ b/models/webhook/webhook.go @@ -380,17 +380,6 @@ func CreateWebhook(ctx context.Context, w *Webhook) error { return db.Insert(ctx, w) } -// CreateWebhooks creates multiple web hooks -func CreateWebhooks(ctx context.Context, ws []*Webhook) error { - // xorm returns err "no element on slice when insert" for empty slices. - if len(ws) == 0 { - return nil - } - for i := 0; i < len(ws); i++ { - ws[i].Type = strings.TrimSpace(ws[i].Type) - } - return db.Insert(ctx, ws) -} // getWebhook uses argument bean as query condition, // ID must be specified and do not assign unnecessary fields. @@ -427,48 +416,6 @@ func GetWebhookByOwnerID(ctx context.Context, ownerID, id int64) (*Webhook, erro }) } -// ListWebhookOptions are options to filter webhooks on ListWebhooksByOpts -type ListWebhookOptions struct { - db.ListOptions - RepoID int64 - OwnerID int64 - IsActive util.OptionalBool -} - -func (opts *ListWebhookOptions) toCond() builder.Cond { - cond := builder.NewCond() - if opts.RepoID != 0 { - cond = cond.And(builder.Eq{"webhook.repo_id": opts.RepoID}) - } - if opts.OwnerID != 0 { - cond = cond.And(builder.Eq{"webhook.owner_id": opts.OwnerID}) - } - if !opts.IsActive.IsNone() { - cond = cond.And(builder.Eq{"webhook.is_active": opts.IsActive.IsTrue()}) - } - return cond -} - -// ListWebhooksByOpts return webhooks based on options -func ListWebhooksByOpts(ctx context.Context, opts *ListWebhookOptions) ([]*Webhook, error) { - sess := db.GetEngine(ctx).Where(opts.toCond()) - - if opts.Page != 0 { - sess = db.SetSessionPagination(sess, opts) - webhooks := make([]*Webhook, 0, opts.PageSize) - err := sess.Find(&webhooks) - return webhooks, err - } - - webhooks := make([]*Webhook, 0, 10) - err := sess.Find(&webhooks) - return webhooks, err -} - -// CountWebhooksByOpts count webhooks based on options and ignore pagination -func CountWebhooksByOpts(ctx context.Context, opts *ListWebhookOptions) (int64, error) { - return db.GetEngine(ctx).Where(opts.toCond()).Count(&Webhook{}) -} // UpdateWebhook updates information of webhook. func UpdateWebhook(ctx context.Context, w *Webhook) error { diff --git a/models/webhook/webhook_list.go b/models/webhook/webhook_list.go new file mode 100644 index 0000000000000..bc7c8ef8d3463 --- /dev/null +++ b/models/webhook/webhook_list.go @@ -0,0 +1,68 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package webhook + +import ( + "context" + "strings" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/util" + "xorm.io/builder" +) + +// CreateWebhooks creates multiple web hooks +func CreateWebhooks(ctx context.Context, ws []*Webhook) error { + // xorm returns err "no element on slice when insert" for empty slices. + if len(ws) == 0 { + return nil + } + for i := 0; i < len(ws); i++ { + ws[i].Type = strings.TrimSpace(ws[i].Type) + } + return db.Insert(ctx, ws) +} + +// ListWebhookOptions are options to filter webhooks on ListWebhooksByOpts +type ListWebhookOptions struct { + db.ListOptions + RepoID int64 + OwnerID int64 + IsActive util.OptionalBool +} + +func (opts *ListWebhookOptions) toCond() builder.Cond { + cond := builder.NewCond() + if opts.RepoID != 0 { + cond = cond.And(builder.Eq{"webhook.repo_id": opts.RepoID}) + } + if opts.OwnerID != 0 { + cond = cond.And(builder.Eq{"webhook.owner_id": opts.OwnerID}) + } + if !opts.IsActive.IsNone() { + cond = cond.And(builder.Eq{"webhook.is_active": opts.IsActive.IsTrue()}) + } + return cond +} + +// ListWebhooksByOpts return webhooks based on options +func ListWebhooksByOpts(ctx context.Context, opts *ListWebhookOptions) ([]*Webhook, error) { + sess := db.GetEngine(ctx).Where(opts.toCond()) + + if opts.Page != 0 { + sess = db.SetSessionPagination(sess, opts) + webhooks := make([]*Webhook, 0, opts.PageSize) + err := sess.Find(&webhooks) + return webhooks, err + } + + webhooks := make([]*Webhook, 0, 10) + err := sess.Find(&webhooks) + return webhooks, err +} + +// CountWebhooksByOpts count webhooks based on options and ignore pagination +func CountWebhooksByOpts(ctx context.Context, opts *ListWebhookOptions) (int64, error) { + return db.GetEngine(ctx).Where(opts.toCond()).Count(&Webhook{}) +} diff --git a/modules/structs/hook.go b/modules/structs/hook.go index 0babe844101a1..32f1e6f434cf9 100644 --- a/modules/structs/hook.go +++ b/modules/structs/hook.go @@ -494,3 +494,30 @@ type PackagePayload struct { func (p *PackagePayload) JSONPayload() ([]byte, error) { return json.MarshalIndent(p, "", " ") } + +// HookWorkflowRunAction an action that happens to a workflow run +type HookWorkflowRunAction string + +const ( + // HookWorkflowRunCompleted completed + HookWorkflowRunCompleted HookWorkflowRunAction = "completed" + // HookWorkflowRunInProgress in progress + HookWorkflowRunInProgress HookWorkflowRunAction = "in_progress" + // HookWorkflowRunRequested deleted + HookWorkflowRunRequested HookWorkflowRunAction = "requested" +) + +// HookWorkflowRunPayload represents a package payload +type HookWorkflowRunPayload struct { + Action HookWorkflowRunAction `json:"action"` + Repository *Repository `json:"repository"` + Workflow *Workflow `json:"workflow"` + WorkflowRun *WorkflowRun `json:"workflow_run"` + Organization *User `json:"organization"` + Sender *User `json:"sender"` +} + +// JSONPayload implements Payload +func (p *HookWorkflowRunPayload) JSONPayload() ([]byte, error) { + return json.MarshalIndent(p, "", " ") +} diff --git a/modules/structs/workflow.go b/modules/structs/workflow.go new file mode 100644 index 0000000000000..7c0aa2c081fbd --- /dev/null +++ b/modules/structs/workflow.go @@ -0,0 +1,22 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package structs + +type Workflow struct { + BadgeURL string `json:"badge_url"` + CreatedAt string `json:"created_at"` + HTMLURL string `json:"html_url"` + ID int64 `json:"id"` + Name string `json:"name"` + NodeID string `json:"node_id"` + Path string `json:"path"` + State string `json:"state"` + UpdatedAt string `json:"updated_at"` + URL string `json:"url"` +} + +type WorkflowRun struct { + Actor *User `json:"actor"` + +} From 97625ac4a05ba98f1b87c1625681aacaf7309e38 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Tue, 14 Nov 2023 22:00:45 +0800 Subject: [PATCH 2/2] Add more structs --- modules/structs/hook.go | 6 +++--- modules/structs/workflow.go | 32 +++++++++++++++++++++++++--- modules/webhook/type.go | 1 + services/convert/workflow.go | 41 ++++++++++++++++++++++++++++++++++++ services/notify/notifier.go | 3 +++ services/notify/null.go | 3 +++ services/webhook/notifier.go | 27 ++++++++++++++++++++++++ 7 files changed, 107 insertions(+), 6 deletions(-) create mode 100644 services/convert/workflow.go diff --git a/modules/structs/hook.go b/modules/structs/hook.go index 32f1e6f434cf9..3e2ef267d7536 100644 --- a/modules/structs/hook.go +++ b/modules/structs/hook.go @@ -507,8 +507,8 @@ const ( HookWorkflowRunRequested HookWorkflowRunAction = "requested" ) -// HookWorkflowRunPayload represents a package payload -type HookWorkflowRunPayload struct { +// WorkflowJobPayload represents a package payload +type WorkflowJobPayload struct { Action HookWorkflowRunAction `json:"action"` Repository *Repository `json:"repository"` Workflow *Workflow `json:"workflow"` @@ -518,6 +518,6 @@ type HookWorkflowRunPayload struct { } // JSONPayload implements Payload -func (p *HookWorkflowRunPayload) JSONPayload() ([]byte, error) { +func (p *WorkflowJobPayload) JSONPayload() ([]byte, error) { return json.MarshalIndent(p, "", " ") } diff --git a/modules/structs/workflow.go b/modules/structs/workflow.go index 7c0aa2c081fbd..1343f9cd8e159 100644 --- a/modules/structs/workflow.go +++ b/modules/structs/workflow.go @@ -3,13 +3,14 @@ package structs +// ref https://docs.github.com/en/webhooks/webhook-events-and-payloads#workflow_run + type Workflow struct { BadgeURL string `json:"badge_url"` CreatedAt string `json:"created_at"` HTMLURL string `json:"html_url"` ID int64 `json:"id"` Name string `json:"name"` - NodeID string `json:"node_id"` Path string `json:"path"` State string `json:"state"` UpdatedAt string `json:"updated_at"` @@ -17,6 +18,31 @@ type Workflow struct { } type WorkflowRun struct { - Actor *User `json:"actor"` - + Actor *User `json:"actor"` + ArtifactsURL string `json:"artifacts_url"` + CancelURL string `json:"cancel_url"` + Conclusion string `json:"conclusion"` // Can be one of: success, failure, neutral, cancelled, timed_out, action_required, stale, null, skipped + CreatedAt string `json:"created_at"` + Event string `json:"event"` + HeadBranch string `json:"head_branch"` + HeadCommit *Commit `json:"head_commit"` + HeadRepository *Repository `json:"head_repository"` + HeadSHA string `json:"head_sha"` + HTMLURL string `json:"html_url"` + ID int64 `json:"id"` + JobsURL string `json:"jobs_url"` + LogsURL string `json:"logs_url"` + Name string `json:"name"` + Path string `json:"path"` + PullRequests []*PullRequest `json:"pull_requests"` + Repository *Repository `json:"repository"` + ReRunURL string `json:"rerun_url"` + RunNumber int64 `json:"run_number"` + RunStartedAt string `json:"run_started_at"` + Status string `json:"status"` // Can be one of: requested, in_progress, completed, queued, pending, waiting + TriggeringActor *User `json:"triggering_actor"` + UpdatedAt string `json:"updated_at"` + URL string `json:"url"` + WorkflowID int64 `json:"workflow_id"` + WorkflowURL string `json:"workflow_url"` } diff --git a/modules/webhook/type.go b/modules/webhook/type.go index 7042d391b7eb4..2fd6d528417e5 100644 --- a/modules/webhook/type.go +++ b/modules/webhook/type.go @@ -31,6 +31,7 @@ const ( HookEventRepository HookEventType = "repository" HookEventRelease HookEventType = "release" HookEventPackage HookEventType = "package" + HookEventWorkflowJob HookEventType = "workflow_job" ) // Event returns the HookEventType as an event string diff --git a/services/convert/workflow.go b/services/convert/workflow.go new file mode 100644 index 0000000000000..c28f81854e68b --- /dev/null +++ b/services/convert/workflow.go @@ -0,0 +1,41 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package convert + +import ( + "context" + + actions_model "code.gitea.io/gitea/models/actions" + user_model "code.gitea.io/gitea/models/user" +) + +func ToWorkflowJob(ctx context.Context, doer *user_model.User, job *actions_model.ActionRunJob, run *actions_model.ActionRun) *WorkflowJob { + return &WorkflowJob{ + Actor: &User{ + Identity: Identity{ + Name: doer.Name, + Email: doer.Email, + }, + AvatarURL: doer.AvatarLink(), + Login: doer.Name, + URL: doer.HTMLURL(), + }, + Commit: &Commit{}, + } +} + +func ToWorkflowRun(ctx context.Context, doer *user_model.User, job *actions_model.ActionRunJob, run *actions_model.ActionRun) *WorkflowRun { + return &WorkflowRun{ + Actor: &User{ + Identity: Identity{ + Name: doer.Name, + Email: doer.Email, + }, + AvatarURL: doer.AvatarLink(), + Login: doer.Name, + URL: doer.HTMLURL(), + }, + Commit: &Commit{}, + } +} diff --git a/services/notify/notifier.go b/services/notify/notifier.go index ed053a812a6fd..7dcdc4efc8170 100644 --- a/services/notify/notifier.go +++ b/services/notify/notifier.go @@ -6,6 +6,7 @@ package notify import ( "context" + actions_model "code.gitea.io/gitea/models/actions" issues_model "code.gitea.io/gitea/models/issues" packages_model "code.gitea.io/gitea/models/packages" repo_model "code.gitea.io/gitea/models/repo" @@ -74,4 +75,6 @@ type Notifier interface { PackageDelete(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor) ChangeDefaultBranch(ctx context.Context, repo *repo_model.Repository) + + WorkflowJob(ctx context.Context, doer *user_model.User, run *actions_model.ActionRun, job *actions_model.ActionRunJob) } diff --git a/services/notify/null.go b/services/notify/null.go index dddd421bef926..71db0150220e0 100644 --- a/services/notify/null.go +++ b/services/notify/null.go @@ -6,6 +6,7 @@ package notify import ( "context" + actions_model "code.gitea.io/gitea/models/actions" issues_model "code.gitea.io/gitea/models/issues" packages_model "code.gitea.io/gitea/models/packages" repo_model "code.gitea.io/gitea/models/repo" @@ -208,3 +209,5 @@ func (*NullNotifier) PackageDelete(ctx context.Context, doer *user_model.User, p // ChangeDefaultBranch places a place holder function func (*NullNotifier) ChangeDefaultBranch(ctx context.Context, repo *repo_model.Repository) { } + +func (*NullNotifier) WorkflowJob(ctx context.Context, doer *user_model.User, run *actions_model.ActionRun, job *actions_model.ActionRunJob, action api.HookWorkflowRunAction) diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go index 1ab14fd6a7e32..7d3e023dd8485 100644 --- a/services/webhook/notifier.go +++ b/services/webhook/notifier.go @@ -6,6 +6,7 @@ package webhook import ( "context" + actions_model "code.gitea.io/gitea/models/actions" issues_model "code.gitea.io/gitea/models/issues" packages_model "code.gitea.io/gitea/models/packages" "code.gitea.io/gitea/models/perm" @@ -887,3 +888,29 @@ func notifyPackage(ctx context.Context, sender *user_model.User, pd *packages_mo log.Error("PrepareWebhooks: %v", err) } } + +func (m *webhookNotifier) WorkflowJob(ctx context.Context, doer *user_model.User, run *actions_model.ActionRun, job *actions_model.ActionRunJob, action api.HookWorkflowRunAction) { + source := EventSource{ + Repository: run.Repo, + Owner: run.Repo.Owner, + } + + perm, err := access_model.GetUserRepoPermission(ctx, run.Repo, doer) + if err != nil { + log.Error("GetUserRepoPermission: %v", err) + return + } + + apiRepo := convert.ToRepo(ctx, run.Repo, perm) + apiJob := convert.ToWorkflowJob(ctx, doer, job, run) + apiRun := convert.ToWorkflowRun(ctx, doer, job, run) + if err := PrepareWebhooks(ctx, source, webhook_module.HookEventWorkflowJob, &api.WorkflowJobPayload{ + Action: action, + Repository: apiRepo, + Workflow: apiJob, + WorkflowRun: apiRun, + Sender: convert.ToUser(ctx, doer, nil), + }); err != nil { + log.Error("PrepareWebhooks: %v", err) + } +}