From edb24592fce1943b08f7acf13e64ec02d60d82ee Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Fri, 14 Mar 2025 21:07:36 +0100 Subject: [PATCH 01/37] wip event --- modules/structs/hook.go | 16 ++++++++ modules/structs/repo_actions.go | 12 ++++++ modules/webhook/type.go | 1 + services/actions/clear_tasks.go | 3 ++ services/actions/notifier.go | 11 ++++++ services/notify/notifier.go | 2 + services/notify/notify.go | 6 +++ services/notify/null.go | 3 ++ services/webhook/notifier.go | 39 +++++++++++++++++++ templates/repo/settings/webhook/settings.tmpl | 9 +++++ 10 files changed, 102 insertions(+) diff --git a/modules/structs/hook.go b/modules/structs/hook.go index aaa9fbc9d3..cd0eef851a 100644 --- a/modules/structs/hook.go +++ b/modules/structs/hook.go @@ -470,6 +470,22 @@ func (p *CommitStatusPayload) JSONPayload() ([]byte, error) { return json.MarshalIndent(p, "", " ") } +// WorkflowRunPayload represents a payload information of workflow run event. +type WorkflowRunPayload struct { + Action string `json:"action"` + Workflow *ActionWorkflow `json:"workflow"` + WorkflowRun *ActionWorkflowRun `json:"workflow_run"` + PullRequest *PullRequest `json:"pull_request,omitempty"` + Organization *Organization `json:"organization,omitempty"` + Repo *Repository `json:"repository"` + Sender *User `json:"sender"` +} + +// JSONPayload implements Payload +func (p *WorkflowRunPayload) JSONPayload() ([]byte, error) { + return json.MarshalIndent(p, "", " ") +} + // WorkflowJobPayload represents a payload information of workflow job event. type WorkflowJobPayload struct { Action string `json:"action"` diff --git a/modules/structs/repo_actions.go b/modules/structs/repo_actions.go index 22409b4aff..23bdb46d1b 100644 --- a/modules/structs/repo_actions.go +++ b/modules/structs/repo_actions.go @@ -87,8 +87,20 @@ type ActionArtifact struct { // ActionWorkflowRun represents a WorkflowRun type ActionWorkflowRun struct { ID int64 `json:"id"` + URL string `json:"url"` + HTMLURL string `json:"html_url"` + Event string `json:"event"` + RunAttempt int64 `json:"run_attempt"` + RunNumber int64 `json:"run_number"` RepositoryID int64 `json:"repository_id"` HeadSha string `json:"head_sha"` + HeadBranch string `json:"head_branch,omitempty"` + Status string `json:"status"` + 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"` } // ActionArtifactsResponse returns ActionArtifacts diff --git a/modules/webhook/type.go b/modules/webhook/type.go index 72ffde26a1..bcf8903b2b 100644 --- a/modules/webhook/type.go +++ b/modules/webhook/type.go @@ -38,6 +38,7 @@ const ( HookEventPullRequestReview HookEventType = "pull_request_review" // Actions event only HookEventSchedule HookEventType = "schedule" + HookEventWorkflowRun HookEventType = "workflow_run" HookEventWorkflowJob HookEventType = "workflow_job" ) diff --git a/services/actions/clear_tasks.go b/services/actions/clear_tasks.go index 2aeb0e8c96..bfa10563bc 100644 --- a/services/actions/clear_tasks.go +++ b/services/actions/clear_tasks.go @@ -125,6 +125,9 @@ func CancelAbandonedJobs(ctx context.Context) error { if updated { _ = job.LoadAttributes(ctx) notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) + if job.Run != nil { + notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) + } } } diff --git a/services/actions/notifier.go b/services/actions/notifier.go index 1a23b4e0c5..1040607a56 100644 --- a/services/actions/notifier.go +++ b/services/actions/notifier.go @@ -6,6 +6,7 @@ package actions 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" perm_model "code.gitea.io/gitea/models/perm" @@ -762,3 +763,13 @@ func (n *actionsNotifier) MigrateRepository(ctx context.Context, doer, u *user_m Sender: convert.ToUser(ctx, doer, nil), }).Notify(ctx) } + +func (n *actionsNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, run *actions_model.ActionRun) { + run.Status.IsBlocked() + newNotifyInput(repo, sender, webhook_module.HookEventWorkflowRun).WithPayload(&api.WorkflowRunPayload{ + Action: "queued", + Repo: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm_model.AccessModeOwner}), + Organization: nil, + Sender: convert.ToUser(ctx, sender, nil), + }).Notify(ctx) +} diff --git a/services/notify/notifier.go b/services/notify/notifier.go index 40428454be..875a70e564 100644 --- a/services/notify/notifier.go +++ b/services/notify/notifier.go @@ -79,5 +79,7 @@ type Notifier interface { CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit *repository.PushCommit, sender *user_model.User, status *git_model.CommitStatus) + WorkflowRunStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, run *actions_model.ActionRun) + WorkflowJobStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, job *actions_model.ActionRunJob, task *actions_model.ActionTask) } diff --git a/services/notify/notify.go b/services/notify/notify.go index 9f8be4b577..0c6fdf9cef 100644 --- a/services/notify/notify.go +++ b/services/notify/notify.go @@ -376,6 +376,12 @@ func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit } } +func WorkflowRunStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, run *actions_model.ActionRun) { + for _, notifier := range notifiers { + notifier.WorkflowRunStatusUpdate(ctx, repo, sender, run) + } +} + func WorkflowJobStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, job *actions_model.ActionRunJob, task *actions_model.ActionTask) { for _, notifier := range notifiers { notifier.WorkflowJobStatusUpdate(ctx, repo, sender, job, task) diff --git a/services/notify/null.go b/services/notify/null.go index 9c794a2342..c3085d7c9e 100644 --- a/services/notify/null.go +++ b/services/notify/null.go @@ -214,5 +214,8 @@ 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) WorkflowRunStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, run *actions_model.ActionRun) { +} + func (*NullNotifier) WorkflowJobStatusUpdate(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 7d779cd527..84842ea374 100644 --- a/services/webhook/notifier.go +++ b/services/webhook/notifier.go @@ -1030,6 +1030,45 @@ func (*webhookNotifier) WorkflowJobStatusUpdate(ctx context.Context, repo *repo_ } } +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, conclusion := toActionStatus(run.Status) + + if err := PrepareWebhooks(ctx, source, webhook_module.HookEventWorkflowJob, &api.WorkflowRunPayload{ + Action: status, + Workflow: nil, + WorkflowRun: &api.ActionWorkflowRun{ + ID: run.ID, + RunNumber: run.Index, + HTMLURL: run.HTMLURL(), + // Missing api endpoint for this location, artifacts are available under a nested url + URL: fmt.Sprintf("%s/actions/runs/%d", repo.APIURL(), run.ID), + Event: run.TriggerEvent, + RunAttempt: 0, + HeadSha: run.CommitSHA, + HeadBranch: git.RefName(run.Ref).BranchName(), + Status: status, + Conclusion: conclusion, + StartedAt: run.Started.AsTime().UTC(), + CompletedAt: run.Stopped.AsTime().UTC(), + }, + 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 toActionStatus(status actions_model.Status) (string, string) { var action string var conclusion string diff --git a/templates/repo/settings/webhook/settings.tmpl b/templates/repo/settings/webhook/settings.tmpl index 16ad263e42..5a69bd737c 100644 --- a/templates/repo/settings/webhook/settings.tmpl +++ b/templates/repo/settings/webhook/settings.tmpl @@ -264,6 +264,15 @@ +
+
+
+ + + {{ctx.Locale.Tr "repo.settings.event_workflow_run_desc"}} +
+
+
From 013a0af3853b10c03a472a93db169fd9dacb434c Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Mon, 17 Mar 2025 17:27:25 +0100 Subject: [PATCH 02/37] wip --- modules/structs/repo_actions.go | 16 +- routers/api/v1/api.go | 2 + routers/api/v1/repo/action.go | 260 ++++++++++++++++++++++++++++++++ services/convert/convert.go | 113 ++++++++++++++ 4 files changed, 390 insertions(+), 1 deletion(-) diff --git a/modules/structs/repo_actions.go b/modules/structs/repo_actions.go index 23bdb46d1b..7c8de3d776 100644 --- a/modules/structs/repo_actions.go +++ b/modules/structs/repo_actions.go @@ -89,10 +89,12 @@ type ActionWorkflowRun struct { ID int64 `json:"id"` URL string `json:"url"` HTMLURL string `json:"html_url"` + DisplayTitle string `json:"display_title"` + Path string `json:"path"` Event string `json:"event"` RunAttempt int64 `json:"run_attempt"` RunNumber int64 `json:"run_number"` - RepositoryID int64 `json:"repository_id"` + RepositoryID int64 `json:"repository_id,omitempty"` HeadSha string `json:"head_sha"` HeadBranch string `json:"head_branch,omitempty"` Status string `json:"status"` @@ -103,6 +105,18 @@ type ActionWorkflowRun struct { CompletedAt time.Time `json:"completed_at,omitempty"` } +// ActionArtifactsResponse returns ActionArtifacts +type ActionWorkflowRunsResponse struct { + Entries []*ActionWorkflowRun `json:"workflow_runs"` + TotalCount int64 `json:"total_count"` +} + +// ActionArtifactsResponse returns ActionArtifacts +type ActionWorkflowJobsResponse struct { + Entries []*ActionWorkflowJob `json:"workflow_jobs"` + TotalCount int64 `json:"total_count"` +} + // ActionArtifactsResponse returns ActionArtifacts type ActionArtifactsResponse struct { Entries []*ActionArtifact `json:"artifacts"` diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index bc76b5285e..a89fb6303a 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1243,6 +1243,8 @@ func Routes() *web.Router { }, reqToken(), reqAdmin()) m.Group("/actions", func() { m.Get("/tasks", repo.ListActionTasks) + m.Get("/runs", repo.GetWorkflowRuns) + m.Get("/runs/{run}/jobs", repo.GetWorkflowJobs) m.Get("/runs/{run}/artifacts", repo.GetArtifactsOfRun) m.Get("/artifacts", repo.GetArtifacts) m.Group("/artifacts/{artifact_id}", func() { diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index 6b4ce37fcf..6463ae50d0 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -21,11 +21,13 @@ import ( repo_model "code.gitea.io/gitea/models/repo" secret_model "code.gitea.io/gitea/models/secret" "code.gitea.io/gitea/modules/actions" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/modules/webhook" "code.gitea.io/gitea/routers/api/v1/shared" "code.gitea.io/gitea/routers/api/v1/utils" actions_service "code.gitea.io/gitea/services/actions" @@ -868,6 +870,264 @@ func ActionsEnableWorkflow(ctx *context.APIContext) { ctx.Status(http.StatusNoContent) } +func convertToInternal(s string) actions_model.Status { + switch s { + case "pending": + return actions_model.StatusBlocked + case "queued": + return actions_model.StatusWaiting + case "in_progress": + return actions_model.StatusRunning + case "failure": + return actions_model.StatusFailure + case "success": + return actions_model.StatusSuccess + case "skipped": + return actions_model.StatusSkipped + default: + return actions_model.StatusUnknown + } +} + +// GetArtifacts Lists all artifacts for a repository. +func GetWorkflowRuns(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/runs repository getWorkflowRuns + // --- + // summary: Lists all runs for a repository run + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: name of the owner + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repository + // type: string + // required: true + // - name: run + // in: path + // description: runid of the workflow run + // type: integer + // required: true + // - name: name + // in: query + // description: name of the artifact + // type: string + // required: false + // responses: + // "200": + // "$ref": "#/responses/ArtifactsList" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + repoID := ctx.Repo.Repository.ID + + opts := actions_model.FindRunOptions{ + RepoID: repoID, + ListOptions: utils.GetListOptions(ctx), + } + + if event := ctx.Req.URL.Query().Get("event"); event != "" { + opts.TriggerEvent = webhook.HookEventType(event) + } + if branch := ctx.Req.URL.Query().Get("branch"); branch != "" { + opts.Ref = string(git.RefNameFromBranch(branch)) + } + if status := ctx.Req.URL.Query().Get("status"); status != "" { + opts.Status = []actions_model.Status{convertToInternal(status)} + } + // if actor := ctx.Req.URL.Query().Get("actor"); actor != "" { + // user_model. + // opts.TriggerUserID = + // } + + runs, total, err := db.FindAndCount[actions_model.ActionRun](ctx, opts) + if err != nil { + ctx.APIErrorInternal(err) + return + } + + res := new(api.ActionWorkflowRunsResponse) + res.TotalCount = total + + res.Entries = make([]*api.ActionWorkflowRun, len(runs)) + for i := range runs { + convertedRun, err := convert.ToActionWorkflowRun(ctx.Repo.Repository, runs[i]) + if err != nil { + ctx.APIErrorInternal(err) + return + } + res.Entries[i] = convertedRun + } + + ctx.JSON(http.StatusOK, &res) +} + +// GetWorkflowRun Gets a specific workflow run. +func GetWorkflowRun(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run} repository GetWorkflowRun + // --- + // summary: Gets a specific workflow run + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: name of the owner + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repository + // type: string + // required: true + // - name: run + // in: path + // description: id of the run + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/Artifact" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + runID := ctx.PathParamInt64("run") + job, _, _ := db.GetByID[actions_model.ActionRun](ctx, runID) + + if job.RepoID != ctx.Repo.Repository.ID { + ctx.APIError(http.StatusNotFound, util.ErrNotExist) + } + + convertedArtifact, err := convert.ToActionWorkflowRun(ctx.Repo.Repository, job) + if err != nil { + ctx.APIErrorInternal(err) + return + } + ctx.JSON(http.StatusOK, convertedArtifact) + return +} + +// GetWorkflowJobs Lists all jobs for a workflow run. +func GetWorkflowJobs(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run}/jobs repository getWorkflowJobs + // --- + // summary: Lists all jobs for a workflow run + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: name of the owner + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repository + // type: string + // required: true + // - name: run + // in: path + // description: runid of the workflow run + // type: integer + // required: true + // - name: name + // in: query + // description: name of the artifact + // type: string + // required: false + // responses: + // "200": + // "$ref": "#/responses/ArtifactsList" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + repoID := ctx.Repo.Repository.ID + + runID := ctx.PathParamInt64("run") + + artifacts, total, err := db.FindAndCount[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{ + RepoID: repoID, + RunID: runID, + ListOptions: utils.GetListOptions(ctx), + }) + if err != nil { + ctx.APIErrorInternal(err) + return + } + + res := new(api.ActionWorkflowJobsResponse) + res.TotalCount = total + + res.Entries = make([]*api.ActionWorkflowJob, len(artifacts)) + for i := range artifacts { + convertedWorkflowJob, err := convert.ToActionWorkflowJob(ctx, ctx.Repo.Repository, artifacts[i]) + if err != nil { + ctx.APIErrorInternal(err) + return + } + res.Entries[i] = convertedWorkflowJob + } + + ctx.JSON(http.StatusOK, &res) +} + +// GetWorkflowJob Gets a specific workflow job for a workflow run. +func GetWorkflowJob(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/jobs/{job_id} repository getWorkflowJob + // --- + // summary: Gets a specific workflow job for a workflow run + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: name of the owner + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repository + // type: string + // required: true + // - name: job_id + // in: path + // description: id of the job + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/Artifact" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + jobID := ctx.PathParamInt64("job_id") + job, _, _ := db.GetByID[actions_model.ActionRunJob](ctx, jobID) + + if job.RepoID != ctx.Repo.Repository.ID { + ctx.APIError(http.StatusNotFound, util.ErrNotExist) + } + + convertedWorkflowJob, err := convert.ToActionWorkflowJob(ctx, ctx.Repo.Repository, job) + if err != nil { + ctx.APIErrorInternal(err) + return + } + ctx.JSON(http.StatusOK, convertedWorkflowJob) + return +} + // GetArtifacts Lists all artifacts for a repository. func GetArtifactsOfRun(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run}/artifacts repository getArtifactsOfRun diff --git a/services/convert/convert.go b/services/convert/convert.go index ac2680766c..674e3f361f 100644 --- a/services/convert/convert.go +++ b/services/convert/convert.go @@ -14,6 +14,7 @@ import ( actions_model "code.gitea.io/gitea/models/actions" asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/auth" + "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" @@ -230,6 +231,118 @@ func ToActionTask(ctx context.Context, t *actions_model.ActionTask) (*api.Action }, nil } +func ToActionWorkflowRun(repo *repo_model.Repository, run *actions_model.ActionRun) (*api.ActionWorkflowRun, error) { + status, conclusion := toActionStatus(run.Status) + return &api.ActionWorkflowRun{ + ID: run.ID, + URL: fmt.Sprintf("%s/actions/runs/%d", repo.APIURL(), run.ID), + HTMLURL: run.HTMLURL(), + RunNumber: run.Index, + StartedAt: run.Started.AsLocalTime(), + CompletedAt: run.Stopped.AsLocalTime(), + Event: run.TriggerEvent, + DisplayTitle: run.Title, + HeadBranch: git.RefName(run.Ref).BranchName(), + HeadSha: run.CommitSHA, + Status: status, + Conclusion: conclusion, + Path: fmt.Sprint("%s@%s", run.WorkflowID, run.Ref), + }, nil +} + +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 +} + +func ToActionWorkflowJob(ctx context.Context, repo *repo_model.Repository, job *actions_model.ActionRunJob) (*api.ActionWorkflowJob, error) { + err := job.LoadAttributes(ctx) + if err != nil { + return nil, err + } + + jobIndex := 0 + jobs, err := actions_model.GetRunJobsByRunID(ctx, job.RunID) + if err != nil { + return nil, err + } + for i, j := range jobs { + if j.ID == job.ID { + jobIndex = i + break + } + } + + status, conclusion := toActionStatus(job.Status) + var runnerID int64 + var runnerName string + var steps []*api.ActionWorkflowStep + + if job.TaskID != 0 { + task, _, _ := db.GetByID[actions_model.ActionTask](ctx, job.TaskID) + + 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(), + }) + } + } + + return &api.ActionWorkflowJob{ + ID: job.ID, + // missing api endpoint for this location + URL: fmt.Sprintf("%s/actions/jobs/%d", repo.APIURL(), 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(), + }, nil +} + // ToActionArtifact convert a actions_model.ActionArtifact to an api.ActionArtifact func ToActionArtifact(repo *repo_model.Repository, art *actions_model.ActionArtifact) (*api.ActionArtifact, error) { url := fmt.Sprintf("%s/actions/artifacts/%d", repo.APIURL(), art.ID) From e93f9d20d1ecc58e02facb0cf802850ef380aa99 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Mon, 17 Mar 2025 18:04:44 +0100 Subject: [PATCH 03/37] update comment --- templates/repo/settings/webhook/settings.tmpl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/templates/repo/settings/webhook/settings.tmpl b/templates/repo/settings/webhook/settings.tmpl index 5a69bd737c..b8d9609391 100644 --- a/templates/repo/settings/webhook/settings.tmpl +++ b/templates/repo/settings/webhook/settings.tmpl @@ -263,7 +263,7 @@
- +
@@ -273,6 +273,7 @@
+
From 6701375e671b6baea257e2320a02c890952efe75 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Mon, 17 Mar 2025 19:59:01 +0100 Subject: [PATCH 04/37] add routes --- routers/api/v1/api.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index a89fb6303a..2023c94e6a 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1244,8 +1244,10 @@ func Routes() *web.Router { m.Group("/actions", func() { m.Get("/tasks", repo.ListActionTasks) m.Get("/runs", repo.GetWorkflowRuns) + m.Get("/runs/{run}", repo.GetWorkflowRun) m.Get("/runs/{run}/jobs", repo.GetWorkflowJobs) m.Get("/runs/{run}/artifacts", repo.GetArtifactsOfRun) + m.Get("/jobs/{job_id}", repo.GetWorkflowJob) m.Get("/artifacts", repo.GetArtifacts) m.Group("/artifacts/{artifact_id}", func() { m.Get("", repo.GetArtifact) From 988cafe7806333b7ecf6b71f1f9aec8c47b41fe3 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Mon, 17 Mar 2025 20:06:19 +0100 Subject: [PATCH 05/37] remove duplicated code --- services/webhook/notifier.go | 97 ++++++------------------------------ 1 file changed, 16 insertions(+), 81 deletions(-) diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go index 84842ea374..ece0e2d8a6 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" @@ -956,72 +954,17 @@ func (*webhookNotifier) WorkflowJobStatusUpdate(ctx context.Context, repo *repo_ org = convert.ToOrganization(ctx, organization.OrgFromUser(repo.Owner)) } - err := job.LoadAttributes(ctx) + status, _ := toActionStatus(job.Status) + + convertedJob, err := convert.ToActionWorkflowJob(ctx, repo, 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 != nil { - log.Error("Error loading getting run jobs: %v", err) - return - } - for i, j := range jobs { - if j.ID == job.ID { - jobIndex = i - break - } - } - - status, conclusion := toActionStatus(job.Status) - var runnerID int64 - var runnerName string - var steps []*api.ActionWorkflowStep - - 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(), - }) - } - } - 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(), - }, + Action: status, + WorkflowJob: convertedJob, Organization: org, Repo: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}), Sender: convert.ToUser(ctx, sender, nil), @@ -1041,26 +984,18 @@ func (*webhookNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *repo_ org = convert.ToOrganization(ctx, organization.OrgFromUser(repo.Owner)) } - status, conclusion := toActionStatus(run.Status) + status, _ := toActionStatus(run.Status) + + convertedRun, err := convert.ToActionWorkflowRun(repo, run) + if err != nil { + log.Error("ToActionWorkflowRun: %v", err) + return + } if err := PrepareWebhooks(ctx, source, webhook_module.HookEventWorkflowJob, &api.WorkflowRunPayload{ - Action: status, - Workflow: nil, - WorkflowRun: &api.ActionWorkflowRun{ - ID: run.ID, - RunNumber: run.Index, - HTMLURL: run.HTMLURL(), - // Missing api endpoint for this location, artifacts are available under a nested url - URL: fmt.Sprintf("%s/actions/runs/%d", repo.APIURL(), run.ID), - Event: run.TriggerEvent, - RunAttempt: 0, - HeadSha: run.CommitSHA, - HeadBranch: git.RefName(run.Ref).BranchName(), - Status: status, - Conclusion: conclusion, - StartedAt: run.Started.AsTime().UTC(), - CompletedAt: run.Stopped.AsTime().UTC(), - }, + Action: status, + Workflow: nil, + WorkflowRun: convertedRun, Organization: org, Repo: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}), Sender: convert.ToUser(ctx, sender, nil), From 20f5d6ee9580b0030da1c05ea661a7531ec4255c Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Mon, 17 Mar 2025 20:08:12 +0100 Subject: [PATCH 06/37] use ToActionsStatus --- services/convert/convert.go | 8 ++++---- services/webhook/notifier.go | 30 ++---------------------------- 2 files changed, 6 insertions(+), 32 deletions(-) diff --git a/services/convert/convert.go b/services/convert/convert.go index 674e3f361f..0a8fa3a477 100644 --- a/services/convert/convert.go +++ b/services/convert/convert.go @@ -232,7 +232,7 @@ func ToActionTask(ctx context.Context, t *actions_model.ActionTask) (*api.Action } func ToActionWorkflowRun(repo *repo_model.Repository, run *actions_model.ActionRun) (*api.ActionWorkflowRun, error) { - status, conclusion := toActionStatus(run.Status) + status, conclusion := ToActionsStatus(run.Status) return &api.ActionWorkflowRun{ ID: run.ID, URL: fmt.Sprintf("%s/actions/runs/%d", repo.APIURL(), run.ID), @@ -250,7 +250,7 @@ func ToActionWorkflowRun(repo *repo_model.Repository, run *actions_model.ActionR }, nil } -func toActionStatus(status actions_model.Status) (string, string) { +func ToActionsStatus(status actions_model.Status) (string, string) { var action string var conclusion string switch status { @@ -294,7 +294,7 @@ func ToActionWorkflowJob(ctx context.Context, repo *repo_model.Repository, job * } } - status, conclusion := toActionStatus(job.Status) + status, conclusion := ToActionsStatus(job.Status) var runnerID int64 var runnerName string var steps []*api.ActionWorkflowStep @@ -307,7 +307,7 @@ func ToActionWorkflowJob(ctx context.Context, repo *repo_model.Repository, job * runnerName = runner.Name } for i, step := range task.Steps { - stepStatus, stepConclusion := toActionStatus(job.Status) + stepStatus, stepConclusion := ToActionsStatus(job.Status) steps = append(steps, &api.ActionWorkflowStep{ Name: step.Name, Number: int64(i), diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go index ece0e2d8a6..95ef0d672e 100644 --- a/services/webhook/notifier.go +++ b/services/webhook/notifier.go @@ -954,7 +954,7 @@ func (*webhookNotifier) WorkflowJobStatusUpdate(ctx context.Context, repo *repo_ org = convert.ToOrganization(ctx, organization.OrgFromUser(repo.Owner)) } - status, _ := toActionStatus(job.Status) + status, _ := convert.ToActionsStatus(job.Status) convertedJob, err := convert.ToActionWorkflowJob(ctx, repo, job) if err != nil { @@ -984,7 +984,7 @@ func (*webhookNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *repo_ org = convert.ToOrganization(ctx, organization.OrgFromUser(repo.Owner)) } - status, _ := toActionStatus(run.Status) + status, _ := convert.ToActionsStatus(run.Status) convertedRun, err := convert.ToActionWorkflowRun(repo, run) if err != nil { @@ -1003,29 +1003,3 @@ func (*webhookNotifier) WorkflowRunStatusUpdate(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 -} From a178e4be7a8fcad47a3a1d4d0ecf70cfc449f6b6 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Mon, 17 Mar 2025 20:13:33 +0100 Subject: [PATCH 07/37] use cached task instance if available --- routers/api/v1/repo/action.go | 12 ++++++------ services/convert/convert.go | 11 +++++++++-- services/webhook/notifier.go | 2 +- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index 6463ae50d0..621f337a72 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -1000,9 +1000,9 @@ func GetWorkflowRun(ctx *context.APIContext) { // "$ref": "#/responses/notFound" runID := ctx.PathParamInt64("run") - job, _, _ := db.GetByID[actions_model.ActionRun](ctx, runID) + job, _, err := db.GetByID[actions_model.ActionRun](ctx, runID) - if job.RepoID != ctx.Repo.Repository.ID { + if err != nil || job.RepoID != ctx.Repo.Repository.ID { ctx.APIError(http.StatusNotFound, util.ErrNotExist) } @@ -1070,7 +1070,7 @@ func GetWorkflowJobs(ctx *context.APIContext) { res.Entries = make([]*api.ActionWorkflowJob, len(artifacts)) for i := range artifacts { - convertedWorkflowJob, err := convert.ToActionWorkflowJob(ctx, ctx.Repo.Repository, artifacts[i]) + convertedWorkflowJob, err := convert.ToActionWorkflowJob(ctx, ctx.Repo.Repository, nil, artifacts[i]) if err != nil { ctx.APIErrorInternal(err) return @@ -1113,13 +1113,13 @@ func GetWorkflowJob(ctx *context.APIContext) { // "$ref": "#/responses/notFound" jobID := ctx.PathParamInt64("job_id") - job, _, _ := db.GetByID[actions_model.ActionRunJob](ctx, jobID) + job, _, err := db.GetByID[actions_model.ActionRunJob](ctx, jobID) - if job.RepoID != ctx.Repo.Repository.ID { + if err != nil || job.RepoID != ctx.Repo.Repository.ID { ctx.APIError(http.StatusNotFound, util.ErrNotExist) } - convertedWorkflowJob, err := convert.ToActionWorkflowJob(ctx, ctx.Repo.Repository, job) + convertedWorkflowJob, err := convert.ToActionWorkflowJob(ctx, ctx.Repo.Repository, nil, job) if err != nil { ctx.APIErrorInternal(err) return diff --git a/services/convert/convert.go b/services/convert/convert.go index 0a8fa3a477..de1d445749 100644 --- a/services/convert/convert.go +++ b/services/convert/convert.go @@ -276,7 +276,9 @@ func ToActionsStatus(status actions_model.Status) (string, string) { return action, conclusion } -func ToActionWorkflowJob(ctx context.Context, repo *repo_model.Repository, job *actions_model.ActionRunJob) (*api.ActionWorkflowJob, error) { +// ToActionWorkflowJob convert a actions_model.ActionRunJob to an api.ActionWorkflowJob +// task is optional and can be nil +func ToActionWorkflowJob(ctx context.Context, repo *repo_model.Repository, task *actions_model.ActionTask, job *actions_model.ActionRunJob) (*api.ActionWorkflowJob, error) { err := job.LoadAttributes(ctx) if err != nil { return nil, err @@ -300,7 +302,12 @@ func ToActionWorkflowJob(ctx context.Context, repo *repo_model.Repository, job * var steps []*api.ActionWorkflowStep if job.TaskID != 0 { - task, _, _ := db.GetByID[actions_model.ActionTask](ctx, job.TaskID) + if task == nil { + task, _, err = db.GetByID[actions_model.ActionTask](ctx, job.TaskID) + if err != nil { + return nil, err + } + } runnerID = task.RunnerID if runner, ok, _ := db.GetByID[actions_model.ActionRunner](ctx, runnerID); ok { diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go index 95ef0d672e..5c47224b4a 100644 --- a/services/webhook/notifier.go +++ b/services/webhook/notifier.go @@ -956,7 +956,7 @@ func (*webhookNotifier) WorkflowJobStatusUpdate(ctx context.Context, repo *repo_ status, _ := convert.ToActionsStatus(job.Status) - convertedJob, err := convert.ToActionWorkflowJob(ctx, repo, job) + convertedJob, err := convert.ToActionWorkflowJob(ctx, repo, task, job) if err != nil { log.Error("ToActionWorkflowJob: %v", err) return From 295bf45d5cbe6de809260b3bf6908ad95618c6d8 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Mon, 17 Mar 2025 20:17:42 +0100 Subject: [PATCH 08/37] cleanup and fix webhook type bug --- services/actions/notifier.go | 23 ++++++++++++++++++++--- services/webhook/notifier.go | 2 +- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/services/actions/notifier.go b/services/actions/notifier.go index 1040607a56..f708bb8043 100644 --- a/services/actions/notifier.go +++ b/services/actions/notifier.go @@ -8,7 +8,9 @@ import ( actions_model "code.gitea.io/gitea/models/actions" issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/organization" packages_model "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/models/perm" perm_model "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" @@ -765,11 +767,26 @@ func (n *actionsNotifier) MigrateRepository(ctx context.Context, doer, u *user_m } func (n *actionsNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, run *actions_model.ActionRun) { + var org *api.Organization + if repo.Owner.IsOrganization() { + org = convert.ToOrganization(ctx, organization.OrgFromUser(repo.Owner)) + } + + status, _ := convert.ToActionsStatus(run.Status) + + convertedRun, err := convert.ToActionWorkflowRun(repo, run) + if err != nil { + log.Error("ToActionWorkflowRun: %v", err) + return + } + run.Status.IsBlocked() newNotifyInput(repo, sender, webhook_module.HookEventWorkflowRun).WithPayload(&api.WorkflowRunPayload{ - Action: "queued", - Repo: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm_model.AccessModeOwner}), - Organization: nil, + Action: status, + Workflow: nil, + WorkflowRun: convertedRun, + Organization: org, + Repo: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}), Sender: convert.ToUser(ctx, sender, nil), }).Notify(ctx) } diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go index 5c47224b4a..c0348243ab 100644 --- a/services/webhook/notifier.go +++ b/services/webhook/notifier.go @@ -992,7 +992,7 @@ func (*webhookNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *repo_ return } - if err := PrepareWebhooks(ctx, source, webhook_module.HookEventWorkflowJob, &api.WorkflowRunPayload{ + if err := PrepareWebhooks(ctx, source, webhook_module.HookEventWorkflowRun, &api.WorkflowRunPayload{ Action: status, Workflow: nil, WorkflowRun: convertedRun, From 392baec50b3e6fe2a33025ea72fa43b0d48898b0 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Mon, 17 Mar 2025 20:18:12 +0100 Subject: [PATCH 09/37] remove dead code --- services/actions/notifier.go | 1 - 1 file changed, 1 deletion(-) diff --git a/services/actions/notifier.go b/services/actions/notifier.go index f708bb8043..e0462542f2 100644 --- a/services/actions/notifier.go +++ b/services/actions/notifier.go @@ -780,7 +780,6 @@ func (n *actionsNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *rep return } - run.Status.IsBlocked() newNotifyInput(repo, sender, webhook_module.HookEventWorkflowRun).WithPayload(&api.WorkflowRunPayload{ Action: status, Workflow: nil, From fb8a221d7f4d8a57c1fbbd6505d14517545aa918 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Mon, 17 Mar 2025 20:40:27 +0100 Subject: [PATCH 10/37] Move GetActionWorkflow to convert --- services/actions/workflow.go | 96 +----------------------------------- services/convert/convert.go | 92 ++++++++++++++++++++++++++++++++++ services/webhook/notifier.go | 3 ++ 3 files changed, 96 insertions(+), 95 deletions(-) diff --git a/services/actions/workflow.go b/services/actions/workflow.go index dc8a1dd349..1eb82c5570 100644 --- a/services/actions/workflow.go +++ b/services/actions/workflow.go @@ -5,9 +5,6 @@ package actions import ( "fmt" - "net/http" - "net/url" - "path" "strings" actions_model "code.gitea.io/gitea/models/actions" @@ -31,61 +28,8 @@ import ( "github.com/nektos/act/pkg/model" ) -func getActionWorkflowPath(commit *git.Commit) string { - paths := []string{".gitea/workflows", ".github/workflows"} - for _, treePath := range paths { - if _, err := commit.SubTree(treePath); err == nil { - return treePath - } - } - return "" -} - -func getActionWorkflowEntry(ctx *context.APIContext, commit *git.Commit, folder string, entry *git.TreeEntry) *api.ActionWorkflow { - cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions) - cfg := cfgUnit.ActionsConfig() - - defaultBranch, _ := commit.GetBranchName() - - workflowURL := fmt.Sprintf("%s/actions/workflows/%s", ctx.Repo.Repository.APIURL(), url.PathEscape(entry.Name())) - workflowRepoURL := fmt.Sprintf("%s/src/branch/%s/%s/%s", ctx.Repo.Repository.HTMLURL(ctx), util.PathEscapeSegments(defaultBranch), util.PathEscapeSegments(folder), url.PathEscape(entry.Name())) - badgeURL := fmt.Sprintf("%s/actions/workflows/%s/badge.svg?branch=%s", ctx.Repo.Repository.HTMLURL(ctx), url.PathEscape(entry.Name()), url.QueryEscape(ctx.Repo.Repository.DefaultBranch)) - - // See https://docs.github.com/en/rest/actions/workflows?apiVersion=2022-11-28#get-a-workflow - // State types: - // - active - // - deleted - // - disabled_fork - // - disabled_inactivity - // - disabled_manually - state := "active" - if cfg.IsWorkflowDisabled(entry.Name()) { - state = "disabled_manually" - } - - // The CreatedAt and UpdatedAt fields currently reflect the timestamp of the latest commit, which can later be refined - // by retrieving the first and last commits for the file history. The first commit would indicate the creation date, - // while the last commit would represent the modification date. The DeletedAt could be determined by identifying - // the last commit where the file existed. However, this implementation has not been done here yet, as it would likely - // cause a significant performance degradation. - createdAt := commit.Author.When - updatedAt := commit.Author.When - - return &api.ActionWorkflow{ - ID: entry.Name(), - Name: entry.Name(), - Path: path.Join(folder, entry.Name()), - State: state, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - URL: workflowURL, - HTMLURL: workflowRepoURL, - BadgeURL: badgeURL, - } -} - func EnableOrDisableWorkflow(ctx *context.APIContext, workflowID string, isEnable bool) error { - workflow, err := GetActionWorkflow(ctx, workflowID) + workflow, err := convert.GetActionWorkflow(ctx, ctx.Repo.GitRepo, ctx.Repo.Repository, workflowID) if err != nil { return err } @@ -102,44 +46,6 @@ func EnableOrDisableWorkflow(ctx *context.APIContext, workflowID string, isEnabl return repo_model.UpdateRepoUnit(ctx, cfgUnit) } -func ListActionWorkflows(ctx *context.APIContext) ([]*api.ActionWorkflow, error) { - defaultBranchCommit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) - if err != nil { - ctx.APIErrorInternal(err) - return nil, err - } - - entries, err := actions.ListWorkflows(defaultBranchCommit) - if err != nil { - ctx.APIError(http.StatusNotFound, err.Error()) - return nil, err - } - - folder := getActionWorkflowPath(defaultBranchCommit) - - workflows := make([]*api.ActionWorkflow, len(entries)) - for i, entry := range entries { - workflows[i] = getActionWorkflowEntry(ctx, defaultBranchCommit, folder, entry) - } - - return workflows, nil -} - -func GetActionWorkflow(ctx *context.APIContext, workflowID string) (*api.ActionWorkflow, error) { - entries, err := ListActionWorkflows(ctx) - if err != nil { - return nil, err - } - - for _, entry := range entries { - if entry.Name == workflowID { - return entry, nil - } - } - - return nil, util.NewNotExistErrorf("workflow %q not found", workflowID) -} - func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, repo *repo_model.Repository, gitRepo *git.Repository, workflowID, ref string, processInputs func(model *model.WorkflowDispatch, inputs map[string]any) error) error { if workflowID == "" { return util.ErrorWrapLocale( diff --git a/services/convert/convert.go b/services/convert/convert.go index de1d445749..e73f9302ca 100644 --- a/services/convert/convert.go +++ b/services/convert/convert.go @@ -7,6 +7,8 @@ package convert import ( "context" "fmt" + "net/url" + "path" "strconv" "strings" "time" @@ -23,6 +25,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" @@ -350,6 +353,95 @@ func ToActionWorkflowJob(ctx context.Context, repo *repo_model.Repository, task }, nil } +func getActionWorkflowPath(commit *git.Commit) string { + paths := []string{".gitea/workflows", ".github/workflows"} + for _, treePath := range paths { + if _, err := commit.SubTree(treePath); err == nil { + return treePath + } + } + return "" +} + +func getActionWorkflowEntry(ctx context.Context, repo *repo_model.Repository, commit *git.Commit, folder string, entry *git.TreeEntry) *api.ActionWorkflow { + cfgUnit := repo.MustGetUnit(ctx, unit.TypeActions) + cfg := cfgUnit.ActionsConfig() + + defaultBranch, _ := commit.GetBranchName() + + workflowURL := fmt.Sprintf("%s/actions/workflows/%s", repo.APIURL(), url.PathEscape(entry.Name())) + workflowRepoURL := fmt.Sprintf("%s/src/branch/%s/%s/%s", repo.HTMLURL(ctx), util.PathEscapeSegments(defaultBranch), util.PathEscapeSegments(folder), url.PathEscape(entry.Name())) + badgeURL := fmt.Sprintf("%s/actions/workflows/%s/badge.svg?branch=%s", repo.HTMLURL(ctx), url.PathEscape(entry.Name()), url.QueryEscape(repo.DefaultBranch)) + + // See https://docs.github.com/en/rest/actions/workflows?apiVersion=2022-11-28#get-a-workflow + // State types: + // - active + // - deleted + // - disabled_fork + // - disabled_inactivity + // - disabled_manually + state := "active" + if cfg.IsWorkflowDisabled(entry.Name()) { + state = "disabled_manually" + } + + // The CreatedAt and UpdatedAt fields currently reflect the timestamp of the latest commit, which can later be refined + // by retrieving the first and last commits for the file history. The first commit would indicate the creation date, + // while the last commit would represent the modification date. The DeletedAt could be determined by identifying + // the last commit where the file existed. However, this implementation has not been done here yet, as it would likely + // cause a significant performance degradation. + createdAt := commit.Author.When + updatedAt := commit.Author.When + + return &api.ActionWorkflow{ + ID: entry.Name(), + Name: entry.Name(), + Path: path.Join(folder, entry.Name()), + State: state, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + URL: workflowURL, + HTMLURL: workflowRepoURL, + BadgeURL: badgeURL, + } +} + +func ListActionWorkflows(ctx context.Context, gitrepo *git.Repository, repo *repo_model.Repository) ([]*api.ActionWorkflow, error) { + defaultBranchCommit, err := gitrepo.GetBranchCommit(repo.DefaultBranch) + if err != nil { + return nil, err + } + + entries, err := actions.ListWorkflows(defaultBranchCommit) + if err != nil { + return nil, err + } + + folder := getActionWorkflowPath(defaultBranchCommit) + + workflows := make([]*api.ActionWorkflow, len(entries)) + for i, entry := range entries { + workflows[i] = getActionWorkflowEntry(ctx, repo, defaultBranchCommit, folder, entry) + } + + return workflows, nil +} + +func GetActionWorkflow(ctx context.Context, gitrepo *git.Repository, repo *repo_model.Repository, workflowID string) (*api.ActionWorkflow, error) { + entries, err := ListActionWorkflows(ctx, gitrepo, repo) + if err != nil { + return nil, err + } + + for _, entry := range entries { + if entry.Name == workflowID { + return entry, nil + } + } + + return nil, util.NewNotExistErrorf("workflow %q not found", workflowID) +} + // ToActionArtifact convert a actions_model.ActionArtifact to an api.ActionArtifact func ToActionArtifact(repo *repo_model.Repository, art *actions_model.ActionArtifact) (*api.ActionArtifact, error) { url := fmt.Sprintf("%s/actions/artifacts/%d", repo.APIURL(), art.ID) diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go index c0348243ab..0ebea5dc8d 100644 --- a/services/webhook/notifier.go +++ b/services/webhook/notifier.go @@ -986,6 +986,9 @@ func (*webhookNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *repo_ status, _ := convert.ToActionsStatus(run.Status) + // TODO get gitrepo instance + // convertedWorkflow, err := convert.GetActionWorkflow(ctx, nil, nil, run.WorkflowID) + convertedRun, err := convert.ToActionWorkflowRun(repo, run) if err != nil { log.Error("ToActionWorkflowRun: %v", err) From ccb9dec6dbafaa2f12bcad07f300f6e2f78fef36 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Mon, 17 Mar 2025 21:20:05 +0100 Subject: [PATCH 11/37] fixup --- routers/api/v1/repo/action.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index 621f337a72..6a60f3ba2d 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -634,7 +634,7 @@ func ActionsListRepositoryWorkflows(ctx *context.APIContext) { // "500": // "$ref": "#/responses/error" - workflows, err := actions_service.ListActionWorkflows(ctx) + workflows, err := convert.ListActionWorkflows(ctx, ctx.Repo.GitRepo, ctx.Repo.Repository) if err != nil { ctx.APIErrorInternal(err) return @@ -680,7 +680,7 @@ func ActionsGetWorkflow(ctx *context.APIContext) { // "$ref": "#/responses/error" workflowID := ctx.PathParam("workflow_id") - workflow, err := actions_service.GetActionWorkflow(ctx, workflowID) + workflow, err := convert.GetActionWorkflow(ctx, ctx.Repo.GitRepo, ctx.Repo.Repository, workflowID) if err != nil { if errors.Is(err, util.ErrNotExist) { ctx.APIError(http.StatusNotFound, err) From da738eaa74ccb253cfbc207196cd72bac42561fe Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Mon, 17 Mar 2025 21:58:29 +0100 Subject: [PATCH 12/37] webhook deliver the workflow field --- services/actions/notifier.go | 12 +++++++++++- services/webhook/notifier.go | 13 ++++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/services/actions/notifier.go b/services/actions/notifier.go index a639d53262..57990a4e99 100644 --- a/services/actions/notifier.go +++ b/services/actions/notifier.go @@ -16,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/log" "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" @@ -774,6 +775,15 @@ func (n *actionsNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *rep status, _ := convert.ToActionsStatus(run.Status) + gitRepo, err := gitrepo.OpenRepository(context.Background(), repo) + if err != nil { + log.Error("OpenRepository: %v", err) + return + } + defer gitRepo.Close() + + convertedWorkflow, err := convert.GetActionWorkflow(ctx, gitRepo, repo, run.WorkflowID) + convertedRun, err := convert.ToActionWorkflowRun(repo, run) if err != nil { log.Error("ToActionWorkflowRun: %v", err) @@ -782,7 +792,7 @@ func (n *actionsNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *rep newNotifyInput(repo, sender, webhook_module.HookEventWorkflowRun).WithPayload(&api.WorkflowRunPayload{ Action: status, - Workflow: nil, + Workflow: convertedWorkflow, WorkflowRun: convertedRun, Organization: org, Repo: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}), diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go index 41d9b07255..9878dd8c62 100644 --- a/services/webhook/notifier.go +++ b/services/webhook/notifier.go @@ -16,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" @@ -986,8 +987,14 @@ func (*webhookNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *repo_ status, _ := convert.ToActionsStatus(run.Status) - // TODO get gitrepo instance - // convertedWorkflow, err := convert.GetActionWorkflow(ctx, nil, nil, run.WorkflowID) + gitRepo, err := gitrepo.OpenRepository(ctx, repo) + if err != nil { + log.Error("OpenRepository: %v", err) + return + } + defer gitRepo.Close() + + convertedWorkflow, err := convert.GetActionWorkflow(ctx, gitRepo, repo, run.WorkflowID) convertedRun, err := convert.ToActionWorkflowRun(repo, run) if err != nil { @@ -997,7 +1004,7 @@ func (*webhookNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *repo_ if err := PrepareWebhooks(ctx, source, webhook_module.HookEventWorkflowRun, &api.WorkflowRunPayload{ Action: status, - Workflow: nil, + Workflow: convertedWorkflow, WorkflowRun: convertedRun, Organization: org, Repo: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}), From 171efe55c564e29f328a0e40f9f69f63ff0d6a33 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Mon, 17 Mar 2025 21:59:18 +0100 Subject: [PATCH 13/37] update context with name --- services/actions/notifier.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/actions/notifier.go b/services/actions/notifier.go index 57990a4e99..258bd80fad 100644 --- a/services/actions/notifier.go +++ b/services/actions/notifier.go @@ -768,6 +768,8 @@ func (n *actionsNotifier) MigrateRepository(ctx context.Context, doer, u *user_m } func (n *actionsNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, run *actions_model.ActionRun) { + ctx = withMethod(ctx, "WorkflowRunStatusUpdate") + var org *api.Organization if repo.Owner.IsOrganization() { org = convert.ToOrganization(ctx, organization.OrgFromUser(repo.Owner)) From 605ed19b9bba8fa05353b2c72cefdd367087b223 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Mon, 17 Mar 2025 22:15:47 +0100 Subject: [PATCH 14/37] invoke workflow_run --- routers/web/repo/actions/view.go | 10 +++++++++- services/actions/clear_tasks.go | 4 ++++ services/actions/job_emitter.go | 13 +++++++++++++ services/actions/notifier_helper.go | 9 +++++++++ services/actions/schedule_tasks.go | 1 + services/actions/workflow.go | 9 +++++++++ 6 files changed, 45 insertions(+), 1 deletion(-) diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 41f0d2d0ec..b04d81a274 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -460,6 +460,7 @@ func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob, shou actions_service.CreateCommitStatus(ctx, job) _ = job.LoadAttributes(ctx) + notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) return nil @@ -561,7 +562,10 @@ func Cancel(ctx *context_module.Context) { _ = job.LoadAttributes(ctx) notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) } - + if len(updatedjobs) > 0 { + job := updatedjobs[0] + notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) + } ctx.JSON(http.StatusOK, struct{}{}) } @@ -607,6 +611,10 @@ func Approve(ctx *context_module.Context) { _ = job.LoadAttributes(ctx) notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) } + if len(updatedjobs) > 0 { + job := updatedjobs[0] + notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) + } ctx.JSON(http.StatusOK, struct{}{}) } diff --git a/services/actions/clear_tasks.go b/services/actions/clear_tasks.go index bfa10563bc..e68719c4f8 100644 --- a/services/actions/clear_tasks.go +++ b/services/actions/clear_tasks.go @@ -42,6 +42,10 @@ func notifyWorkflowJobStatusUpdate(ctx context.Context, jobs []*actions_model.Ac _ = job.LoadAttributes(ctx) notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) } + if len(jobs) > 0 { + job := jobs[0] + notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) + } } } diff --git a/services/actions/job_emitter.go b/services/actions/job_emitter.go index c11bb5875f..f500604c08 100644 --- a/services/actions/job_emitter.go +++ b/services/actions/job_emitter.go @@ -78,6 +78,19 @@ func checkJobsOfRun(ctx context.Context, runID int64) error { _ = job.LoadAttributes(ctx) notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) } + if len(updatedjobs) > 0 { + runUpdated := true + run := updatedjobs[0].Run + for _, job := range jobs { + if !job.Status.IsDone() { + runUpdated = false + break + } + } + if runUpdated { + notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run) + } + } return nil } diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index d179134798..c4179c0a06 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -364,6 +364,15 @@ func handleWorkflows( continue } CreateCommitStatus(ctx, alljobs...) + if len(alljobs) > 0 { + job := alljobs[0] + err := job.LoadRun(ctx) + if err != nil { + log.Error("LoadRun: %v", err) + continue + } + notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) + } for _, job := range alljobs { notify_service.WorkflowJobStatusUpdate(ctx, input.Repo, input.Doer, job, nil) } diff --git a/services/actions/schedule_tasks.go b/services/actions/schedule_tasks.go index a30b166063..c029c5a1a2 100644 --- a/services/actions/schedule_tasks.go +++ b/services/actions/schedule_tasks.go @@ -157,6 +157,7 @@ func CreateScheduleTask(ctx context.Context, cron *actions_model.ActionSchedule) if err != nil { log.Error("LoadAttributes: %v", err) } + notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run) for _, job := range allJobs { notify_service.WorkflowJobStatusUpdate(ctx, run.Repo, run.TriggerUser, job, nil) } diff --git a/services/actions/workflow.go b/services/actions/workflow.go index 1eb82c5570..7f4df0058d 100644 --- a/services/actions/workflow.go +++ b/services/actions/workflow.go @@ -183,6 +183,15 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re log.Error("FindRunJobs: %v", err) } CreateCommitStatus(ctx, allJobs...) + if len(allJobs) > 0 { + job := allJobs[0] + err := job.LoadRun(ctx) + if err != nil { + log.Error("LoadRun: %v", err) + } else { + notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) + } + } for _, job := range allJobs { notify_service.WorkflowJobStatusUpdate(ctx, repo, doer, job, nil) } From c3f6f133775fefd8bcf362d8ab4d33bae7e085e3 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Mon, 17 Mar 2025 22:23:14 +0100 Subject: [PATCH 15/37] prevent endless workflow_run trigger --- services/actions/notifier_helper.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index c4179c0a06..3b07cc688e 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -263,6 +263,15 @@ func skipWorkflows(input *notifyInput, commit *git.Commit) bool { } } } + if input.Event == webhook_module.HookEventWorkflowRun { + wrun, ok := input.Payload.(*api.WorkflowRunPayload) + if ok && wrun.WorkflowRun != nil && wrun.WorkflowRun.Event != "workflow_run" { + // skip workflow runs triggered by another workflow run + // TODO GitHub allows chaining up to 5 of them + log.Debug("repo %s: skipped workflow_run because of recursive event", input.Repo.RepoPath()) + return true + } + } return false } From ca6bbc6c6d6f77e423135221c2bf79713c4eb14a Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Mon, 17 Mar 2025 22:43:23 +0100 Subject: [PATCH 16/37] wip --- models/webhook/webhook_test.go | 2 +- modules/webhook/type.go | 1 + routers/api/v1/utils/hook.go | 1 + routers/web/repo/setting/webhook.go | 1 + services/forms/repo_form.go | 1 + services/webhook/payloader.go | 2 ++ 6 files changed, 7 insertions(+), 1 deletion(-) diff --git a/models/webhook/webhook_test.go b/models/webhook/webhook_test.go index 6ff77a380d..d1fe722dd0 100644 --- a/models/webhook/webhook_test.go +++ b/models/webhook/webhook_test.go @@ -73,7 +73,7 @@ func TestWebhook_EventsArray(t *testing.T) { "pull_request", "pull_request_assign", "pull_request_label", "pull_request_milestone", "pull_request_comment", "pull_request_review_approved", "pull_request_review_rejected", "pull_request_review_comment", "pull_request_sync", "pull_request_review_request", "wiki", "repository", "release", - "package", "status", "workflow_job", + "package", "status", "workflow_run", "workflow_job", }, (&Webhook{ HookEvent: &webhook_module.HookEvent{SendEverything: true}, diff --git a/modules/webhook/type.go b/modules/webhook/type.go index bcf8903b2b..89c6a4bfe5 100644 --- a/modules/webhook/type.go +++ b/modules/webhook/type.go @@ -68,6 +68,7 @@ func AllEvents() []HookEventType { HookEventRelease, HookEventPackage, HookEventStatus, + HookEventWorkflowRun, HookEventWorkflowJob, } } diff --git a/routers/api/v1/utils/hook.go b/routers/api/v1/utils/hook.go index ce0c1b5097..3b87a8f257 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.HookEventWorkflowRun: util.SliceContainsString(form.Events, string(webhook_module.HookEventWorkflowRun), true), webhook_module.HookEventWorkflowJob: util.SliceContainsString(form.Events, string(webhook_module.HookEventWorkflowJob), true), }, BranchFilter: form.BranchFilter, diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go index d3151a86a2..006abafe57 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.HookEventWorkflowRun: form.WorkflowRun, webhook_module.HookEventWorkflowJob: form.WorkflowJob, }, BranchFilter: form.BranchFilter, diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 1366d30b1f..bf7554971d 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -237,6 +237,7 @@ type WebhookForm struct { Release bool Package bool Status bool + WorkflowRun bool WorkflowJob bool Active bool BranchFilter string `binding:"GlobPattern"` diff --git a/services/webhook/payloader.go b/services/webhook/payloader.go index adb7243fb1..058dd08589 100644 --- a/services/webhook/payloader.go +++ b/services/webhook/payloader.go @@ -81,6 +81,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) } From 875c7745e44526f6c178e172f5100e1ea2ed5297 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Tue, 18 Mar 2025 20:26:22 +0100 Subject: [PATCH 17/37] wip test and fixes --- routers/api/v1/repo/action.go | 4 +- services/actions/notifier.go | 2 +- services/convert/convert.go | 7 +- services/webhook/notifier.go | 2 +- .../workflow_run_api_check_test.go | 72 +++++++++++++++++++ 5 files changed, 80 insertions(+), 7 deletions(-) create mode 100644 tests/integration/workflow_run_api_check_test.go diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index 88769fa858..c03f735710 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -962,7 +962,7 @@ func GetWorkflowRuns(ctx *context.APIContext) { res.Entries = make([]*api.ActionWorkflowRun, len(runs)) for i := range runs { - convertedRun, err := convert.ToActionWorkflowRun(ctx.Repo.Repository, runs[i]) + convertedRun, err := convert.ToActionWorkflowRun(ctx, ctx.Repo.Repository, runs[i]) if err != nil { ctx.APIErrorInternal(err) return @@ -1011,7 +1011,7 @@ func GetWorkflowRun(ctx *context.APIContext) { ctx.APIError(http.StatusNotFound, util.ErrNotExist) } - convertedArtifact, err := convert.ToActionWorkflowRun(ctx.Repo.Repository, job) + convertedArtifact, err := convert.ToActionWorkflowRun(ctx, ctx.Repo.Repository, job) if err != nil { ctx.APIErrorInternal(err) return diff --git a/services/actions/notifier.go b/services/actions/notifier.go index 258bd80fad..0b96ee06b9 100644 --- a/services/actions/notifier.go +++ b/services/actions/notifier.go @@ -786,7 +786,7 @@ func (n *actionsNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *rep convertedWorkflow, err := convert.GetActionWorkflow(ctx, gitRepo, repo, run.WorkflowID) - convertedRun, err := convert.ToActionWorkflowRun(repo, run) + convertedRun, err := convert.ToActionWorkflowRun(ctx, repo, run) if err != nil { log.Error("ToActionWorkflowRun: %v", err) return diff --git a/services/convert/convert.go b/services/convert/convert.go index e73f9302ca..f4084884c3 100644 --- a/services/convert/convert.go +++ b/services/convert/convert.go @@ -234,7 +234,8 @@ func ToActionTask(ctx context.Context, t *actions_model.ActionTask) (*api.Action }, nil } -func ToActionWorkflowRun(repo *repo_model.Repository, run *actions_model.ActionRun) (*api.ActionWorkflowRun, error) { +func ToActionWorkflowRun(ctx context.Context, repo *repo_model.Repository, run *actions_model.ActionRun) (*api.ActionWorkflowRun, error) { + run.LoadRepo(ctx) status, conclusion := ToActionsStatus(run.Status) return &api.ActionWorkflowRun{ ID: run.ID, @@ -243,13 +244,13 @@ func ToActionWorkflowRun(repo *repo_model.Repository, run *actions_model.ActionR RunNumber: run.Index, StartedAt: run.Started.AsLocalTime(), CompletedAt: run.Stopped.AsLocalTime(), - Event: run.TriggerEvent, + Event: string(run.Event), DisplayTitle: run.Title, HeadBranch: git.RefName(run.Ref).BranchName(), HeadSha: run.CommitSHA, Status: status, Conclusion: conclusion, - Path: fmt.Sprint("%s@%s", run.WorkflowID, run.Ref), + Path: fmt.Sprintf("%s@%s", run.WorkflowID, run.Ref), }, nil } diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go index 9878dd8c62..5689748724 100644 --- a/services/webhook/notifier.go +++ b/services/webhook/notifier.go @@ -996,7 +996,7 @@ func (*webhookNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *repo_ convertedWorkflow, err := convert.GetActionWorkflow(ctx, gitRepo, repo, run.WorkflowID) - convertedRun, err := convert.ToActionWorkflowRun(repo, run) + convertedRun, err := convert.ToActionWorkflowRun(ctx, repo, run) if err != nil { log.Error("ToActionWorkflowRun: %v", err) return diff --git a/tests/integration/workflow_run_api_check_test.go b/tests/integration/workflow_run_api_check_test.go new file mode 100644 index 0000000000..f142da7b22 --- /dev/null +++ b/tests/integration/workflow_run_api_check_test.go @@ -0,0 +1,72 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "fmt" + "net/http" + "testing" + + auth_model "code.gitea.io/gitea/models/auth" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func TestAPIWorkflowRunRepoApi(t *testing.T) { + defer tests.PrepareTestEnv(t)() + userUsername := "user5" + token := getUserToken(t, userUsername, auth_model.AccessTokenScopeWriteRepository) + + req := NewRequest(t, "GET", "/api/v1/repos/user5/repo4/actions/runs").AddTokenAuth(token) + runnerListResp := MakeRequest(t, req, http.StatusOK) + runnerList := api.ActionWorkflowRunsResponse{} + DecodeJSON(t, runnerListResp, &runnerList) + + assert.Len(t, runnerList.Entries, 4) + + for _, run := range runnerList.Entries { + req := NewRequest(t, "GET", fmt.Sprintf("%s/%s", run.URL, "jobs")).AddTokenAuth(token) + jobsResp := MakeRequest(t, req, http.StatusOK) + jobList := api.ActionWorkflowJobsResponse{} + DecodeJSON(t, jobsResp, &jobList) + + // assert.NotEmpty(t, jobList.Entries) + for _, job := range jobList.Entries { + req := NewRequest(t, "GET", job.URL).AddTokenAuth(token) + jobsResp := MakeRequest(t, req, http.StatusOK) + apiJob := api.ActionWorkflowJob{} + DecodeJSON(t, jobsResp, &apiJob) + assert.Equal(t, job.ID, apiJob.ID) + assert.Equal(t, job.RunID, apiJob.RunID) + assert.Equal(t, job.Status, apiJob.Status) + assert.Equal(t, job.Conclusion, apiJob.Conclusion) + } + // assert.NotEmpty(t, run.ID) + // assert.NotEmpty(t, run.Status) + // assert.NotEmpty(t, run.Event) + // assert.NotEmpty(t, run.WorkflowID) + // assert.NotEmpty(t, run.HeadBranch) + // assert.NotEmpty(t, run.HeadSHA) + // assert.NotEmpty(t, run.CreatedAt) + // assert.NotEmpty(t, run.UpdatedAt) + // assert.NotEmpty(t, run.URL) + // assert.NotEmpty(t, run.HTMLURL) + // assert.NotEmpty(t, run.PullRequests) + // assert.NotEmpty(t, run.WorkflowURL) + // assert.NotEmpty(t, run.HeadCommit) + // assert.NotEmpty(t, run.HeadRepository) + // assert.NotEmpty(t, run.Repository) + // assert.NotEmpty(t, run.HeadRepository) + // assert.NotEmpty(t, run.HeadRepository.Owner) + // assert.NotEmpty(t, run.HeadRepository.Name) + // assert.NotEmpty(t, run.Repository.Owner) + // assert.NotEmpty(t, run.Repository.Name) + // assert.NotEmpty(t, run.HeadRepository.Owner.Login) + // assert.NotEmpty(t, run.HeadRepository.Name) + // assert.NotEmpty(t, run.Repository.Owner.Login) + // assert.NotEmpty(t, run.Repository.Name) + } +} From 22bfd96b9cff00567e496a5b53fb42a2c23dd82b Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Tue, 18 Mar 2025 21:03:33 +0100 Subject: [PATCH 18/37] fix keda scaler compat --- modules/structs/repo_actions.go | 28 +++++++++++++++------------- services/convert/convert.go | 1 + 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/modules/structs/repo_actions.go b/modules/structs/repo_actions.go index 7c8de3d776..150302e8b9 100644 --- a/modules/structs/repo_actions.go +++ b/modules/structs/repo_actions.go @@ -86,19 +86,21 @@ type ActionArtifact struct { // ActionWorkflowRun represents a WorkflowRun type ActionWorkflowRun struct { - ID int64 `json:"id"` - URL string `json:"url"` - HTMLURL string `json:"html_url"` - DisplayTitle string `json:"display_title"` - Path string `json:"path"` - Event string `json:"event"` - RunAttempt int64 `json:"run_attempt"` - RunNumber int64 `json:"run_number"` - RepositoryID int64 `json:"repository_id,omitempty"` - HeadSha string `json:"head_sha"` - HeadBranch string `json:"head_branch,omitempty"` - Status string `json:"status"` - Conclusion string `json:"conclusion,omitempty"` + ID int64 `json:"id"` + URL string `json:"url"` + HTMLURL string `json:"html_url"` + DisplayTitle string `json:"display_title"` + Path string `json:"path"` + Event string `json:"event"` + RunAttempt int64 `json:"run_attempt"` + RunNumber int64 `json:"run_number"` + RepositoryID int64 `json:"repository_id,omitempty"` + HeadSha string `json:"head_sha"` + HeadBranch string `json:"head_branch,omitempty"` + Status string `json:"status"` + Repository *Repository `json:"repository,omitempty"` + HeadRepository *Repository `json:"head_repository,omitempty"` + Conclusion string `json:"conclusion,omitempty"` // swagger:strfmt date-time StartedAt time.Time `json:"started_at,omitempty"` // swagger:strfmt date-time diff --git a/services/convert/convert.go b/services/convert/convert.go index f4084884c3..bb34d97a34 100644 --- a/services/convert/convert.go +++ b/services/convert/convert.go @@ -251,6 +251,7 @@ func ToActionWorkflowRun(ctx context.Context, repo *repo_model.Repository, run * Status: status, Conclusion: conclusion, Path: fmt.Sprintf("%s@%s", run.WorkflowID, run.Ref), + Repository: ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeNone}), }, nil } From 6c9dae9ff54269e3021bcff1fd2fbe1e00f23d3d Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Tue, 18 Mar 2025 21:07:27 +0100 Subject: [PATCH 19/37] fix api result --- modules/structs/repo_actions.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/structs/repo_actions.go b/modules/structs/repo_actions.go index 150302e8b9..eca1382506 100644 --- a/modules/structs/repo_actions.go +++ b/modules/structs/repo_actions.go @@ -115,7 +115,7 @@ type ActionWorkflowRunsResponse struct { // ActionArtifactsResponse returns ActionArtifacts type ActionWorkflowJobsResponse struct { - Entries []*ActionWorkflowJob `json:"workflow_jobs"` + Entries []*ActionWorkflowJob `json:"jobs"` TotalCount int64 `json:"total_count"` } From 5588588d2858ff02f26817cdf0bc6361be906948 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Tue, 18 Mar 2025 21:58:02 +0100 Subject: [PATCH 20/37] add missing translate keys --- options/locale/locale_en-US.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 876e135b22..7d9f2995bb 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -2387,6 +2387,8 @@ settings.event_pull_request_review_request_desc = Pull request review requested settings.event_pull_request_approvals = Pull Request Approvals settings.event_pull_request_merge = Pull Request Merge settings.event_header_workflow = Workflow Events +settings.event_workflow_run = Workflow Run +settings.event_workflow_run_desc = Gitea Actions Workflow run queued, waiting, in progress, or completed. settings.event_workflow_job = Workflow Jobs settings.event_workflow_job_desc = Gitea Actions Workflow job queued, waiting, in progress, or completed. settings.event_package = Package From 5a0f4c9869aa0464b524beb9561238130ea6c628 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Fri, 21 Mar 2025 12:15:06 +0100 Subject: [PATCH 21/37] add other webhook types --- services/webhook/dingtalk.go | 6 ++++++ services/webhook/discord.go | 6 ++++++ services/webhook/feishu.go | 6 ++++++ services/webhook/general.go | 31 +++++++++++++++++++++++++++++++ services/webhook/matrix.go | 6 ++++++ services/webhook/msteams.go | 14 ++++++++++++++ services/webhook/packagist.go | 4 ++++ services/webhook/payloader.go | 5 +++-- services/webhook/slack.go | 6 ++++++ services/webhook/telegram.go | 6 ++++++ services/webhook/wechatwork.go | 6 ++++++ 11 files changed, 94 insertions(+), 2 deletions(-) diff --git a/services/webhook/dingtalk.go b/services/webhook/dingtalk.go index 5afca8d65a..6a6aa2a52b 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 0a7eb0b166..0911d1e16a 100644 --- a/services/webhook/discord.go +++ b/services/webhook/discord.go @@ -271,6 +271,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..c7d2309ac4 100644 --- a/services/webhook/feishu.go +++ b/services/webhook/feishu.go @@ -172,6 +172,12 @@ 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) diff --git a/services/webhook/general.go b/services/webhook/general.go index ea75038faf..825c6fe051 100644 --- a/services/webhook/general.go +++ b/services/webhook/general.go @@ -325,6 +325,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 += fmt.Sprintf(" by %s", 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 f70e235f20..3edcf90abd 100644 --- a/services/webhook/msteams.go +++ b/services/webhook/msteams.go @@ -317,6 +317,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/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 058dd08589..c25d700c23 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,8 +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.HookEventWorkflowRun: + return convertUnmarshalledJSON(rc.WorkflowRun, data) case webhook_module.HookEventWorkflowJob: return convertUnmarshalledJSON(rc.WorkflowJob, data) } 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) From 6df185423d9dcd440982fb882dfaea1d0f46b8e3 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Fri, 21 Mar 2025 12:27:21 +0100 Subject: [PATCH 22/37] .. --- services/actions/notifier_helper.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index 3b07cc688e..cc6b1eb09a 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -265,7 +265,7 @@ func skipWorkflows(input *notifyInput, commit *git.Commit) bool { } if input.Event == webhook_module.HookEventWorkflowRun { wrun, ok := input.Payload.(*api.WorkflowRunPayload) - if ok && wrun.WorkflowRun != nil && wrun.WorkflowRun.Event != "workflow_run" { + if ok && wrun.WorkflowRun != nil && wrun.WorkflowRun.Event == "workflow_run" { // skip workflow runs triggered by another workflow run // TODO GitHub allows chaining up to 5 of them log.Debug("repo %s: skipped workflow_run because of recursive event", input.Repo.RepoPath()) From a983d34622ad9b557830dc2ce245fb19fa75c891 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Fri, 21 Mar 2025 13:09:38 +0100 Subject: [PATCH 23/37] fix workflow_run action event processing --- modules/actions/workflows.go | 46 +++++++++++++++++++++++++++++++++ services/actions/job_emitter.go | 4 +-- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index 0d2b0dd919..5afcab145b 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -243,6 +243,10 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web webhook_module.HookEventPackage: return matchPackageEvent(payload.(*api.PackagePayload), evt) + case // registry_package + webhook_module.HookEventWorkflowRun: + return matchWorkflowRunEvent(payload.(*api.WorkflowRunPayload), evt) + default: log.Warn("unsupported event %q", triggedEvent) return false @@ -698,3 +702,45 @@ func matchPackageEvent(payload *api.PackagePayload, evt *jobparser.Event) bool { } return matchTimes == len(evt.Acts()) } + +func matchWorkflowRunEvent(payload *api.WorkflowRunPayload, evt *jobparser.Event) bool { + // with no special filter parameters + if len(evt.Acts()) == 0 { + return true + } + + matchTimes := 0 + // all acts conditions should be satisfied + for cond, vals := range evt.Acts() { + switch cond { + case "types": + action := payload.Action + for _, val := range vals { + if glob.MustCompile(val, '/').Match(string(action)) { + matchTimes++ + break + } + } + case "workflows": + workflow := payload.Workflow + patterns, err := workflowpattern.CompilePatterns(vals...) + if err != nil { + break + } + if !workflowpattern.Skip(patterns, []string{workflow.Name}, &workflowpattern.EmptyTraceWriter{}) { + matchTimes++ + } + case "branches": + patterns, err := workflowpattern.CompilePatterns(vals...) + if err != nil { + break + } + if !workflowpattern.Skip(patterns, []string{payload.WorkflowRun.HeadBranch}, &workflowpattern.EmptyTraceWriter{}) { + matchTimes++ + } + default: + log.Warn("package event unsupported condition %q", cond) + } + } + return matchTimes == len(evt.Acts()) +} diff --git a/services/actions/job_emitter.go b/services/actions/job_emitter.go index f500604c08..c77771d6be 100644 --- a/services/actions/job_emitter.go +++ b/services/actions/job_emitter.go @@ -78,9 +78,9 @@ func checkJobsOfRun(ctx context.Context, runID int64) error { _ = job.LoadAttributes(ctx) notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) } - if len(updatedjobs) > 0 { + if len(jobs) > 0 { runUpdated := true - run := updatedjobs[0].Run + run := jobs[0].Run for _, job := range jobs { if !job.Status.IsDone() { runUpdated = false From 8cfb0479837ceec77118f3a209925216b934122d Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Fri, 21 Mar 2025 15:48:00 +0100 Subject: [PATCH 24/37] fix lint --- modules/actions/workflows.go | 2 +- routers/api/v1/repo/action.go | 2 - services/actions/notifier.go | 8 +- services/convert/convert.go | 5 +- services/webhook/notifier.go | 4 + templates/swagger/v1_json.tmpl | 254 +++++++++++++++++++++++++ tests/integration/repo_webhook_test.go | 4 +- 7 files changed, 270 insertions(+), 9 deletions(-) diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index 5afcab145b..70d8de4687 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -716,7 +716,7 @@ func matchWorkflowRunEvent(payload *api.WorkflowRunPayload, evt *jobparser.Event case "types": action := payload.Action for _, val := range vals { - if glob.MustCompile(val, '/').Match(string(action)) { + if glob.MustCompile(val, '/').Match(action) { matchTimes++ break } diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index c03f735710..d2abab9577 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -1017,7 +1017,6 @@ func GetWorkflowRun(ctx *context.APIContext) { return } ctx.JSON(http.StatusOK, convertedArtifact) - return } // GetWorkflowJobs Lists all jobs for a workflow run. @@ -1130,7 +1129,6 @@ func GetWorkflowJob(ctx *context.APIContext) { return } ctx.JSON(http.StatusOK, convertedWorkflowJob) - return } // GetArtifacts Lists all artifacts for a repository. diff --git a/services/actions/notifier.go b/services/actions/notifier.go index 0b96ee06b9..a7ae52d4f3 100644 --- a/services/actions/notifier.go +++ b/services/actions/notifier.go @@ -10,7 +10,6 @@ import ( issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" packages_model "code.gitea.io/gitea/models/packages" - "code.gitea.io/gitea/models/perm" perm_model "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" @@ -785,7 +784,10 @@ func (n *actionsNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *rep defer gitRepo.Close() convertedWorkflow, err := convert.GetActionWorkflow(ctx, gitRepo, repo, run.WorkflowID) - + if err != nil { + log.Error("GetActionWorkflow: %v", err) + return + } convertedRun, err := convert.ToActionWorkflowRun(ctx, repo, run) if err != nil { log.Error("ToActionWorkflowRun: %v", err) @@ -797,7 +799,7 @@ func (n *actionsNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *rep Workflow: convertedWorkflow, WorkflowRun: convertedRun, Organization: org, - Repo: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}), + Repo: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm_model.AccessModeOwner}), Sender: convert.ToUser(ctx, sender, nil), }).Notify(ctx) } diff --git a/services/convert/convert.go b/services/convert/convert.go index bb34d97a34..9fcda5f46f 100644 --- a/services/convert/convert.go +++ b/services/convert/convert.go @@ -235,7 +235,10 @@ func ToActionTask(ctx context.Context, t *actions_model.ActionTask) (*api.Action } func ToActionWorkflowRun(ctx context.Context, repo *repo_model.Repository, run *actions_model.ActionRun) (*api.ActionWorkflowRun, error) { - run.LoadRepo(ctx) + err := run.LoadRepo(ctx) + if err != nil { + return nil, err + } status, conclusion := ToActionsStatus(run.Status) return &api.ActionWorkflowRun{ ID: run.ID, diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go index 5689748724..cc40dd3377 100644 --- a/services/webhook/notifier.go +++ b/services/webhook/notifier.go @@ -995,6 +995,10 @@ func (*webhookNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *repo_ defer gitRepo.Close() convertedWorkflow, err := convert.GetActionWorkflow(ctx, gitRepo, repo, run.WorkflowID) + if err != nil { + log.Error("GetActionWorkflow: %v", err) + return + } convertedRun, err := convert.ToActionWorkflowRun(ctx, repo, run) if err != nil { diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 1efaf1a875..e93add7642 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -4187,6 +4187,52 @@ } } }, + "/repos/{owner}/{repo}/actions/jobs/{job_id}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Gets a specific workflow job for a workflow run", + "operationId": "getWorkflowJob", + "parameters": [ + { + "type": "string", + "description": "name of the owner", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repository", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "id of the job", + "name": "job_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/Artifact" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/repos/{owner}/{repo}/actions/runners/registration-token": { "get": { "produces": [ @@ -4220,6 +4266,104 @@ } } }, + "/repos/{owner}/{repo}/actions/runs": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Lists all runs for a repository run", + "operationId": "getWorkflowRuns", + "parameters": [ + { + "type": "string", + "description": "name of the owner", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repository", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "runid of the workflow run", + "name": "run", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the artifact", + "name": "name", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/ArtifactsList" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, + "/repos/{owner}/{repo}/actions/runs/{run}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Gets a specific workflow run", + "operationId": "GetWorkflowRun", + "parameters": [ + { + "type": "string", + "description": "name of the owner", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repository", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "id of the run", + "name": "run", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/Artifact" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/repos/{owner}/{repo}/actions/runs/{run}/artifacts": { "get": { "produces": [ @@ -4272,6 +4416,58 @@ } } }, + "/repos/{owner}/{repo}/actions/runs/{run}/jobs": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Lists all jobs for a workflow run", + "operationId": "getWorkflowJobs", + "parameters": [ + { + "type": "string", + "description": "name of the owner", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repository", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "runid of the workflow run", + "name": "run", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the artifact", + "name": "name", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/ArtifactsList" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/repos/{owner}/{repo}/actions/secrets": { "get": { "produces": [ @@ -19404,19 +19600,77 @@ "description": "ActionWorkflowRun represents a WorkflowRun", "type": "object", "properties": { + "completed_at": { + "type": "string", + "format": "date-time", + "x-go-name": "CompletedAt" + }, + "conclusion": { + "type": "string", + "x-go-name": "Conclusion" + }, + "display_title": { + "type": "string", + "x-go-name": "DisplayTitle" + }, + "event": { + "type": "string", + "x-go-name": "Event" + }, + "head_branch": { + "type": "string", + "x-go-name": "HeadBranch" + }, + "head_repository": { + "$ref": "#/definitions/Repository" + }, "head_sha": { "type": "string", "x-go-name": "HeadSha" }, + "html_url": { + "type": "string", + "x-go-name": "HTMLURL" + }, "id": { "type": "integer", "format": "int64", "x-go-name": "ID" }, + "path": { + "type": "string", + "x-go-name": "Path" + }, + "repository": { + "$ref": "#/definitions/Repository" + }, "repository_id": { "type": "integer", "format": "int64", "x-go-name": "RepositoryID" + }, + "run_attempt": { + "type": "integer", + "format": "int64", + "x-go-name": "RunAttempt" + }, + "run_number": { + "type": "integer", + "format": "int64", + "x-go-name": "RunNumber" + }, + "started_at": { + "type": "string", + "format": "date-time", + "x-go-name": "StartedAt" + }, + "status": { + "type": "string", + "x-go-name": "Status" + }, + "url": { + "type": "string", + "x-go-name": "URL" } }, "x-go-package": "code.gitea.io/gitea/modules/structs" diff --git a/tests/integration/repo_webhook_test.go b/tests/integration/repo_webhook_test.go index 7e85c10d4b..4dfe1945c3 100644 --- a/tests/integration/repo_webhook_test.go +++ b/tests/integration/repo_webhook_test.go @@ -707,7 +707,7 @@ jobs: assert.EqualValues(t, commitID, payloads[3].WorkflowJob.HeadSha) assert.EqualValues(t, "repo1", payloads[3].Repo.Name) assert.EqualValues(t, "user2/repo1", payloads[3].Repo.FullName) - assert.Contains(t, payloads[3].WorkflowJob.URL, fmt.Sprintf("/actions/runs/%d/jobs/%d", payloads[3].WorkflowJob.RunID, payloads[3].WorkflowJob.ID)) + assert.Contains(t, payloads[3].WorkflowJob.URL, fmt.Sprintf("/actions/jobs/%d", payloads[3].WorkflowJob.ID)) assert.Contains(t, payloads[3].WorkflowJob.URL, payloads[3].WorkflowJob.RunURL) assert.Contains(t, payloads[3].WorkflowJob.HTMLURL, fmt.Sprintf("/jobs/%d", 0)) assert.Len(t, payloads[3].WorkflowJob.Steps, 1) @@ -745,7 +745,7 @@ jobs: assert.EqualValues(t, commitID, payloads[6].WorkflowJob.HeadSha) assert.EqualValues(t, "repo1", payloads[6].Repo.Name) assert.EqualValues(t, "user2/repo1", payloads[6].Repo.FullName) - assert.Contains(t, payloads[6].WorkflowJob.URL, fmt.Sprintf("/actions/runs/%d/jobs/%d", payloads[6].WorkflowJob.RunID, payloads[6].WorkflowJob.ID)) + assert.Contains(t, payloads[6].WorkflowJob.URL, fmt.Sprintf("/actions/jobs/%d", payloads[6].WorkflowJob.ID)) assert.Contains(t, payloads[6].WorkflowJob.URL, payloads[6].WorkflowJob.RunURL) assert.Contains(t, payloads[6].WorkflowJob.HTMLURL, fmt.Sprintf("/jobs/%d", 1)) assert.Len(t, payloads[6].WorkflowJob.Steps, 2) From 8fd54f285ab73ec824a29a44e50b4ddc1a25f950 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Fri, 21 Mar 2025 15:52:41 +0100 Subject: [PATCH 25/37] update swagger docu --- routers/api/v1/repo/action.go | 24 ++++++++++++------------ templates/swagger/v1_json.tmpl | 25 ++++++++++++------------- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index d2abab9577..b8ccdb0a31 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -912,14 +912,19 @@ func GetWorkflowRuns(ctx *context.APIContext) { // description: name of the repository // type: string // required: true - // - name: run - // in: path - // description: runid of the workflow run - // type: integer - // required: true - // - name: name + // - name: event // in: query - // description: name of the artifact + // description: workflow event name + // type: string + // required: false + // - name: branch + // in: query + // description: workflow branch + // type: string + // required: false + // - name: status + // in: query + // description: workflow status (pending, queued, in_progress, failure, success, skipped) // type: string // required: false // responses: @@ -1042,11 +1047,6 @@ func GetWorkflowJobs(ctx *context.APIContext) { // description: runid of the workflow run // type: integer // required: true - // - name: name - // in: query - // description: name of the artifact - // type: string - // required: false // responses: // "200": // "$ref": "#/responses/ArtifactsList" diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index e93add7642..9ee75770e1 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -4292,16 +4292,21 @@ "required": true }, { - "type": "integer", - "description": "runid of the workflow run", - "name": "run", - "in": "path", - "required": true + "type": "string", + "description": "workflow event name", + "name": "event", + "in": "query" }, { "type": "string", - "description": "name of the artifact", - "name": "name", + "description": "workflow branch", + "name": "branch", + "in": "query" + }, + { + "type": "string", + "description": "workflow status (pending, queued, in_progress, failure, success, skipped)", + "name": "status", "in": "query" } ], @@ -4447,12 +4452,6 @@ "name": "run", "in": "path", "required": true - }, - { - "type": "string", - "description": "name of the artifact", - "name": "name", - "in": "query" } ], "responses": { From c43cb79f957c5861bd4f82c3dc8bc13383121178 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Fri, 21 Mar 2025 16:21:37 +0100 Subject: [PATCH 26/37] ... --- tests/integration/repo_webhook_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/integration/repo_webhook_test.go b/tests/integration/repo_webhook_test.go index 4dfe1945c3..35b5d6ddf7 100644 --- a/tests/integration/repo_webhook_test.go +++ b/tests/integration/repo_webhook_test.go @@ -708,7 +708,6 @@ jobs: assert.EqualValues(t, "repo1", payloads[3].Repo.Name) assert.EqualValues(t, "user2/repo1", payloads[3].Repo.FullName) assert.Contains(t, payloads[3].WorkflowJob.URL, fmt.Sprintf("/actions/jobs/%d", payloads[3].WorkflowJob.ID)) - assert.Contains(t, payloads[3].WorkflowJob.URL, payloads[3].WorkflowJob.RunURL) assert.Contains(t, payloads[3].WorkflowJob.HTMLURL, fmt.Sprintf("/jobs/%d", 0)) assert.Len(t, payloads[3].WorkflowJob.Steps, 1) @@ -746,7 +745,6 @@ jobs: assert.EqualValues(t, "repo1", payloads[6].Repo.Name) assert.EqualValues(t, "user2/repo1", payloads[6].Repo.FullName) assert.Contains(t, payloads[6].WorkflowJob.URL, fmt.Sprintf("/actions/jobs/%d", payloads[6].WorkflowJob.ID)) - assert.Contains(t, payloads[6].WorkflowJob.URL, payloads[6].WorkflowJob.RunURL) assert.Contains(t, payloads[6].WorkflowJob.HTMLURL, fmt.Sprintf("/jobs/%d", 1)) assert.Len(t, payloads[6].WorkflowJob.Steps, 2) }) From 23de934a1ece39982450875528aac22356b29553 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Fri, 21 Mar 2025 18:34:59 +0100 Subject: [PATCH 27/37] add branches-ignore to workflow_run --- modules/actions/workflows.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index 70d8de4687..9279da9ef0 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -738,6 +738,14 @@ func matchWorkflowRunEvent(payload *api.WorkflowRunPayload, evt *jobparser.Event if !workflowpattern.Skip(patterns, []string{payload.WorkflowRun.HeadBranch}, &workflowpattern.EmptyTraceWriter{}) { matchTimes++ } + case "branches-ignore": + patterns, err := workflowpattern.CompilePatterns(vals...) + if err != nil { + break + } + if !workflowpattern.Filter(patterns, []string{payload.WorkflowRun.HeadBranch}, &workflowpattern.EmptyTraceWriter{}) { + matchTimes++ + } default: log.Warn("package event unsupported condition %q", cond) } From 956556dc20c5b63ec79ef0613bb7ca6c88b74699 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Fri, 21 Mar 2025 18:36:56 +0100 Subject: [PATCH 28/37] update comment --- modules/actions/workflows.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index 9279da9ef0..760a85b2da 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -243,7 +243,7 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web webhook_module.HookEventPackage: return matchPackageEvent(payload.(*api.PackagePayload), evt) - case // registry_package + case // workflow_run webhook_module.HookEventWorkflowRun: return matchWorkflowRunEvent(payload.(*api.WorkflowRunPayload), evt) From bb85519b06d0d5b5845a764cb2de1aa96457f3d8 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Fri, 21 Mar 2025 18:48:30 +0100 Subject: [PATCH 29/37] allow workflow_run for recusive depth of 5 --- models/actions/run.go | 11 +++++++++++ services/actions/notifier_helper.go | 23 ++++++++++++++++------- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/models/actions/run.go b/models/actions/run.go index 89f7f3e640..1e9f046c8d 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -164,6 +164,17 @@ func (run *ActionRun) GetPullRequestEventPayload() (*api.PullRequestPayload, err return nil, fmt.Errorf("event %s is not a pull request event", run.Event) } +func (run *ActionRun) GetWorkflowRunEventPayload() (*api.WorkflowRunPayload, error) { + if run.Event == webhook_module.HookEventWorkflowRun { + var payload api.WorkflowRunPayload + if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil { + return nil, err + } + return &payload, nil + } + return nil, fmt.Errorf("event %s is not a pull request event", run.Event) +} + func (run *ActionRun) IsSchedule() bool { return run.ScheduleID > 0 } diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index cc6b1eb09a..d58229728a 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -178,7 +178,7 @@ func notify(ctx context.Context, input *notifyInput) error { return fmt.Errorf("gitRepo.GetCommit: %w", err) } - if skipWorkflows(input, commit) { + if skipWorkflows(ctx, input, commit) { return nil } @@ -243,7 +243,7 @@ func notify(ctx context.Context, input *notifyInput) error { return handleWorkflows(ctx, detectedWorkflows, commit, input, ref.String()) } -func skipWorkflows(input *notifyInput, commit *git.Commit) bool { +func skipWorkflows(ctx context.Context, input *notifyInput, commit *git.Commit) bool { // skip workflow runs with a configured skip-ci string in commit message or pr title if the event is push or pull_request(_sync) // https://docs.github.com/en/actions/managing-workflow-runs/skipping-workflow-runs skipWorkflowEvents := []webhook_module.HookEventType{ @@ -265,12 +265,21 @@ func skipWorkflows(input *notifyInput, commit *git.Commit) bool { } if input.Event == webhook_module.HookEventWorkflowRun { wrun, ok := input.Payload.(*api.WorkflowRunPayload) - if ok && wrun.WorkflowRun != nil && wrun.WorkflowRun.Event == "workflow_run" { - // skip workflow runs triggered by another workflow run - // TODO GitHub allows chaining up to 5 of them - log.Debug("repo %s: skipped workflow_run because of recursive event", input.Repo.RepoPath()) - return true + for i := 0; i < 5 && ok && wrun.WorkflowRun != nil; i++ { + if wrun.WorkflowRun.Event != "workflow_run" { + return false + } + r, _ := actions_model.GetRunByID(ctx, wrun.WorkflowRun.ID) + var err error + wrun, err = r.GetWorkflowRunEventPayload() + if err != nil { + log.Error("GetWorkflowRunEventPayload: %v", err) + return true + } } + // skip workflow runs events exceeding the maxiumum of 5 recursive events + log.Debug("repo %s: skipped workflow_run because of recursive event of 5", input.Repo.RepoPath()) + return true } return false } From cdefda13a3b746bbe21d41355be13915bf08b42a Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Fri, 21 Mar 2025 18:54:29 +0100 Subject: [PATCH 30/37] fix comment --- modules/actions/workflows.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index 760a85b2da..749977739e 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -747,7 +747,7 @@ func matchWorkflowRunEvent(payload *api.WorkflowRunPayload, evt *jobparser.Event matchTimes++ } default: - log.Warn("package event unsupported condition %q", cond) + log.Warn("workflow run event unsupported condition %q", cond) } } return matchTimes == len(evt.Acts()) From d404c60b73490936e0339065ac2061b4dfb9168e Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Fri, 21 Mar 2025 19:33:17 +0100 Subject: [PATCH 31/37] Sync run status with db prior webhook delivery * fixes status glitch of webhook * e.g. queued for cancel * e.g. completed for rerun --- routers/web/repo/actions/view.go | 17 +++++++++++++---- services/actions/clear_tasks.go | 6 +++--- services/actions/job_emitter.go | 5 ++++- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index b04d81a274..30d16b8dc9 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -459,6 +459,8 @@ func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob, shou } actions_service.CreateCommitStatus(ctx, job) + // Sync run status with db + job.Run = nil _ = job.LoadAttributes(ctx) notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) @@ -564,6 +566,9 @@ func Cancel(ctx *context_module.Context) { } if len(updatedjobs) > 0 { job := updatedjobs[0] + // Sync run status with db + job.Run = nil + job.LoadAttributes(ctx) notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) } ctx.JSON(http.StatusOK, struct{}{}) @@ -607,14 +612,18 @@ func Approve(ctx *context_module.Context) { actions_service.CreateCommitStatus(ctx, jobs...) + if len(updatedjobs) > 0 { + job := updatedjobs[0] + // Sync run status with db + job.Run = nil + job.LoadAttributes(ctx) + notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) + } + for _, job := range updatedjobs { _ = job.LoadAttributes(ctx) notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) } - if len(updatedjobs) > 0 { - job := updatedjobs[0] - notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) - } ctx.JSON(http.StatusOK, struct{}{}) } diff --git a/services/actions/clear_tasks.go b/services/actions/clear_tasks.go index e68719c4f8..d74e3f43e2 100644 --- a/services/actions/clear_tasks.go +++ b/services/actions/clear_tasks.go @@ -127,11 +127,11 @@ func CancelAbandonedJobs(ctx context.Context) error { } CreateCommitStatus(ctx, job) if updated { + // Sync run status with db + job.Run = nil _ = job.LoadAttributes(ctx) notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) - if job.Run != nil { - notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) - } + notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) } } diff --git a/services/actions/job_emitter.go b/services/actions/job_emitter.go index c77771d6be..6504c8e1bb 100644 --- a/services/actions/job_emitter.go +++ b/services/actions/job_emitter.go @@ -80,7 +80,6 @@ func checkJobsOfRun(ctx context.Context, runID int64) error { } if len(jobs) > 0 { runUpdated := true - run := jobs[0].Run for _, job := range jobs { if !job.Status.IsDone() { runUpdated = false @@ -88,6 +87,10 @@ func checkJobsOfRun(ctx context.Context, runID int64) error { } } if runUpdated { + // Sync run status with db + jobs[0].Run = nil + jobs[0].LoadAttributes(ctx) + run := jobs[0].Run notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run) } } From 0940208a00f22bb0374cb0457e3e1cca6eb16594 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Fri, 21 Mar 2025 21:35:22 +0100 Subject: [PATCH 32/37] fix lint --- routers/web/repo/actions/view.go | 9 +++++++-- services/actions/job_emitter.go | 4 +++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 30d16b8dc9..d634a49bb9 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -461,7 +461,9 @@ func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob, shou actions_service.CreateCommitStatus(ctx, job) // Sync run status with db job.Run = nil - _ = job.LoadAttributes(ctx) + if err := job.LoadAttributes(ctx); err != nil { + return err + } notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) @@ -568,7 +570,10 @@ func Cancel(ctx *context_module.Context) { job := updatedjobs[0] // Sync run status with db job.Run = nil - job.LoadAttributes(ctx) + if err := job.LoadAttributes(ctx); err != nil { + ctx.HTTPError(http.StatusInternalServerError, err.Error()) + return + } notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) } ctx.JSON(http.StatusOK, struct{}{}) diff --git a/services/actions/job_emitter.go b/services/actions/job_emitter.go index 6504c8e1bb..e0cf1136f2 100644 --- a/services/actions/job_emitter.go +++ b/services/actions/job_emitter.go @@ -89,7 +89,9 @@ func checkJobsOfRun(ctx context.Context, runID int64) error { if runUpdated { // Sync run status with db jobs[0].Run = nil - jobs[0].LoadAttributes(ctx) + if err := jobs[0].LoadAttributes(ctx); err != nil { + return err + } run := jobs[0].Run notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run) } From 4629a68229833fa469fb44344fb472eeaeea4f83 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Fri, 21 Mar 2025 22:06:54 +0100 Subject: [PATCH 33/37] fix comment --- routers/api/v1/repo/action.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index b8ccdb0a31..7af852c4f9 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -894,7 +894,7 @@ func convertToInternal(s string) actions_model.Status { } } -// GetArtifacts Lists all artifacts for a repository. +// GetWorkflowRuns Lists all runs for a repository run. func GetWorkflowRuns(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/actions/runs repository getWorkflowRuns // --- From 9b3eb4c180367be8945d97646e9ac5755f17f5ab Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Fri, 21 Mar 2025 22:13:39 +0100 Subject: [PATCH 34/37] change action of workflow_run to align --- services/actions/notifier.go | 2 +- services/convert/convert.go | 14 ++++++++++++++ services/webhook/notifier.go | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/services/actions/notifier.go b/services/actions/notifier.go index a7ae52d4f3..1039d48cbd 100644 --- a/services/actions/notifier.go +++ b/services/actions/notifier.go @@ -774,7 +774,7 @@ func (n *actionsNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *rep org = convert.ToOrganization(ctx, organization.OrgFromUser(repo.Owner)) } - status, _ := convert.ToActionsStatus(run.Status) + status := convert.ToWorkflowRunAction(run.Status) gitRepo, err := gitrepo.OpenRepository(context.Background(), repo) if err != nil { diff --git a/services/convert/convert.go b/services/convert/convert.go index 9fcda5f46f..0ce6bf15d3 100644 --- a/services/convert/convert.go +++ b/services/convert/convert.go @@ -258,6 +258,20 @@ func ToActionWorkflowRun(ctx context.Context, repo *repo_model.Repository, run * }, nil } +func ToWorkflowRunAction(status actions_model.Status) string { + var action string + switch status { + case actions_model.StatusWaiting, actions_model.StatusBlocked: + action = "requested" + case actions_model.StatusRunning: + action = "in_progress" + } + if status.IsDone() { + action = "completed" + } + return action +} + func ToActionsStatus(status actions_model.Status) (string, string) { var action string var conclusion string diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go index cc40dd3377..dc44460860 100644 --- a/services/webhook/notifier.go +++ b/services/webhook/notifier.go @@ -985,7 +985,7 @@ func (*webhookNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *repo_ org = convert.ToOrganization(ctx, organization.OrgFromUser(repo.Owner)) } - status, _ := convert.ToActionsStatus(run.Status) + status := convert.ToWorkflowRunAction(run.Status) gitRepo, err := gitrepo.OpenRepository(ctx, repo) if err != nil { From 5beb9ae0db32ddb8b4f84909149e2c5711fbb473 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Sat, 22 Mar 2025 00:25:08 +0100 Subject: [PATCH 35/37] ... --- routers/web/repo/actions/view.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index d634a49bb9..531edd8961 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -621,7 +621,7 @@ func Approve(ctx *context_module.Context) { job := updatedjobs[0] // Sync run status with db job.Run = nil - job.LoadAttributes(ctx) + _ = job.LoadAttributes(ctx) notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) } From 7a0bb0325bc7bd0bf998515303296664d9599007 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Sat, 12 Apr 2025 23:43:58 +0200 Subject: [PATCH 36/37] reorder api --- routers/api/v1/api.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 38464dcbd6..7609ca7243 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1169,6 +1169,7 @@ func Routes() *web.Router { }, context.ReferencesGitRepo(), reqToken(), reqRepoReader(unit.TypeActions)) m.Group("/actions/jobs", func() { + m.Get("/{job_id}", repo.GetWorkflowJob) m.Get("/{job_id}/logs", repo.DownloadActionsRunJobLogs) }, reqToken(), reqRepoReader(unit.TypeActions)) @@ -1247,11 +1248,14 @@ func Routes() *web.Router { }, reqToken(), reqAdmin()) m.Group("/actions", func() { m.Get("/tasks", repo.ListActionTasks) - m.Get("/runs", repo.GetWorkflowRuns) - m.Get("/runs/{run}", repo.GetWorkflowRun) - m.Get("/runs/{run}/jobs", repo.GetWorkflowJobs) - m.Get("/runs/{run}/artifacts", repo.GetArtifactsOfRun) - m.Get("/jobs/{job_id}", repo.GetWorkflowJob) + m.Group("/runs", func() { + m.Get("", repo.GetWorkflowRuns) + m.Group("/{run}", func() { + m.Get("", repo.GetWorkflowRun) + m.Get("/jobs", repo.GetWorkflowJobs) + m.Get("/artifacts", repo.GetArtifactsOfRun) + }) + }) m.Get("/artifacts", repo.GetArtifacts) m.Group("/artifacts/{artifact_id}", func() { m.Get("", repo.GetArtifact) From 896b24aac6d6bff828862113e7aa64e1c506c6e4 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Sat, 12 Apr 2025 23:44:19 +0200 Subject: [PATCH 37/37] fix lint --- services/webhook/general.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/webhook/general.go b/services/webhook/general.go index bcb8eb14a0..be457e46f5 100644 --- a/services/webhook/general.go +++ b/services/webhook/general.go @@ -352,7 +352,7 @@ func getWorkflowRunPayloadInfo(p *api.WorkflowRunPayload, linkFormatter linkForm color = greyColor } if withSender { - text += fmt.Sprintf(" by %s", linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)) + text += " by " + linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName) } return text, color