diff --git a/routers/api/v1/admin/action.go b/routers/api/v1/admin/action.go new file mode 100644 index 0000000000..2fbb8e1a95 --- /dev/null +++ b/routers/api/v1/admin/action.go @@ -0,0 +1,93 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package admin + +import ( + "code.gitea.io/gitea/routers/api/v1/shared" + "code.gitea.io/gitea/services/context" +) + +// ListWorkflowJobs Lists all jobs +func ListWorkflowJobs(ctx *context.APIContext) { + // swagger:operation GET /admin/actions/jobs admin listAdminWorkflowJobs + // --- + // summary: Lists all jobs + // produces: + // - application/json + // parameters: + // - name: status + // in: query + // description: workflow status (pending, queued, in_progress, failure, success, skipped) + // type: string + // required: false + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/WorkflowJobsList" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + shared.ListJobs(ctx, 0, 0, 0) +} + +// ListWorkflowRuns Lists all runs +func ListWorkflowRuns(ctx *context.APIContext) { + // swagger:operation GET /admin/actions/runs admin listAdminWorkflowRuns + // --- + // summary: Lists all runs + // produces: + // - application/json + // parameters: + // - name: event + // in: query + // 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 + // - name: actor + // in: query + // description: triggered by user + // type: string + // required: false + // - name: head_sha + // in: query + // description: triggering sha of the workflow run + // type: string + // required: false + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/WorkflowRunsList" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + shared.ListRuns(ctx, 0, 0) +} diff --git a/routers/api/v1/admin/runners.go b/routers/api/v1/admin/runners.go index 1abb314afc..736c421229 100644 --- a/routers/api/v1/admin/runners.go +++ b/routers/api/v1/admin/runners.go @@ -102,87 +102,3 @@ func DeleteRunner(ctx *context.APIContext) { // "$ref": "#/responses/notFound" shared.DeleteRunner(ctx, 0, 0, ctx.PathParamInt64("runner_id")) } - -// ListWorkflowJobs Lists all jobs -func ListWorkflowJobs(ctx *context.APIContext) { - // swagger:operation GET /admin/actions/jobs admin listAdminWorkflowJobs - // --- - // summary: Lists all jobs - // produces: - // - application/json - // parameters: - // - name: status - // in: query - // description: workflow status (pending, queued, in_progress, failure, success, skipped) - // type: string - // required: false - // - name: page - // in: query - // description: page number of results to return (1-based) - // type: integer - // - name: limit - // in: query - // description: page size of results - // type: integer - // responses: - // "200": - // "$ref": "#/responses/WorkflowJobsList" - // "400": - // "$ref": "#/responses/error" - // "404": - // "$ref": "#/responses/notFound" - - shared.ListJobs(ctx, 0, 0, 0) -} - -// ListWorkflowRuns Lists all runs -func ListWorkflowRuns(ctx *context.APIContext) { - // swagger:operation GET /admin/actions/runs admin listAdminWorkflowRuns - // --- - // summary: Lists all runs - // produces: - // - application/json - // parameters: - // - name: event - // in: query - // 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 - // - name: actor - // in: query - // description: triggered by user - // type: string - // required: false - // - name: head_sha - // in: query - // description: triggering sha of the workflow run - // type: string - // required: false - // - name: page - // in: query - // description: page number of results to return (1-based) - // type: integer - // - name: limit - // in: query - // description: page size of results - // type: integer - // responses: - // "200": - // "$ref": "#/responses/WorkflowRunsList" - // "400": - // "$ref": "#/responses/error" - // "404": - // "$ref": "#/responses/notFound" - - shared.ListRuns(ctx, 0, 0) -} diff --git a/routers/api/v1/shared/action.go b/routers/api/v1/shared/action.go new file mode 100644 index 0000000000..507c669ece --- /dev/null +++ b/routers/api/v1/shared/action.go @@ -0,0 +1,190 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package shared + +import ( + "fmt" + "net/http" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + 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/setting" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/webhook" + "code.gitea.io/gitea/routers/api/v1/utils" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/convert" +) + +// ListJobs lists jobs for api route validated ownerID and repoID +// ownerID == 0 and repoID == 0 means all jobs +// ownerID == 0 and repoID != 0 means all jobs for the given repo +// ownerID != 0 and repoID == 0 means all jobs for the given user/org +// ownerID != 0 and repoID != 0 undefined behavior +// runID == 0 means all jobs +// Access rights are checked at the API route level +func ListJobs(ctx *context.APIContext, ownerID, repoID, runID int64) { + if ownerID != 0 && repoID != 0 { + setting.PanicInDevOrTesting("ownerID and repoID should not be both set") + } + opts := actions_model.FindRunJobOptions{ + OwnerID: ownerID, + RepoID: repoID, + RunID: runID, + ListOptions: utils.GetListOptions(ctx), + } + if statuses, ok := ctx.Req.URL.Query()["status"]; ok { + for _, status := range statuses { + values, err := convertToInternal(status) + if err != nil { + ctx.APIError(http.StatusBadRequest, fmt.Errorf("Invalid status %s", status)) + return + } + opts.Statuses = append(opts.Statuses, values...) + } + } + + jobs, total, err := db.FindAndCount[actions_model.ActionRunJob](ctx, opts) + if err != nil { + ctx.APIErrorInternal(err) + return + } + + res := new(api.ActionWorkflowJobsResponse) + res.TotalCount = total + + res.Entries = make([]*api.ActionWorkflowJob, len(jobs)) + + isRepoLevel := repoID != 0 && ctx.Repo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == repoID + for i := range jobs { + var repository *repo_model.Repository + if isRepoLevel { + repository = ctx.Repo.Repository + } else { + repository, err = repo_model.GetRepositoryByID(ctx, jobs[i].RepoID) + if err != nil { + ctx.APIErrorInternal(err) + return + } + } + + convertedWorkflowJob, err := convert.ToActionWorkflowJob(ctx, repository, nil, jobs[i]) + if err != nil { + ctx.APIErrorInternal(err) + return + } + res.Entries[i] = convertedWorkflowJob + } + + ctx.JSON(http.StatusOK, &res) +} + +func convertToInternal(s string) ([]actions_model.Status, error) { + switch s { + case "pending", "waiting", "requested", "action_required": + return []actions_model.Status{actions_model.StatusBlocked}, nil + case "queued": + return []actions_model.Status{actions_model.StatusWaiting}, nil + case "in_progress": + return []actions_model.Status{actions_model.StatusRunning}, nil + case "completed": + return []actions_model.Status{ + actions_model.StatusSuccess, + actions_model.StatusFailure, + actions_model.StatusSkipped, + actions_model.StatusCancelled, + }, nil + case "failure": + return []actions_model.Status{actions_model.StatusFailure}, nil + case "success": + return []actions_model.Status{actions_model.StatusSuccess}, nil + case "skipped", "neutral": + return []actions_model.Status{actions_model.StatusSkipped}, nil + case "cancelled", "timed_out": + return []actions_model.Status{actions_model.StatusCancelled}, nil + default: + return nil, fmt.Errorf("invalid status %s", s) + } +} + +// ListRuns lists jobs for api route validated ownerID and repoID +// ownerID == 0 and repoID == 0 means all runs +// ownerID == 0 and repoID != 0 means all runs for the given repo +// ownerID != 0 and repoID == 0 means all runs for the given user/org +// ownerID != 0 and repoID != 0 undefined behavior +// Access rights are checked at the API route level +func ListRuns(ctx *context.APIContext, ownerID, repoID int64) { + if ownerID != 0 && repoID != 0 { + setting.PanicInDevOrTesting("ownerID and repoID should not be both set") + } + opts := actions_model.FindRunOptions{ + OwnerID: ownerID, + 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 statuses, ok := ctx.Req.URL.Query()["status"]; ok { + for _, status := range statuses { + values, err := convertToInternal(status) + if err != nil { + ctx.APIError(http.StatusBadRequest, fmt.Errorf("Invalid status %s", status)) + return + } + opts.Status = append(opts.Status, values...) + } + } + if actor := ctx.Req.URL.Query().Get("actor"); actor != "" { + user, err := user_model.GetUserByName(ctx, actor) + if err != nil { + ctx.APIErrorInternal(err) + return + } + opts.TriggerUserID = user.ID + } + if headSHA := ctx.Req.URL.Query().Get("head_sha"); headSHA != "" { + opts.CommitSHA = headSHA + } + + 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)) + isRepoLevel := repoID != 0 && ctx.Repo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == repoID + for i := range runs { + var repository *repo_model.Repository + if isRepoLevel { + repository = ctx.Repo.Repository + } else { + repository, err = repo_model.GetRepositoryByID(ctx, runs[i].RepoID) + if err != nil { + ctx.APIErrorInternal(err) + return + } + } + + convertedRun, err := convert.ToActionWorkflowRun(ctx, repository, runs[i]) + if err != nil { + ctx.APIErrorInternal(err) + return + } + res.Entries[i] = convertedRun + } + + ctx.JSON(http.StatusOK, &res) +} diff --git a/routers/api/v1/shared/runners.go b/routers/api/v1/shared/runners.go index d3f53c9356..d42f330d1c 100644 --- a/routers/api/v1/shared/runners.go +++ b/routers/api/v1/shared/runners.go @@ -5,18 +5,13 @@ package shared import ( "errors" - "fmt" "net/http" actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" - 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/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" - "code.gitea.io/gitea/modules/webhook" "code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" @@ -121,172 +116,3 @@ func DeleteRunner(ctx *context.APIContext, ownerID, repoID, runnerID int64) { } ctx.Status(http.StatusNoContent) } - -// ListJobs lists jobs for api route validated ownerID and repoID -// ownerID == 0 and repoID == 0 means all jobs -// ownerID == 0 and repoID != 0 means all jobs for the given repo -// ownerID != 0 and repoID == 0 means all jobs for the given user/org -// ownerID != 0 and repoID != 0 undefined behavior -// runID == 0 means all jobs -// Access rights are checked at the API route level -func ListJobs(ctx *context.APIContext, ownerID, repoID, runID int64) { - if ownerID != 0 && repoID != 0 { - setting.PanicInDevOrTesting("ownerID and repoID should not be both set") - } - opts := actions_model.FindRunJobOptions{ - OwnerID: ownerID, - RepoID: repoID, - RunID: runID, - ListOptions: utils.GetListOptions(ctx), - } - if statuses, ok := ctx.Req.URL.Query()["status"]; ok { - for _, status := range statuses { - values, err := convertToInternal(status) - if err != nil { - ctx.APIError(http.StatusBadRequest, fmt.Errorf("Invalid status %s", status)) - return - } - opts.Statuses = append(opts.Statuses, values...) - } - } - - jobs, total, err := db.FindAndCount[actions_model.ActionRunJob](ctx, opts) - if err != nil { - ctx.APIErrorInternal(err) - return - } - - res := new(api.ActionWorkflowJobsResponse) - res.TotalCount = total - - res.Entries = make([]*api.ActionWorkflowJob, len(jobs)) - - isRepoLevel := repoID != 0 && ctx.Repo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == repoID - for i := range jobs { - var repository *repo_model.Repository - if isRepoLevel { - repository = ctx.Repo.Repository - } else { - repository, err = repo_model.GetRepositoryByID(ctx, jobs[i].RepoID) - if err != nil { - ctx.APIErrorInternal(err) - return - } - } - - convertedWorkflowJob, err := convert.ToActionWorkflowJob(ctx, repository, nil, jobs[i]) - if err != nil { - ctx.APIErrorInternal(err) - return - } - res.Entries[i] = convertedWorkflowJob - } - - ctx.JSON(http.StatusOK, &res) -} - -func convertToInternal(s string) ([]actions_model.Status, error) { - switch s { - case "pending", "waiting", "requested", "action_required": - return []actions_model.Status{actions_model.StatusBlocked}, nil - case "queued": - return []actions_model.Status{actions_model.StatusWaiting}, nil - case "in_progress": - return []actions_model.Status{actions_model.StatusRunning}, nil - case "completed": - return []actions_model.Status{ - actions_model.StatusSuccess, - actions_model.StatusFailure, - actions_model.StatusSkipped, - actions_model.StatusCancelled, - }, nil - case "failure": - return []actions_model.Status{actions_model.StatusFailure}, nil - case "success": - return []actions_model.Status{actions_model.StatusSuccess}, nil - case "skipped", "neutral": - return []actions_model.Status{actions_model.StatusSkipped}, nil - case "cancelled", "timed_out": - return []actions_model.Status{actions_model.StatusCancelled}, nil - default: - return nil, fmt.Errorf("invalid status %s", s) - } -} - -// ListRuns lists jobs for api route validated ownerID and repoID -// ownerID == 0 and repoID == 0 means all runs -// ownerID == 0 and repoID != 0 means all runs for the given repo -// ownerID != 0 and repoID == 0 means all runs for the given user/org -// ownerID != 0 and repoID != 0 undefined behavior -// Access rights are checked at the API route level -func ListRuns(ctx *context.APIContext, ownerID, repoID int64) { - if ownerID != 0 && repoID != 0 { - setting.PanicInDevOrTesting("ownerID and repoID should not be both set") - } - opts := actions_model.FindRunOptions{ - OwnerID: ownerID, - 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 statuses, ok := ctx.Req.URL.Query()["status"]; ok { - for _, status := range statuses { - values, err := convertToInternal(status) - if err != nil { - ctx.APIError(http.StatusBadRequest, fmt.Errorf("Invalid status %s", status)) - return - } - opts.Status = append(opts.Status, values...) - } - } - if actor := ctx.Req.URL.Query().Get("actor"); actor != "" { - user, err := user_model.GetUserByName(ctx, actor) - if err != nil { - ctx.APIErrorInternal(err) - return - } - opts.TriggerUserID = user.ID - } - if headSHA := ctx.Req.URL.Query().Get("head_sha"); headSHA != "" { - opts.CommitSHA = headSHA - } - - 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)) - isRepoLevel := repoID != 0 && ctx.Repo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == repoID - for i := range runs { - var repository *repo_model.Repository - if isRepoLevel { - repository = ctx.Repo.Repository - } else { - repository, err = repo_model.GetRepositoryByID(ctx, runs[i].RepoID) - if err != nil { - ctx.APIErrorInternal(err) - return - } - } - - convertedRun, err := convert.ToActionWorkflowRun(ctx, repository, runs[i]) - if err != nil { - ctx.APIErrorInternal(err) - return - } - res.Entries[i] = convertedRun - } - - ctx.JSON(http.StatusOK, &res) -} diff --git a/routers/api/v1/user/action.go b/routers/api/v1/user/action.go index 04097fcc95..aa5327e95e 100644 --- a/routers/api/v1/user/action.go +++ b/routers/api/v1/user/action.go @@ -12,6 +12,7 @@ import ( api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/api/v1/shared" "code.gitea.io/gitea/routers/api/v1/utils" actions_service "code.gitea.io/gitea/services/actions" "code.gitea.io/gitea/services/context" @@ -358,3 +359,86 @@ func ListVariables(ctx *context.APIContext) { ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, variables) } + +// ListWorkflowRuns lists workflow runs +func ListWorkflowRuns(ctx *context.APIContext) { + // swagger:operation GET /user/actions/runs user getUserWorkflowRuns + // --- + // summary: Get workflow runs + // parameters: + // - name: event + // in: query + // 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 + // - name: actor + // in: query + // description: triggered by user + // type: string + // required: false + // - name: head_sha + // in: query + // description: triggering sha of the workflow run + // type: string + // required: false + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // produces: + // - application/json + // responses: + // "200": + // "$ref": "#/responses/WorkflowRunsList" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + shared.ListRuns(ctx, ctx.Doer.ID, 0) +} + +// ListWorkflowJobs lists workflow jobs +func ListWorkflowJobs(ctx *context.APIContext) { + // swagger:operation GET /user/actions/jobs user getUserWorkflowJobs + // --- + // summary: Get workflow jobs + // parameters: + // - name: status + // in: query + // description: workflow status (pending, queued, in_progress, failure, success, skipped) + // type: string + // required: false + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // produces: + // - application/json + // responses: + // "200": + // "$ref": "#/responses/WorkflowJobsList" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + shared.ListJobs(ctx, ctx.Doer.ID, 0, 0) +} diff --git a/routers/api/v1/user/runners.go b/routers/api/v1/user/runners.go index 37c606478e..be3f63cc5e 100644 --- a/routers/api/v1/user/runners.go +++ b/routers/api/v1/user/runners.go @@ -102,86 +102,3 @@ func DeleteRunner(ctx *context.APIContext) { // "$ref": "#/responses/notFound" shared.DeleteRunner(ctx, ctx.Doer.ID, 0, ctx.PathParamInt64("runner_id")) } - -// ListWorkflowRuns lists workflow runs -func ListWorkflowRuns(ctx *context.APIContext) { - // swagger:operation GET /user/actions/runs user getUserWorkflowRuns - // --- - // summary: Get workflow runs - // parameters: - // - name: event - // in: query - // 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 - // - name: actor - // in: query - // description: triggered by user - // type: string - // required: false - // - name: head_sha - // in: query - // description: triggering sha of the workflow run - // type: string - // required: false - // - name: page - // in: query - // description: page number of results to return (1-based) - // type: integer - // - name: limit - // in: query - // description: page size of results - // type: integer - // produces: - // - application/json - // responses: - // "200": - // "$ref": "#/responses/WorkflowRunsList" - // "400": - // "$ref": "#/responses/error" - // "404": - // "$ref": "#/responses/notFound" - shared.ListRuns(ctx, ctx.Doer.ID, 0) -} - -// ListWorkflowJobs lists workflow jobs -func ListWorkflowJobs(ctx *context.APIContext) { - // swagger:operation GET /user/actions/jobs user getUserWorkflowJobs - // --- - // summary: Get workflow jobs - // parameters: - // - name: status - // in: query - // description: workflow status (pending, queued, in_progress, failure, success, skipped) - // type: string - // required: false - // - name: page - // in: query - // description: page number of results to return (1-based) - // type: integer - // - name: limit - // in: query - // description: page size of results - // type: integer - // produces: - // - application/json - // responses: - // "200": - // "$ref": "#/responses/WorkflowJobsList" - // "400": - // "$ref": "#/responses/error" - // "404": - // "$ref": "#/responses/notFound" - - shared.ListJobs(ctx, ctx.Doer.ID, 0, 0) -}