From f82178edb3a76b70ce1b2e9197c9c15d29c485ae Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Sat, 3 May 2025 13:26:14 +0200 Subject: [PATCH] add more test and more level of runs / jobs access --- models/actions/run_job_list.go | 15 +- models/actions/run_list.go | 13 +- models/fixtures/action_run.yml | 19 ++ models/fixtures/action_run_job.yml | 17 +- routers/api/v1/admin/runners.go | 36 +++ routers/api/v1/api.go | 22 +- routers/api/v1/org/action.go | 30 +++ routers/api/v1/repo/action.go | 217 +++++++----------- routers/api/v1/shared/runners.go | 142 ++++++++++++ routers/api/v1/user/runners.go | 26 +++ services/actions/interface.go | 4 + templates/swagger/v1_json.tmpl | 167 +++++++++++++- .../workflow_run_api_check_test.go | 28 ++- 13 files changed, 578 insertions(+), 158 deletions(-) diff --git a/models/actions/run_job_list.go b/models/actions/run_job_list.go index 1d50c9c8dd..17f4339a53 100644 --- a/models/actions/run_job_list.go +++ b/models/actions/run_job_list.go @@ -85,9 +85,6 @@ func (opts FindRunJobOptions) ToConds() builder.Cond { if opts.RepoID > 0 { cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) } - if opts.OwnerID > 0 { - cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) - } if opts.CommitSHA != "" { cond = cond.And(builder.Eq{"commit_sha": opts.CommitSHA}) } @@ -99,3 +96,15 @@ func (opts FindRunJobOptions) ToConds() builder.Cond { } return cond } + +func (opts FindRunJobOptions) ToJoins() []db.JoinFunc { + if opts.OwnerID > 0 { + return []db.JoinFunc{ + func(sess db.Engine) error { + sess.Join("INNER", "repository", "repository.id = repo_id AND repository.owner_id = ?", opts.OwnerID) + return nil + }, + } + } + return nil +} diff --git a/models/actions/run_list.go b/models/actions/run_list.go index b9b9324e07..e48f45aa7d 100644 --- a/models/actions/run_list.go +++ b/models/actions/run_list.go @@ -79,9 +79,6 @@ func (opts FindRunOptions) ToConds() builder.Cond { if opts.RepoID > 0 { cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) } - if opts.OwnerID > 0 { - cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) - } if opts.WorkflowID != "" { cond = cond.And(builder.Eq{"workflow_id": opts.WorkflowID}) } @@ -103,6 +100,16 @@ func (opts FindRunOptions) ToConds() builder.Cond { return cond } +func (opts FindRunOptions) ToJoins() []db.JoinFunc { + if opts.OwnerID > 0 { + return []db.JoinFunc{func(sess db.Engine) error { + sess.Join("INNER", "repository", "repository.id = repo_id AND repository.owner_id = ?", opts.OwnerID) + return nil + }} + } + return nil +} + func (opts FindRunOptions) ToOrders() string { return "`id` DESC" } diff --git a/models/fixtures/action_run.yml b/models/fixtures/action_run.yml index e30f19a43f..f285750482 100644 --- a/models/fixtures/action_run.yml +++ b/models/fixtures/action_run.yml @@ -93,3 +93,22 @@ updated: 1683636626 need_approval: 0 approved_by: 0 +- + id: 803 + title: "workflow run list for user" + repo_id: 2 + owner_id: 0 + workflow_id: "test.yaml" + index: 191 + trigger_user_id: 1 + ref: "refs/heads/test" + commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" + event: "push" + is_fork_pull_request: 0 + status: 1 + started: 1683636528 + stopped: 1683636626 + created: 1683636108 + updated: 1683636626 + need_approval: 0 + approved_by: 0 diff --git a/models/fixtures/action_run_job.yml b/models/fixtures/action_run_job.yml index 73138b1c6b..8b16f7b6f8 100644 --- a/models/fixtures/action_run_job.yml +++ b/models/fixtures/action_run_job.yml @@ -73,7 +73,22 @@ id: 203 run_id: 802 repo_id: 5 - owner_id: 3 + owner_id: 0 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + name: job2 + attempt: 1 + job_id: job2 + needs: '["job1"]' + task_id: 51 + status: 5 + started: 1683636528 + stopped: 1683636626 +- + id: 204 + run_id: 803 + repo_id: 2 + owner_id: 0 commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 is_fork_pull_request: 0 name: job2 diff --git a/routers/api/v1/admin/runners.go b/routers/api/v1/admin/runners.go index 736c421229..8fad9304ab 100644 --- a/routers/api/v1/admin/runners.go +++ b/routers/api/v1/admin/runners.go @@ -102,3 +102,39 @@ 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 + // responses: + // "200": + // "$ref": "#/responses/JobList" + // "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 + // responses: + // "200": + // "$ref": "#/responses/RunList" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + shared.ListRuns(ctx, 0, 0) +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 70111b6af8..713c52bc72 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -942,6 +942,8 @@ func Routes() *web.Router { m.Get("/{runner_id}", reqToken(), reqChecker, act.GetRunner) m.Delete("/{runner_id}", reqToken(), reqChecker, act.DeleteRunner) }) + m.Get("/runs", reqToken(), reqChecker, act.ListWorkflowRuns) + m.Get("/jobs", reqToken(), reqChecker, act.ListWorkflowJobs) }) } @@ -1077,6 +1079,9 @@ func Routes() *web.Router { m.Get("/{runner_id}", reqToken(), user.GetRunner) m.Delete("/{runner_id}", reqToken(), user.DeleteRunner) }) + + m.Get("/runs", reqToken(), user.ListWorkflowRuns) + m.Get("/jobs", reqToken(), user.ListWorkflowJobs) }) m.Get("/followers", user.ListMyFollowers) @@ -1281,10 +1286,9 @@ func Routes() *web.Router { m.Group("/actions", func() { m.Get("/tasks", repo.ListActionTasks) m.Group("/runs", func() { - m.Get("", repo.GetWorkflowRuns) m.Group("/{run}", func() { m.Get("", repo.GetWorkflowRun) - m.Get("/jobs", repo.GetWorkflowJobs) + m.Get("/jobs", repo.ListWorkflowRunJobs) m.Get("/artifacts", repo.GetArtifactsOfRun) }) }) @@ -1737,11 +1741,15 @@ func Routes() *web.Router { Patch(bind(api.EditHookOption{}), admin.EditHook). Delete(admin.DeleteHook) }) - m.Group("/actions/runners", func() { - m.Get("", admin.ListRunners) - m.Post("/registration-token", admin.CreateRegistrationToken) - m.Get("/{runner_id}", admin.GetRunner) - m.Delete("/{runner_id}", admin.DeleteRunner) + m.Group("/actions", func() { + m.Group("/runners", func() { + m.Get("", admin.ListRunners) + m.Post("/registration-token", admin.CreateRegistrationToken) + m.Get("/{runner_id}", admin.GetRunner) + m.Delete("/{runner_id}", admin.DeleteRunner) + }) + m.Get("/runs", admin.ListWorkflowRuns) + m.Get("/jobs", admin.ListWorkflowJobs) }) m.Group("/runners", func() { m.Get("/registration-token", admin.GetRegistrationToken) diff --git a/routers/api/v1/org/action.go b/routers/api/v1/org/action.go index 700a5ef8ea..40640753ed 100644 --- a/routers/api/v1/org/action.go +++ b/routers/api/v1/org/action.go @@ -570,6 +570,36 @@ func (Action) DeleteRunner(ctx *context.APIContext) { shared.DeleteRunner(ctx, ctx.Org.Organization.ID, 0, ctx.PathParamInt64("runner_id")) } +func (Action) ListWorkflowJobs(ctx *context.APIContext) { + // swagger:operation GET /orgs/{org}/actions/jobs organization getOrgWorkflowJobs + // --- + // summary: Get org-level workflow jobs + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of the organization + // type: string + // required: true + shared.ListJobs(ctx, ctx.Org.Organization.ID, 0, 0) +} + +func (Action) ListWorkflowRuns(ctx *context.APIContext) { + // swagger:operation GET /orgs/{org}/actions/runs organization getOrgWorkflowRuns + // --- + // summary: Get org-level workflow runs + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of the organization + // type: string + // required: true + shared.ListRuns(ctx, ctx.Org.Organization.ID, 0) +} + var _ actions_service.API = new(Action) // Action implements actions_service.API diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index a390f31e81..f20ec410a9 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -21,13 +21,11 @@ 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" @@ -652,6 +650,83 @@ func (Action) DeleteRunner(ctx *context.APIContext) { shared.DeleteRunner(ctx, 0, ctx.Repo.Repository.ID, ctx.PathParamInt64("runner_id")) } +// GetWorkflowRunJobs Lists all jobs for a workflow run. +func (Action) ListWorkflowJobs(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/jobs repository listWorkflowJobs + // --- + // summary: Lists all jobs for a repository + // 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 + // responses: + // "200": + // "$ref": "#/responses/JobList" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + repoID := ctx.Repo.Repository.ID + + shared.ListJobs(ctx, 0, repoID, 0) +} + +// ListWorkflowRuns Lists all runs for a repository run. +func (Action) ListWorkflowRuns(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: 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 + // responses: + // "200": + // "$ref": "#/responses/ArtifactsList" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + repoID := ctx.Repo.Repository.ID + + shared.ListRuns(ctx, 0, repoID) +} + var _ actions_service.API = new(Action) // Action implements actions_service.API @@ -994,109 +1069,6 @@ 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 - } -} - -// GetWorkflowRuns Lists all runs for a repository run. -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: 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 - // 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, 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 @@ -1143,9 +1115,9 @@ func GetWorkflowRun(ctx *context.APIContext) { ctx.JSON(http.StatusOK, convertedArtifact) } -// 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 +// ListWorkflowRunJobs Lists all jobs for a workflow run. +func ListWorkflowRunJobs(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run}/jobs repository listWorkflowRunJobs // --- // summary: Lists all jobs for a workflow run // produces: @@ -1168,7 +1140,7 @@ func GetWorkflowJobs(ctx *context.APIContext) { // required: true // responses: // "200": - // "$ref": "#/responses/ArtifactsList" + // "$ref": "#/responses/JobList" // "400": // "$ref": "#/responses/error" // "404": @@ -1178,30 +1150,7 @@ func GetWorkflowJobs(ctx *context.APIContext) { 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, nil, artifacts[i]) - if err != nil { - ctx.APIErrorInternal(err) - return - } - res.Entries[i] = convertedWorkflowJob - } - - ctx.JSON(http.StatusOK, &res) + shared.ListJobs(ctx, 0, repoID, runID) } // GetWorkflowJob Gets a specific workflow job for a workflow run. @@ -1229,7 +1178,7 @@ func GetWorkflowJob(ctx *context.APIContext) { // required: true // responses: // "200": - // "$ref": "#/responses/Artifact" + // "$ref": "#/responses/Job" // "400": // "$ref": "#/responses/error" // "404": diff --git a/routers/api/v1/shared/runners.go b/routers/api/v1/shared/runners.go index d42f330d1c..68310cf078 100644 --- a/routers/api/v1/shared/runners.go +++ b/routers/api/v1/shared/runners.go @@ -9,9 +9,13 @@ import ( 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" @@ -116,3 +120,141 @@ 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") + } + jobs, total, err := db.FindAndCount[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{ + OwnerID: ownerID, + 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(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, 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 { + 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 + } +} + +// 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 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, err := user_model.GetUserByName(ctx, actor) + if err != nil { + ctx.APIErrorInternal(err) + return + } + opts.TriggerUserID = user.ID + } + + 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/runners.go b/routers/api/v1/user/runners.go index be3f63cc5e..2409d28685 100644 --- a/routers/api/v1/user/runners.go +++ b/routers/api/v1/user/runners.go @@ -102,3 +102,29 @@ 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 + // produces: + // - application/json + // responses: + // "200": + // "$ref": "#/responses/ActionWorkflowRunsResponse" + 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 + // produces: + // - application/json + // responses: + // "200": + // "$ref": "#/responses/ActionWorkflowJobsResponse" + shared.ListJobs(ctx, ctx.Doer.ID, 0, 0) +} diff --git a/services/actions/interface.go b/services/actions/interface.go index b407f5c6c8..a054c38e4f 100644 --- a/services/actions/interface.go +++ b/services/actions/interface.go @@ -33,4 +33,8 @@ type API interface { GetRunner(*context.APIContext) // DeleteRunner delete runner DeleteRunner(*context.APIContext) + // ListWorkflowJobs list jobs + ListWorkflowJobs(*context.APIContext) + // ListWorkflowRuns list runs + ListWorkflowRuns(*context.APIContext) } diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index f5789f4091..707e226f5e 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -75,6 +75,29 @@ } } }, + "/admin/actions/jobs": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Lists all jobs", + "operationId": "listAdminWorkflowJobs", + "responses": { + "200": { + "$ref": "#/responses/JobList" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/admin/actions/runners": { "get": { "produces": [ @@ -177,6 +200,29 @@ } } }, + "/admin/actions/runs": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Lists all runs", + "operationId": "listAdminWorkflowRuns", + "responses": { + "200": { + "$ref": "#/responses/RunList" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/admin/cron": { "get": { "produces": [ @@ -1799,6 +1845,27 @@ } } }, + "/orgs/{org}/actions/jobs": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Get org-level workflow jobs", + "operationId": "getOrgWorkflowJobs", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + } + ] + } + }, "/orgs/{org}/actions/runners": { "get": { "produces": [ @@ -1957,6 +2024,27 @@ } } }, + "/orgs/{org}/actions/runs": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Get org-level workflow runs", + "operationId": "getOrgWorkflowRuns", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + } + ] + } + }, "/orgs/{org}/actions/secrets": { "get": { "produces": [ @@ -4519,6 +4607,45 @@ } } }, + "/repos/{owner}/{repo}/actions/jobs": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Lists all jobs for a repository", + "operationId": "listWorkflowJobs", + "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 + } + ], + "responses": { + "200": { + "$ref": "#/responses/JobList" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/repos/{owner}/{repo}/actions/jobs/{job_id}": { "get": { "produces": [ @@ -4554,7 +4681,7 @@ ], "responses": { "200": { - "$ref": "#/responses/Artifact" + "$ref": "#/responses/Job" }, "400": { "$ref": "#/responses/error" @@ -4968,7 +5095,7 @@ "repository" ], "summary": "Lists all jobs for a workflow run", - "operationId": "getWorkflowJobs", + "operationId": "listWorkflowRunJobs", "parameters": [ { "type": "string", @@ -4994,7 +5121,7 @@ ], "responses": { "200": { - "$ref": "#/responses/ArtifactsList" + "$ref": "#/responses/JobList" }, "400": { "$ref": "#/responses/error" @@ -17662,6 +17789,23 @@ } } }, + "/user/actions/jobs": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Get workflow jobs", + "operationId": "getUserWorkflowJobs", + "responses": { + "200": { + "$ref": "#/responses/ActionWorkflowJobsResponse" + } + } + } + }, "/user/actions/runners": { "get": { "produces": [ @@ -17779,6 +17923,23 @@ } } }, + "/user/actions/runs": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Get workflow runs", + "operationId": "getUserWorkflowRuns", + "responses": { + "200": { + "$ref": "#/responses/ActionWorkflowRunsResponse" + } + } + } + }, "/user/actions/secrets/{secretname}": { "put": { "consumes": [ diff --git a/tests/integration/workflow_run_api_check_test.go b/tests/integration/workflow_run_api_check_test.go index 25e9ed6475..618830512a 100644 --- a/tests/integration/workflow_run_api_check_test.go +++ b/tests/integration/workflow_run_api_check_test.go @@ -15,17 +15,31 @@ import ( "github.com/stretchr/testify/assert" ) -func TestAPIWorkflowRunRepoApi(t *testing.T) { - defer tests.PrepareTestEnv(t)() - userUsername := "user2" - token := getUserToken(t, userUsername, auth_model.AccessTokenScopeWriteRepository) +func TestAPIWorkflowRun(t *testing.T) { + t.Run("AdminRunner", func(t *testing.T) { + testAPIWorkflowRunBasic(t, "/api/v1/admin/actions/runs", 6, "User1", 802, auth_model.AccessTokenScopeReadAdmin, auth_model.AccessTokenScopeReadRepository) + }) + t.Run("UserRunner", func(t *testing.T) { + testAPIWorkflowRunBasic(t, "/api/v1/user/actions/runs", 1, "User2", 803, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadRepository) + }) + t.Run("OrgRuns", func(t *testing.T) { + testAPIWorkflowRunBasic(t, "/api/v1/orgs/org3/actions/runs", 1, "User1", 802, auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopeReadRepository) + }) + t.Run("RepoRuns", func(t *testing.T) { + testAPIWorkflowRunBasic(t, "/api/v1/repos/org3/repo5/actions/runs", 1, "User2", 802, auth_model.AccessTokenScopeReadRepository) + }) +} - req := NewRequest(t, "GET", "/api/v1/repos/org3/repo5/actions/runs").AddTokenAuth(token) +func testAPIWorkflowRunBasic(t *testing.T, runAPIURL string, itemCount int, userUsername string, runID int64, scope ...auth_model.AccessTokenScope) { + defer tests.PrepareTestEnv(t)() + token := getUserToken(t, userUsername, scope...) + + req := NewRequest(t, "GET", runAPIURL).AddTokenAuth(token) runnerListResp := MakeRequest(t, req, http.StatusOK) runnerList := api.ActionWorkflowRunsResponse{} DecodeJSON(t, runnerListResp, &runnerList) - assert.Len(t, runnerList.Entries, 1) + assert.Len(t, runnerList.Entries, itemCount) foundRun := false @@ -35,7 +49,7 @@ func TestAPIWorkflowRunRepoApi(t *testing.T) { jobList := api.ActionWorkflowJobsResponse{} DecodeJSON(t, jobsResp, &jobList) - if run.ID == 802 { + if run.ID == runID { foundRun = true assert.Len(t, jobList.Entries, 1) for _, job := range jobList.Entries {