Skip to content

WIP: Add workflow run webhook #28047

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 0 additions & 53 deletions models/webhook/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down
68 changes: 68 additions & 0 deletions models/webhook/webhook_list.go
Original file line number Diff line number Diff line change
@@ -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{})
}
27 changes: 27 additions & 0 deletions modules/structs/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

// WorkflowJobPayload represents a package payload
type WorkflowJobPayload 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 *WorkflowJobPayload) JSONPayload() ([]byte, error) {
return json.MarshalIndent(p, "", " ")
}
48 changes: 48 additions & 0 deletions modules/structs/workflow.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

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"`
Path string `json:"path"`
State string `json:"state"`
UpdatedAt string `json:"updated_at"`
URL string `json:"url"`
}

type WorkflowRun struct {
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"`
}
1 change: 1 addition & 0 deletions modules/webhook/type.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 41 additions & 0 deletions services/convert/workflow.go
Original file line number Diff line number Diff line change
@@ -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{},
}
}
3 changes: 3 additions & 0 deletions services/notify/notifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
3 changes: 3 additions & 0 deletions services/notify/null.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
27 changes: 27 additions & 0 deletions services/webhook/notifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
}