From 9f842f0dec6a20edcaae47d91b78028e52ad24f3 Mon Sep 17 00:00:00 2001 From: Jaime merino Date: Tue, 14 Jan 2025 11:17:42 +0000 Subject: Add search action jobs for API routes, repo, org and global level (#6300) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR wants to improve information of the tasks waiting to be executed on a global, organization, user and repository leve. The main motivation is explained here https://codeberg.org/forgejo/discussions/issues/241 ## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). ### Tests - I added test coverage for Go changes... - [x] in their respective `*_test.go` for unit tests. - [x] in the `tests/integration` directory if it involves interactions with a live Forgejo server. - I added test coverage for JavaScript changes... - [ ] in `web_src/js/*.test.js` if it can be unit tested. - [ ] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)). ### Documentation - [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - [x] I did not document these changes and I do not expect someone else to do it. ### Release notes - [ ] I do not want this change to show in the release notes. - [x] I want the title to show in the release notes with a link to this pull request. - [ ] I want the content of the `release-notes/.md` to be be used for the release notes instead of the title. ## Release notes - Features - [PR](https://codeberg.org/forgejo/forgejo/pulls/6300): Add search action jobs for API routes, repo, org and global level Co-authored-by: jaime merino Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6300 Reviewed-by: Earl Warren Co-authored-by: Jaime merino Co-committed-by: Jaime merino --- models/actions/run_job.go | 10 ++ models/actions/run_job_test.go | 29 ++++ models/actions/task.go | 17 +-- models/actions/task_list.go | 6 +- models/fixtures/action_run_job.yml | 45 +++++++ modules/container/set.go | 9 ++ modules/container/set_test.go | 5 + modules/structs/action.go | 25 ++++ routers/api/v1/admin/runners.go | 20 +++ routers/api/v1/api.go | 3 + routers/api/v1/org/action.go | 25 ++++ routers/api/v1/repo/action.go | 30 +++++ routers/api/v1/shared/runners.go | 48 +++++++ routers/api/v1/user/runners.go | 22 +++ routers/web/shared/actions/runners.go | 2 +- services/actions/clear_tasks.go | 4 +- services/actions/interface.go | 2 + templates/swagger/v1_json.tmpl | 202 ++++++++++++++++++++++++++++ tests/integration/api_admin_actions_test.go | 39 ++++++ tests/integration/api_org_actions_test.go | 38 ++++++ tests/integration/api_repo_actions_test.go | 43 ++++++ tests/integration/api_user_actions_test.go | 38 ++++++ 22 files changed, 640 insertions(+), 22 deletions(-) create mode 100644 models/actions/run_job_test.go create mode 100644 modules/structs/action.go create mode 100644 tests/integration/api_admin_actions_test.go create mode 100644 tests/integration/api_org_actions_test.go create mode 100644 tests/integration/api_repo_actions_test.go create mode 100644 tests/integration/api_user_actions_test.go diff --git a/models/actions/run_job.go b/models/actions/run_job.go index de4b6aab66..9f8edfe4fc 100644 --- a/models/actions/run_job.go +++ b/models/actions/run_job.go @@ -10,6 +10,7 @@ import ( "time" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -71,6 +72,15 @@ func (job *ActionRunJob) LoadAttributes(ctx context.Context) error { return job.Run.LoadAttributes(ctx) } +func (job *ActionRunJob) ItRunsOn(labels []string) bool { + if len(labels) == 0 || len(job.RunsOn) == 0 { + return false + } + labelSet := make(container.Set[string]) + labelSet.AddMultiple(labels...) + return labelSet.IsSubset(job.RunsOn) +} + func GetRunJobByID(ctx context.Context, id int64) (*ActionRunJob, error) { var job ActionRunJob has, err := db.GetEngine(ctx).Where("id=?", id).Get(&job) diff --git a/models/actions/run_job_test.go b/models/actions/run_job_test.go new file mode 100644 index 0000000000..50a4ba10d8 --- /dev/null +++ b/models/actions/run_job_test.go @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT + +package actions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestActionRunJob_ItRunsOn(t *testing.T) { + actionJob := ActionRunJob{RunsOn: []string{"ubuntu"}} + agentLabels := []string{"ubuntu", "node-20"} + + assert.True(t, actionJob.ItRunsOn(agentLabels)) + assert.False(t, actionJob.ItRunsOn([]string{})) + + actionJob.RunsOn = append(actionJob.RunsOn, "node-20") + + assert.True(t, actionJob.ItRunsOn(agentLabels)) + + agentLabels = []string{"ubuntu"} + + assert.False(t, actionJob.ItRunsOn(agentLabels)) + + actionJob.RunsOn = []string{} + + assert.False(t, actionJob.ItRunsOn(agentLabels)) +} diff --git a/models/actions/task.go b/models/actions/task.go index 8bd139a2d6..31655d2f1d 100644 --- a/models/actions/task.go +++ b/models/actions/task.go @@ -12,7 +12,6 @@ import ( auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unit" - "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" @@ -245,7 +244,7 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask var job *ActionRunJob log.Trace("runner labels: %v", runner.AgentLabels) for _, v := range jobs { - if isSubset(runner.AgentLabels, v.RunsOn) { + if v.ItRunsOn(runner.AgentLabels) { job = v break } @@ -482,20 +481,6 @@ func FindOldTasksToExpire(ctx context.Context, olderThan timeutil.TimeStamp, lim Find(&tasks) } -func isSubset(set, subset []string) bool { - m := make(container.Set[string], len(set)) - for _, v := range set { - m.Add(v) - } - - for _, v := range subset { - if !m.Contains(v) { - return false - } - } - return true -} - func convertTimestamp(timestamp *timestamppb.Timestamp) timeutil.TimeStamp { if timestamp.GetSeconds() == 0 && timestamp.GetNanos() == 0 { return timeutil.TimeStamp(0) diff --git a/models/actions/task_list.go b/models/actions/task_list.go index df4b43c5ef..502d29e1a3 100644 --- a/models/actions/task_list.go +++ b/models/actions/task_list.go @@ -50,7 +50,7 @@ type FindTaskOptions struct { RepoID int64 OwnerID int64 CommitSHA string - Status Status + Status []Status UpdatedBefore timeutil.TimeStamp StartedBefore timeutil.TimeStamp RunnerID int64 @@ -67,8 +67,8 @@ func (opts FindTaskOptions) ToConds() builder.Cond { if opts.CommitSHA != "" { cond = cond.And(builder.Eq{"commit_sha": opts.CommitSHA}) } - if opts.Status > StatusUnknown { - cond = cond.And(builder.Eq{"status": opts.Status}) + if opts.Status != nil { + cond = cond.And(builder.In("status", opts.Status)) } if opts.UpdatedBefore > 0 { cond = cond.And(builder.Lt{"updated": opts.UpdatedBefore}) diff --git a/models/fixtures/action_run_job.yml b/models/fixtures/action_run_job.yml index 117bb5ea05..702c6bc832 100644 --- a/models/fixtures/action_run_job.yml +++ b/models/fixtures/action_run_job.yml @@ -83,3 +83,48 @@ status: 1 started: 1683636528 stopped: 1683636626 +- + id: 393 + run_id: 891 + repo_id: 1 + owner_id: 1 + commit_sha: 985f0301dba5e7b34be866819cd15ad3d8f508ee + is_fork_pull_request: 0 + name: job_2 + attempt: 1 + job_id: job_2 + task_id: 47 + status: 5 + runs_on: '["ubuntu-latest"]' + started: 1683636528 + stopped: 1683636626 +- + id: 394 + run_id: 891 + repo_id: 1 + owner_id: 2 + commit_sha: 985f0301dba5e7b34be866819cd15ad3d8f508ee + is_fork_pull_request: 0 + name: job_2 + attempt: 1 + job_id: job_2 + task_id: 47 + status: 5 + runs_on: '["debian-latest"]' + started: 1683636528 + stopped: 1683636626 +- + id: 395 + run_id: 891 + repo_id: 1 + owner_id: 3 + commit_sha: 985f0301dba5e7b34be866819cd15ad3d8f508ee + is_fork_pull_request: 0 + name: job_2 + attempt: 1 + job_id: job_2 + task_id: 47 + status: 5 + runs_on: '["fedora"]' + started: 1683636528 + stopped: 1683636626 diff --git a/modules/container/set.go b/modules/container/set.go index 15779983fd..2d654d0aee 100644 --- a/modules/container/set.go +++ b/modules/container/set.go @@ -29,6 +29,15 @@ func (s Set[T]) AddMultiple(values ...T) { } } +func (s Set[T]) IsSubset(subset []T) bool { + for _, v := range subset { + if !s.Contains(v) { + return false + } + } + return true +} + // Contains determines whether a set contains the specified element. // Returns true if the set contains the specified element; otherwise, false. func (s Set[T]) Contains(value T) bool { diff --git a/modules/container/set_test.go b/modules/container/set_test.go index 1502236034..3cfbf7cc2c 100644 --- a/modules/container/set_test.go +++ b/modules/container/set_test.go @@ -33,4 +33,9 @@ func TestSet(t *testing.T) { assert.False(t, s.Contains("key1")) assert.True(t, s.Contains("key6")) assert.True(t, s.Contains("key7")) + + assert.True(t, s.IsSubset([]string{"key6", "key7"})) + assert.False(t, s.IsSubset([]string{"key1"})) + + assert.True(t, s.IsSubset([]string{})) } diff --git a/modules/structs/action.go b/modules/structs/action.go new file mode 100644 index 0000000000..df9f845adc --- /dev/null +++ b/modules/structs/action.go @@ -0,0 +1,25 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package structs + +// ActionRunJob represents a job of a run +// swagger:model +type ActionRunJob struct { + // the action run job id + ID int64 `json:"id"` + // the repository id + RepoID int64 `json:"repo_id"` + // the owner id + OwnerID int64 `json:"owner_id"` + // the action run job name + Name string `json:"name"` + // the action run job needed ids + Needs []string `json:"needs"` + // the action run job labels to run on + RunsOn []string `json:"runs_on"` + // the action run job latest task id + TaskID int64 `json:"task_id"` + // the action run job status + Status string `json:"status"` +} diff --git a/routers/api/v1/admin/runners.go b/routers/api/v1/admin/runners.go index 329242d9f6..d0cfef8e48 100644 --- a/routers/api/v1/admin/runners.go +++ b/routers/api/v1/admin/runners.go @@ -24,3 +24,23 @@ func GetRegistrationToken(ctx *context.APIContext) { shared.GetRegistrationToken(ctx, 0, 0) } + +// SearchActionRunJobs return a list of actions jobs filtered by the provided parameters +func SearchActionRunJobs(ctx *context.APIContext) { + // swagger:operation GET /admin/runners/jobs admin adminSearchRunJobs + // --- + // summary: Search action jobs according filter conditions + // produces: + // - application/json + // parameters: + // - name: labels + // in: query + // description: a comma separated list of run job labels to search for + // type: string + // responses: + // "200": + // "$ref": "#/responses/RunJobList" + // "403": + // "$ref": "#/responses/forbidden" + shared.GetActionRunJobs(ctx, 0, 0) +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 4928c9ff58..89338d6977 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -822,6 +822,7 @@ func Routes() *web.Route { m.Group("/runners", func() { m.Get("/registration-token", reqToken(), reqChecker, act.GetRegistrationToken) + m.Get("/jobs", reqToken(), reqChecker, act.SearchActionRunJobs) }) }) } @@ -975,6 +976,7 @@ func Routes() *web.Route { m.Group("/runners", func() { m.Get("/registration-token", reqToken(), user.GetRegistrationToken) + m.Get("/jobs", reqToken(), user.SearchActionRunJobs) }) }) @@ -1631,6 +1633,7 @@ func Routes() *web.Route { }) m.Group("/runners", func() { m.Get("/registration-token", admin.GetRegistrationToken) + m.Get("/jobs", admin.SearchActionRunJobs) }) if setting.Quota.Enabled { m.Group("/quota", func() { diff --git a/routers/api/v1/org/action.go b/routers/api/v1/org/action.go index 03a1fa8ccc..8cd2e00e00 100644 --- a/routers/api/v1/org/action.go +++ b/routers/api/v1/org/action.go @@ -189,6 +189,31 @@ func (Action) GetRegistrationToken(ctx *context.APIContext) { shared.GetRegistrationToken(ctx, ctx.Org.Organization.ID, 0) } +// SearchActionRunJobs return a list of actions jobs filtered by the provided parameters +func (Action) SearchActionRunJobs(ctx *context.APIContext) { + // swagger:operation GET /orgs/{org}/actions/runners/jobs organization orgSearchRunJobs + // --- + // summary: Search for organization's action jobs according filter conditions + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of the organization + // type: string + // required: true + // - name: labels + // in: query + // description: a comma separated list of run job labels to search for + // type: string + // responses: + // "200": + // "$ref": "#/responses/RunJobList" + // "403": + // "$ref": "#/responses/forbidden" + shared.GetActionRunJobs(ctx, ctx.Org.Organization.ID, 0) +} + // ListVariables list org-level variables func (Action) ListVariables(ctx *context.APIContext) { // swagger:operation GET /orgs/{org}/actions/variables organization getOrgVariablesList diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index 0c7506b13b..2ff52c3744 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -507,6 +507,36 @@ func (Action) GetRegistrationToken(ctx *context.APIContext) { shared.GetRegistrationToken(ctx, 0, ctx.Repo.Repository.ID) } +// SearchActionRunJobs return a list of actions jobs filtered by the provided parameters +func (Action) SearchActionRunJobs(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/runners/jobs repository repoSearchRunJobs + // --- + // summary: Search for repository's action jobs according filter conditions + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: labels + // in: query + // description: a comma separated list of run job labels to search for + // type: string + // responses: + // "200": + // "$ref": "#/responses/RunJobList" + // "403": + // "$ref": "#/responses/forbidden" + shared.GetActionRunJobs(ctx, 0, ctx.Repo.Repository.ID) +} + var _ actions_service.API = new(Action) // Action implements actions_service.API diff --git a/routers/api/v1/shared/runners.go b/routers/api/v1/shared/runners.go index f184786d7d..53761a07e9 100644 --- a/routers/api/v1/shared/runners.go +++ b/routers/api/v1/shared/runners.go @@ -6,8 +6,11 @@ package shared import ( "errors" "net/http" + "strings" actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" ) @@ -30,3 +33,48 @@ func GetRegistrationToken(ctx *context.APIContext, ownerID, repoID int64) { ctx.JSON(http.StatusOK, RegistrationToken{Token: token.Token}) } + +// RunJobList is a list of action run jobs +// swagger:response RunJobList +type RunJobList struct { + // in:body + Body []*structs.ActionRunJob `json:"body"` +} + +func GetActionRunJobs(ctx *context.APIContext, ownerID, repoID int64) { + labels := strings.Split(ctx.FormTrim("labels"), ",") + + total, err := db.Find[actions_model.ActionRunJob](ctx, &actions_model.FindTaskOptions{ + Status: []actions_model.Status{actions_model.StatusWaiting, actions_model.StatusRunning}, + OwnerID: ownerID, + RepoID: repoID, + }) + if err != nil { + ctx.Error(http.StatusInternalServerError, "CountWaitingActionRunJobs", err) + return + } + + res := new(RunJobList) + res.Body = fromRunJobModelToResponse(total, labels) + + ctx.JSON(http.StatusOK, res) +} + +func fromRunJobModelToResponse(job []*actions_model.ActionRunJob, labels []string) []*structs.ActionRunJob { + var res []*structs.ActionRunJob + for i := range job { + if job[i].ItRunsOn(labels) { + res = append(res, &structs.ActionRunJob{ + ID: job[i].ID, + RepoID: job[i].RepoID, + OwnerID: job[i].OwnerID, + Name: job[i].Name, + Needs: job[i].Needs, + RunsOn: job[i].RunsOn, + TaskID: job[i].TaskID, + Status: job[i].Status.String(), + }) + } + } + return res +} diff --git a/routers/api/v1/user/runners.go b/routers/api/v1/user/runners.go index dc4c187ffe..5e8cdbeb58 100644 --- a/routers/api/v1/user/runners.go +++ b/routers/api/v1/user/runners.go @@ -28,3 +28,25 @@ func GetRegistrationToken(ctx *context.APIContext) { shared.GetRegistrationToken(ctx, ctx.Doer.ID, 0) } + +// SearchActionRunJobs return a list of actions jobs filtered by the provided parameters +func SearchActionRunJobs(ctx *context.APIContext) { + // swagger:operation GET /user/actions/runners/jobs user userSearchRunJobs + // --- + // summary: Search for user's action jobs according filter conditions + // produces: + // - application/json + // parameters: + // - name: labels + // in: query + // description: a comma separated list of run job labels to search for + // type: string + // responses: + // "200": + // "$ref": "#/responses/RunJobList" + // "401": + // "$ref": "#/responses/unauthorized" + // "403": + // "$ref": "#/responses/forbidden" + shared.GetActionRunJobs(ctx, ctx.Doer.ID, 0) +} diff --git a/routers/web/shared/actions/runners.go b/routers/web/shared/actions/runners.go index f38933226b..7ed3f88f6c 100644 --- a/routers/web/shared/actions/runners.go +++ b/routers/web/shared/actions/runners.go @@ -79,7 +79,7 @@ func RunnerDetails(ctx *context.Context, page int, runnerID, ownerID, repoID int Page: page, PageSize: 30, }, - Status: actions_model.StatusUnknown, // Unknown means all + Status: []actions_model.Status{actions_model.StatusUnknown}, // Unknown means all RunnerID: runner.ID, } diff --git a/services/actions/clear_tasks.go b/services/actions/clear_tasks.go index 67373782d5..f146c22372 100644 --- a/services/actions/clear_tasks.go +++ b/services/actions/clear_tasks.go @@ -19,7 +19,7 @@ import ( // StopZombieTasks stops the task which have running status, but haven't been updated for a long time func StopZombieTasks(ctx context.Context) error { return stopTasks(ctx, actions_model.FindTaskOptions{ - Status: actions_model.StatusRunning, + Status: []actions_model.Status{actions_model.StatusRunning}, UpdatedBefore: timeutil.TimeStamp(time.Now().Add(-setting.Actions.ZombieTaskTimeout).Unix()), }) } @@ -27,7 +27,7 @@ func StopZombieTasks(ctx context.Context) error { // StopEndlessTasks stops the tasks which have running status and continuous updates, but don't end for a long time func StopEndlessTasks(ctx context.Context) error { return stopTasks(ctx, actions_model.FindTaskOptions{ - Status: actions_model.StatusRunning, + Status: []actions_model.Status{actions_model.StatusRunning}, StartedBefore: timeutil.TimeStamp(time.Now().Add(-setting.Actions.EndlessTaskTimeout).Unix()), }) } diff --git a/services/actions/interface.go b/services/actions/interface.go index d4fa782fec..76bee6f153 100644 --- a/services/actions/interface.go +++ b/services/actions/interface.go @@ -25,4 +25,6 @@ type API interface { UpdateVariable(*context.APIContext) // GetRegistrationToken get registration token GetRegistrationToken(*context.APIContext) + // SearchActionRunJobs get pending Action run jobs + SearchActionRunJobs(*context.APIContext) } diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index f726176883..1832e9d732 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -992,6 +992,34 @@ } } }, + "/admin/runners/jobs": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Search action jobs according filter conditions", + "operationId": "adminSearchRunJobs", + "parameters": [ + { + "type": "string", + "description": "a comma separated list of run job labels to search for", + "name": "labels", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/RunJobList" + }, + "403": { + "$ref": "#/responses/forbidden" + } + } + } + }, "/admin/runners/registration-token": { "get": { "produces": [ @@ -2284,6 +2312,41 @@ } } }, + "/orgs/{org}/actions/runners/jobs": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Search for organization's action jobs according filter conditions", + "operationId": "orgSearchRunJobs", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "a comma separated list of run job labels to search for", + "name": "labels", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/RunJobList" + }, + "403": { + "$ref": "#/responses/forbidden" + } + } + } + }, "/orgs/{org}/actions/runners/registration-token": { "get": { "produces": [ @@ -4639,6 +4702,48 @@ } } }, + "/repos/{owner}/{repo}/actions/runners/jobs": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Search for repository's action jobs according filter conditions", + "operationId": "repoSearchRunJobs", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "a comma separated list of run job labels to search for", + "name": "labels", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/RunJobList" + }, + "403": { + "$ref": "#/responses/forbidden" + } + } + } + }, "/repos/{owner}/{repo}/actions/runners/registration-token": { "get": { "produces": [ @@ -17399,6 +17504,37 @@ } } }, + "/user/actions/runners/jobs": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Search for user's action jobs according filter conditions", + "operationId": "userSearchRunJobs", + "parameters": [ + { + "type": "string", + "description": "a comma separated list of run job labels to search for", + "name": "labels", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/RunJobList" + }, + "401": { + "$ref": "#/responses/unauthorized" + }, + "403": { + "$ref": "#/responses/forbidden" + } + } + } + }, "/user/actions/runners/registration-token": { "get": { "produces": [ @@ -20387,6 +20523,63 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "ActionRunJob": { + "description": "ActionRunJob represents a job of a run", + "type": "object", + "properties": { + "id": { + "description": "the action run job id", + "type": "integer", + "format": "int64", + "x-go-name": "ID" + }, + "name": { + "description": "the action run job name", + "type": "string", + "x-go-name": "Name" + }, + "needs": { + "description": "the action run job needed ids", + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "Needs" + }, + "owner_id": { + "description": "the owner id", + "type": "integer", + "format": "int64", + "x-go-name": "OwnerID" + }, + "repo_id": { + "description": "the repository id", + "type": "integer", + "format": "int64", + "x-go-name": "RepoID" + }, + "runs_on": { + "description": "the action run job labels to run on", + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "RunsOn" + }, + "status": { + "description": "the action run job status", + "type": "string", + "x-go-name": "Status" + }, + "task_id": { + "description": "the action run job latest task id", + "type": "integer", + "format": "int64", + "x-go-name": "TaskID" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "ActionTask": { "description": "ActionTask represents a ActionTask", "type": "object", @@ -28678,6 +28871,15 @@ } } }, + "RunJobList": { + "description": "RunJobList is a list of action run jobs", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/ActionRunJob" + } + } + }, "SearchResults": { "description": "SearchResults", "schema": { diff --git a/tests/integration/api_admin_actions_test.go b/tests/integration/api_admin_actions_test.go new file mode 100644 index 0000000000..22590dc4c4 --- /dev/null +++ b/tests/integration/api_admin_actions_test.go @@ -0,0 +1,39 @@ +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "fmt" + "net/http" + "testing" + + actions_model "code.gitea.io/gitea/models/actions" + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/routers/api/v1/shared" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func TestAPISearchActionJobs_GlobalRunner(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + job := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: 393}) + adminUsername := "user1" + token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeWriteAdmin) + + req := NewRequest( + t, + "GET", + fmt.Sprintf("/api/v1/admin/runners/jobs?labels=%s", "ubuntu-latest"), + ).AddTokenAuth(token) + res := MakeRequest(t, req, http.StatusOK) + + var jobs shared.RunJobList + DecodeJSON(t, res, &jobs) + + assert.Len(t, jobs.Body, 1) + assert.EqualValues(t, job.ID, jobs.Body[0].ID) +} diff --git a/tests/integration/api_org_actions_test.go b/tests/integration/api_org_actions_test.go new file mode 100644 index 0000000000..c8ebbdf293 --- /dev/null +++ b/tests/integration/api_org_actions_test.go @@ -0,0 +1,38 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "fmt" + "net/http" + "testing" + + actions_model "code.gitea.io/gitea/models/actions" + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/routers/api/v1/shared" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func TestAPISearchActionJobs_OrgRunner(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + session := loginUser(t, "user1") + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization) + + job := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: 395}) + + req := NewRequest(t, "GET", + fmt.Sprintf("/api/v1/orgs/org3/actions/runners/jobs?labels=%s", "fedora")). + AddTokenAuth(token) + res := MakeRequest(t, req, http.StatusOK) + + var jobs shared.RunJobList + DecodeJSON(t, res, &jobs) + + assert.Len(t, jobs.Body, 1) + assert.EqualValues(t, job.ID, jobs.Body[0].ID) +} diff --git a/tests/integration/api_repo_actions_test.go b/tests/integration/api_repo_actions_test.go new file mode 100644 index 0000000000..9c3b6aa2b6 --- /dev/null +++ b/tests/integration/api_repo_actions_test.go @@ -0,0 +1,43 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "net/http" + "testing" + + actions_model "code.gitea.io/gitea/models/actions" + auth_model "code.gitea.io/gitea/models/auth" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/routers/api/v1/shared" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func TestAPISearchActionJobs_RepoRunner(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + token := getUserToken(t, user2.LowerName, auth_model.AccessTokenScopeWriteRepository) + job := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: 393}) + + req := NewRequestf( + t, + "GET", + "/api/v1/repos/%s/%s/actions/runners/jobs?labels=%s", + repo.OwnerName, repo.Name, + "ubuntu-latest", + ).AddTokenAuth(token) + res := MakeRequest(t, req, http.StatusOK) + + var jobs shared.RunJobList + DecodeJSON(t, res, &jobs) + + assert.Len(t, jobs.Body, 1) + assert.EqualValues(t, job.ID, jobs.Body[0].ID) +} diff --git a/tests/integration/api_user_actions_test.go b/tests/integration/api_user_actions_test.go new file mode 100644 index 0000000000..f9c9c1df4e --- /dev/null +++ b/tests/integration/api_user_actions_test.go @@ -0,0 +1,38 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "fmt" + "net/http" + "testing" + + actions_model "code.gitea.io/gitea/models/actions" + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/routers/api/v1/shared" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func TestAPISearchActionJobs_UserRunner(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + normalUsername := "user2" + session := loginUser(t, normalUsername) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser) + job := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: 394}) + + req := NewRequest(t, "GET", + fmt.Sprintf("/api/v1/user/actions/runners/jobs?labels=%s", "debian-latest")). + AddTokenAuth(token) + res := MakeRequest(t, req, http.StatusOK) + + var jobs shared.RunJobList + DecodeJSON(t, res, &jobs) + + assert.Len(t, jobs.Body, 1) + assert.EqualValues(t, job.ID, jobs.Body[0].ID) +} -- cgit v1.2.3