diff --git a/modules/structs/hook.go b/modules/structs/hook.go index cef2dbd712..ed35c24cc6 100644 --- a/modules/structs/hook.go +++ b/modules/structs/hook.go @@ -469,3 +469,18 @@ type CommitStatusPayload struct { func (p *CommitStatusPayload) JSONPayload() ([]byte, error) { return json.MarshalIndent(p, "", " ") } + +// WorkflowJobPayload represents a payload information of workflow job event. +type WorkflowJobPayload struct { + Action string `json:"action"` + WorkflowJob *ActionWorkflowJob `json:"workflow_job"` + PullRequest *PullRequest `json:"pull_request,omitempty"` + Organization *Organization `json:"organization,omitempty"` + Repository *Repository `json:"repository"` + Sender *User `json:"sender"` +} + +// JSONPayload implements Payload +func (p *WorkflowJobPayload) JSONPayload() ([]byte, error) { + return json.MarshalIndent(p, "", " ") +} diff --git a/modules/structs/repo_actions.go b/modules/structs/repo_actions.go index 203491ac02..b595d218ba 100644 --- a/modules/structs/repo_actions.go +++ b/modules/structs/repo_actions.go @@ -96,3 +96,36 @@ type ActionArtifactsResponse struct { Entries []*ActionArtifact `json:"artifacts"` TotalCount int64 `json:"total_count"` } + +// ActionWorkflowRun represents a WorkflowJob +type ActionWorkflowStep struct { + Name string `json:"name"` + Number int64 `json:"number"` + Conclusion string `json:"conclusion,omitempty"` + // swagger:strfmt date-time + StartedAt time.Time `json:"started_at,omitempty"` + // swagger:strfmt date-time + CompletedAt time.Time `json:"completed_at,omitempty"` +} + +// ActionWorkflowJob represents a WorkflowJob +type ActionWorkflowJob struct { + ID int64 `json:"id"` + RunID int64 `json:"run_id"` + RunURL string `json:"run_url"` + Name string `json:"name"` + Labels []string `json:"labels"` + RunAttempt int64 `json:"run_attempt"` + HeadSha string `json:"head_sha"` + HeadBranch string `json:"head_branch,omitempty"` + Conclusion string `json:"conclusion,omitempty"` + RunnerID int64 `json:"runner_id"` + RunnerName string `json:"runner_name"` + Steps []*ActionWorkflowStep `json:"steps"` + // swagger:strfmt date-time + CreatedAt time.Time `json:"created_at"` + // swagger:strfmt date-time + StartedAt time.Time `json:"started_at,omitempty"` + // swagger:strfmt date-time + CompletedAt time.Time `json:"completed_at,omitempty"` +} diff --git a/modules/webhook/type.go b/modules/webhook/type.go index b244bb0cff..72ffde26a1 100644 --- a/modules/webhook/type.go +++ b/modules/webhook/type.go @@ -37,7 +37,8 @@ const ( // FIXME: This event should be a group of pull_request_review_xxx events HookEventPullRequestReview HookEventType = "pull_request_review" // Actions event only - HookEventSchedule HookEventType = "schedule" + HookEventSchedule HookEventType = "schedule" + HookEventWorkflowJob HookEventType = "workflow_job" ) func AllEvents() []HookEventType { @@ -66,6 +67,7 @@ func AllEvents() []HookEventType { HookEventRelease, HookEventPackage, HookEventStatus, + HookEventWorkflowJob, } } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index c2c5b07b65..7948e37d2b 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -2379,6 +2379,8 @@ settings.event_pull_request_review_request = Pull Request Review Requested settings.event_pull_request_review_request_desc = Pull request review requested or review request removed. settings.event_pull_request_approvals = Pull Request Approvals settings.event_pull_request_merge = Pull Request Merge +settings.event_workflow_job = Workflow Job +settings.event_workflow_job_desc = Gitea Actions Workflow Job queued, started or completed. settings.event_package = Package settings.event_package_desc = Package created or deleted in a repository. settings.branch_filter = Branch filter diff --git a/routers/api/actions/runner/runner.go b/routers/api/actions/runner/runner.go index f34dfb443b..aa7ab06130 100644 --- a/routers/api/actions/runner/runner.go +++ b/routers/api/actions/runner/runner.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/util" actions_service "code.gitea.io/gitea/services/actions" + notifier "code.gitea.io/gitea/services/notify" runnerv1 "code.gitea.io/actions-proto-go/runner/v1" "code.gitea.io/actions-proto-go/runner/v1/runnerv1connect" @@ -210,7 +211,7 @@ func (s *Service) UpdateTask( if err := task.LoadJob(ctx); err != nil { return nil, status.Errorf(codes.Internal, "load job: %v", err) } - if err := task.Job.LoadRun(ctx); err != nil { + if err := task.Job.LoadAttributes(ctx); err != nil { return nil, status.Errorf(codes.Internal, "load run: %v", err) } @@ -224,7 +225,9 @@ func (s *Service) UpdateTask( log.Error("Emit ready jobs of run %d: %v", task.Job.RunID, err) } } - + if task.Status.IsDone() { + notifier.CreateWorkflowJob(ctx, task.Job.Run.Repo, task.Job.Run.TriggerUser, task.Job, task) + } return connect.NewResponse(&runnerv1.UpdateTaskResponse{ State: &runnerv1.TaskState{ Id: req.Msg.State.Id, diff --git a/routers/api/v1/utils/hook.go b/routers/api/v1/utils/hook.go index 9c49819970..ce0c1b5097 100644 --- a/routers/api/v1/utils/hook.go +++ b/routers/api/v1/utils/hook.go @@ -207,6 +207,7 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoI webhook_module.HookEventRelease: util.SliceContainsString(form.Events, string(webhook_module.HookEventRelease), true), webhook_module.HookEventPackage: util.SliceContainsString(form.Events, string(webhook_module.HookEventPackage), true), webhook_module.HookEventStatus: util.SliceContainsString(form.Events, string(webhook_module.HookEventStatus), true), + webhook_module.HookEventWorkflowJob: util.SliceContainsString(form.Events, string(webhook_module.HookEventWorkflowJob), true), }, BranchFilter: form.BranchFilter, }, diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index b27f8e0e7a..4215fd1fbf 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -33,6 +33,7 @@ import ( "code.gitea.io/gitea/modules/web" actions_service "code.gitea.io/gitea/services/actions" context_module "code.gitea.io/gitea/services/context" + notifier "code.gitea.io/gitea/services/notify" "github.com/nektos/act/pkg/model" "xorm.io/builder" @@ -458,6 +459,9 @@ func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob, shou } actions_service.CreateCommitStatus(ctx, job) + _ = job.LoadAttributes(ctx) + notifier.CreateWorkflowJob(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) + return nil } @@ -518,6 +522,8 @@ func Cancel(ctx *context_module.Context) { return } + var updatedjobs []*actions_model.ActionRunJob + if err := db.WithTx(ctx, func(ctx context.Context) error { for _, job := range jobs { status := job.Status @@ -534,6 +540,9 @@ func Cancel(ctx *context_module.Context) { if n == 0 { return fmt.Errorf("job has changed, try again") } + if n > 0 { + updatedjobs = append(updatedjobs, job) + } continue } if err := actions_model.StopTask(ctx, job.TaskID, actions_model.StatusCancelled); err != nil { @@ -548,6 +557,11 @@ func Cancel(ctx *context_module.Context) { actions_service.CreateCommitStatus(ctx, jobs...) + for _, job := range updatedjobs { + _ = job.LoadAttributes(ctx) + notifier.CreateWorkflowJob(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) + } + ctx.JSON(http.StatusOK, struct{}{}) } @@ -561,6 +575,8 @@ func Approve(ctx *context_module.Context) { run := current.Run doer := ctx.Doer + var updatedjobs []*actions_model.ActionRunJob + if err := db.WithTx(ctx, func(ctx context.Context) error { run.NeedApproval = false run.ApprovedBy = doer.ID @@ -570,10 +586,13 @@ func Approve(ctx *context_module.Context) { for _, job := range jobs { if len(job.Needs) == 0 && job.Status.IsBlocked() { job.Status = actions_model.StatusWaiting - _, err := actions_model.UpdateRunJob(ctx, job, nil, "status") + n, err := actions_model.UpdateRunJob(ctx, job, nil, "status") if err != nil { return err } + if n > 0 { + updatedjobs = append(updatedjobs, job) + } } } return nil @@ -584,6 +603,11 @@ func Approve(ctx *context_module.Context) { actions_service.CreateCommitStatus(ctx, jobs...) + for _, job := range updatedjobs { + _ = job.LoadAttributes(ctx) + notifier.CreateWorkflowJob(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) + } + ctx.JSON(http.StatusOK, struct{}{}) } diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go index 6875584d0b..d3151a86a2 100644 --- a/routers/web/repo/setting/webhook.go +++ b/routers/web/repo/setting/webhook.go @@ -185,6 +185,7 @@ func ParseHookEvent(form forms.WebhookForm) *webhook_module.HookEvent { webhook_module.HookEventRepository: form.Repository, webhook_module.HookEventPackage: form.Package, webhook_module.HookEventStatus: form.Status, + webhook_module.HookEventWorkflowJob: form.WorkflowJob, }, BranchFilter: form.BranchFilter, } diff --git a/services/actions/clear_tasks.go b/services/actions/clear_tasks.go index 67373782d5..f8b8978697 100644 --- a/services/actions/clear_tasks.go +++ b/services/actions/clear_tasks.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" + notifier "code.gitea.io/gitea/services/notify" ) // StopZombieTasks stops the task which have running status, but haven't been updated for a long time @@ -68,7 +69,10 @@ func stopTasks(ctx context.Context, opts actions_model.FindTaskOptions) error { } CreateCommitStatus(ctx, jobs...) - + for _, job := range jobs { + _ = job.LoadAttributes(ctx) + notifier.CreateWorkflowJob(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) + } return nil } @@ -87,14 +91,20 @@ func CancelAbandonedJobs(ctx context.Context) error { for _, job := range jobs { job.Status = actions_model.StatusCancelled job.Stopped = now + updated := false if err := db.WithTx(ctx, func(ctx context.Context) error { - _, err := actions_model.UpdateRunJob(ctx, job, nil, "status", "stopped") + n, err := actions_model.UpdateRunJob(ctx, job, nil, "status", "stopped") + updated = err == nil && n > 0 return err }); err != nil { log.Warn("cancel abandoned job %v: %v", job.ID, err) // go on } CreateCommitStatus(ctx, job) + if updated { + _ = job.LoadAttributes(ctx) + notifier.CreateWorkflowJob(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) + } } return nil diff --git a/services/actions/job_emitter.go b/services/actions/job_emitter.go index 1f859fcf70..4bbce6078a 100644 --- a/services/actions/job_emitter.go +++ b/services/actions/job_emitter.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/queue" + notifier "code.gitea.io/gitea/services/notify" "github.com/nektos/act/pkg/jobparser" "xorm.io/builder" @@ -49,6 +50,7 @@ func checkJobsOfRun(ctx context.Context, runID int64) error { if err != nil { return err } + var updatedjobs []*actions_model.ActionRunJob if err := db.WithTx(ctx, func(ctx context.Context) error { idToJobs := make(map[string][]*actions_model.ActionRunJob, len(jobs)) for _, job := range jobs { @@ -64,6 +66,7 @@ func checkJobsOfRun(ctx context.Context, runID int64) error { } else if n != 1 { return fmt.Errorf("no affected for updating blocked job %v", job.ID) } + updatedjobs = append(updatedjobs, job) } } return nil @@ -71,6 +74,10 @@ func checkJobsOfRun(ctx context.Context, runID int64) error { return err } CreateCommitStatus(ctx, jobs...) + for _, job := range updatedjobs { + _ = job.LoadAttributes(ctx) + notifier.CreateWorkflowJob(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) + } return nil } diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index 2d8885dc32..99cf03ce16 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -27,6 +27,7 @@ import ( api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" "code.gitea.io/gitea/services/convert" + notifier "code.gitea.io/gitea/services/notify" "github.com/nektos/act/pkg/jobparser" "github.com/nektos/act/pkg/model" @@ -363,6 +364,9 @@ func handleWorkflows( continue } CreateCommitStatus(ctx, alljobs...) + for _, job := range alljobs { + notifier.CreateWorkflowJob(ctx, input.Repo, input.Doer, job, nil) + } } return nil } diff --git a/services/actions/schedule_tasks.go b/services/actions/schedule_tasks.go index 18f3324fd2..cea79ee32a 100644 --- a/services/actions/schedule_tasks.go +++ b/services/actions/schedule_tasks.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/timeutil" webhook_module "code.gitea.io/gitea/modules/webhook" + notifier "code.gitea.io/gitea/services/notify" "github.com/nektos/act/pkg/jobparser" ) @@ -148,6 +149,17 @@ func CreateScheduleTask(ctx context.Context, cron *actions_model.ActionSchedule) if err := actions_model.InsertRun(ctx, run, workflows); err != nil { return err } + allJobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: run.ID}) + if err != nil { + log.Error("FindRunJobs: %v", err) + } + err = run.LoadAttributes(ctx) + if err != nil { + log.Error("LoadAttributes: %v", err) + } + for _, job := range allJobs { + notifier.CreateWorkflowJob(ctx, run.Repo, run.TriggerUser, job, nil) + } // Return nil if no errors occurred return nil diff --git a/services/actions/task.go b/services/actions/task.go index bc54ade347..51d1e9d143 100644 --- a/services/actions/task.go +++ b/services/actions/task.go @@ -12,13 +12,15 @@ import ( secret_model "code.gitea.io/gitea/models/secret" runnerv1 "code.gitea.io/actions-proto-go/runner/v1" + notifier "code.gitea.io/gitea/services/notify" "google.golang.org/protobuf/types/known/structpb" ) func PickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv1.Task, bool, error) { var ( - task *runnerv1.Task - job *actions_model.ActionRunJob + task *runnerv1.Task + job *actions_model.ActionRunJob + actionTask *actions_model.ActionTask ) if err := db.WithTx(ctx, func(ctx context.Context) error { @@ -34,6 +36,7 @@ func PickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv return fmt.Errorf("task LoadAttributes: %w", err) } job = t.Job + actionTask = t secrets, err := secret_model.GetSecretsOfTask(ctx, t) if err != nil { @@ -74,6 +77,7 @@ func PickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv } CreateCommitStatus(ctx, job) + notifier.CreateWorkflowJob(ctx, job.Run.Repo, job.Run.TriggerUser, job, actionTask) return task, true, nil } diff --git a/services/actions/workflow.go b/services/actions/workflow.go index 9aecad171b..d3326f3c54 100644 --- a/services/actions/workflow.go +++ b/services/actions/workflow.go @@ -10,6 +10,8 @@ import ( "path" "strings" + notifier "code.gitea.io/gitea/services/notify" + actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/perm" @@ -276,6 +278,9 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re log.Error("FindRunJobs: %v", err) } CreateCommitStatus(ctx, allJobs...) + for _, job := range allJobs { + notifier.CreateWorkflowJob(ctx, repo, doer, job, nil) + } return nil } diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 70019f3fa9..f07f4f2261 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -244,6 +244,7 @@ type WebhookForm struct { Release bool Package bool Status bool + WorkflowJob bool Active bool BranchFilter string `binding:"GlobPattern"` AuthorizationHeader string diff --git a/services/notify/notifier.go b/services/notify/notifier.go index 29bbb5702b..450ac878ea 100644 --- a/services/notify/notifier.go +++ b/services/notify/notifier.go @@ -6,6 +6,8 @@ package notify import ( "context" + "code.gitea.io/gitea/models/actions" + actions_model "code.gitea.io/gitea/models/actions" git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" packages_model "code.gitea.io/gitea/models/packages" @@ -77,4 +79,6 @@ type Notifier interface { ChangeDefaultBranch(ctx context.Context, repo *repo_model.Repository) CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit *repository.PushCommit, sender *user_model.User, status *git_model.CommitStatus) + + CreateWorkflowJob(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, job *actions.ActionRunJob, task *actions_model.ActionTask) } diff --git a/services/notify/notify.go b/services/notify/notify.go index c97d0fcbaf..6bc5e053e3 100644 --- a/services/notify/notify.go +++ b/services/notify/notify.go @@ -6,6 +6,7 @@ package notify import ( "context" + actions_model "code.gitea.io/gitea/models/actions" git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" packages_model "code.gitea.io/gitea/models/packages" @@ -374,3 +375,9 @@ func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit notifier.CreateCommitStatus(ctx, repo, commit, sender, status) } } + +func CreateWorkflowJob(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, job *actions_model.ActionRunJob, task *actions_model.ActionTask) { + for _, notifier := range notifiers { + notifier.CreateWorkflowJob(ctx, repo, sender, job, task) + } +} diff --git a/services/notify/null.go b/services/notify/null.go index 7354efd701..54b8270049 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" git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" packages_model "code.gitea.io/gitea/models/packages" @@ -212,3 +213,6 @@ func (*NullNotifier) ChangeDefaultBranch(ctx context.Context, repo *repo_model.R func (*NullNotifier) CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit *repository.PushCommit, sender *user_model.User, status *git_model.CommitStatus) { } + +func (*NullNotifier) CreateWorkflowJob(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, job *actions_model.ActionRunJob, task *actions_model.ActionTask) { +} diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go index 76d6fd3472..7483cc9261 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" git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" @@ -941,3 +942,86 @@ func notifyPackage(ctx context.Context, sender *user_model.User, pd *packages_mo log.Error("PrepareWebhooks: %v", err) } } + +func (*webhookNotifier) CreateWorkflowJob(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, job *actions_model.ActionRunJob, task *actions_model.ActionTask) { + source := EventSource{ + Repository: repo, + Owner: repo.Owner, + } + + var org *api.Organization + if repo.Owner.IsOrganization() { + org = convert.ToOrganization(ctx, organization.OrgFromUser(repo.Owner)) + } + + job.LoadAttributes(ctx) + + action, conclusion := toActionStatus(job.Status) + var runnerId int64 + var steps []*api.ActionWorkflowStep + + if task != nil { + runnerId = task.RunnerID + for i, step := range task.Steps { + _, stepConclusion := toActionStatus(job.Status) + steps = append(steps, &api.ActionWorkflowStep{ + Name: step.Name, + Number: int64(i), + Conclusion: stepConclusion, + StartedAt: step.Started.AsTime().UTC(), + CompletedAt: step.Stopped.AsTime().UTC(), + }) + } + } + + if err := PrepareWebhooks(ctx, source, webhook_module.HookEventWorkflowJob, &api.WorkflowJobPayload{ + Action: action, + WorkflowJob: &api.ActionWorkflowJob{ + ID: job.ID, + RunID: job.RunID, + RunURL: job.Run.HTMLURL(), + Name: job.Name, + Labels: job.RunsOn, + RunAttempt: job.Attempt, + HeadSha: job.Run.CommitSHA, + HeadBranch: git.RefName(job.Run.Ref).BranchName(), + Conclusion: conclusion, + RunnerID: runnerId, + Steps: steps, + CreatedAt: job.Created.AsTime().UTC(), + StartedAt: job.Started.AsTime().UTC(), + CompletedAt: job.Stopped.AsTime().UTC(), + }, + Organization: org, + Repository: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}), + Sender: convert.ToUser(ctx, sender, nil), + }); err != nil { + 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/templates/repo/settings/webhook/settings.tmpl b/templates/repo/settings/webhook/settings.tmpl index 3b28a4c6c0..d6f7feb22d 100644 --- a/templates/repo/settings/webhook/settings.tmpl +++ b/templates/repo/settings/webhook/settings.tmpl @@ -259,6 +259,16 @@ + +