summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJaime merino <cobak78@gmail.com>2025-01-14 12:17:42 +0100
committerEarl Warren <earl-warren@noreply.codeberg.org>2025-01-14 12:17:42 +0100
commit9f842f0dec6a20edcaae47d91b78028e52ad24f3 (patch)
treeeea35a92387c585adb3e4f99ee59891850871a27
parentUpdate https://data.forgejo.org/infrastructure/issue-action action to v1.3.0 ... (diff)
downloadforgejo-9f842f0dec6a20edcaae47d91b78028e52ad24f3.tar.xz
forgejo-9f842f0dec6a20edcaae47d91b78028e52ad24f3.zip
Add search action jobs for API routes, repo, org and global level (#6300)
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/<pull request number>.md` to be be used for the release notes instead of the title. <!--start release-notes-assistant--> ## Release notes <!--URL:https://codeberg.org/forgejo/forgejo--> - Features - [PR](https://codeberg.org/forgejo/forgejo/pulls/6300): <!--number 6300 --><!--line 0 --><!--description QWRkIHNlYXJjaCBhY3Rpb24gam9icyBmb3IgQVBJIHJvdXRlcywgcmVwbywgb3JnIGFuZCBnbG9iYWwgbGV2ZWw=-->Add search action jobs for API routes, repo, org and global level<!--description--> <!--end release-notes-assistant--> Co-authored-by: jaime merino <jaime.merino_mora@mail.schwarzª> Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6300 Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org> Co-authored-by: Jaime merino <cobak78@gmail.com> Co-committed-by: Jaime merino <cobak78@gmail.com>
-rw-r--r--models/actions/run_job.go10
-rw-r--r--models/actions/run_job_test.go29
-rw-r--r--models/actions/task.go17
-rw-r--r--models/actions/task_list.go6
-rw-r--r--models/fixtures/action_run_job.yml45
-rw-r--r--modules/container/set.go9
-rw-r--r--modules/container/set_test.go5
-rw-r--r--modules/structs/action.go25
-rw-r--r--routers/api/v1/admin/runners.go20
-rw-r--r--routers/api/v1/api.go3
-rw-r--r--routers/api/v1/org/action.go25
-rw-r--r--routers/api/v1/repo/action.go30
-rw-r--r--routers/api/v1/shared/runners.go48
-rw-r--r--routers/api/v1/user/runners.go22
-rw-r--r--routers/web/shared/actions/runners.go2
-rw-r--r--services/actions/clear_tasks.go4
-rw-r--r--services/actions/interface.go2
-rw-r--r--templates/swagger/v1_json.tmpl202
-rw-r--r--tests/integration/api_admin_actions_test.go39
-rw-r--r--tests/integration/api_org_actions_test.go38
-rw-r--r--tests/integration/api_repo_actions_test.go43
-rw-r--r--tests/integration/api_user_actions_test.go38
22 files changed, 640 insertions, 22 deletions
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)
+}