mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-01-20 16:50:28 -05:00
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>
This commit is contained in:
parent
a013acb632
commit
9f842f0dec
22 changed files with 640 additions and 22 deletions
|
@ -10,6 +10,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
|
"code.gitea.io/gitea/modules/container"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
|
@ -71,6 +72,15 @@ func (job *ActionRunJob) LoadAttributes(ctx context.Context) error {
|
||||||
return job.Run.LoadAttributes(ctx)
|
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) {
|
func GetRunJobByID(ctx context.Context, id int64) (*ActionRunJob, error) {
|
||||||
var job ActionRunJob
|
var job ActionRunJob
|
||||||
has, err := db.GetEngine(ctx).Where("id=?", id).Get(&job)
|
has, err := db.GetEngine(ctx).Where("id=?", id).Get(&job)
|
||||||
|
|
29
models/actions/run_job_test.go
Normal file
29
models/actions/run_job_test.go
Normal file
|
@ -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))
|
||||||
|
}
|
|
@ -12,7 +12,6 @@ import (
|
||||||
auth_model "code.gitea.io/gitea/models/auth"
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
"code.gitea.io/gitea/modules/container"
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
@ -245,7 +244,7 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
|
||||||
var job *ActionRunJob
|
var job *ActionRunJob
|
||||||
log.Trace("runner labels: %v", runner.AgentLabels)
|
log.Trace("runner labels: %v", runner.AgentLabels)
|
||||||
for _, v := range jobs {
|
for _, v := range jobs {
|
||||||
if isSubset(runner.AgentLabels, v.RunsOn) {
|
if v.ItRunsOn(runner.AgentLabels) {
|
||||||
job = v
|
job = v
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -482,20 +481,6 @@ func FindOldTasksToExpire(ctx context.Context, olderThan timeutil.TimeStamp, lim
|
||||||
Find(&tasks)
|
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 {
|
func convertTimestamp(timestamp *timestamppb.Timestamp) timeutil.TimeStamp {
|
||||||
if timestamp.GetSeconds() == 0 && timestamp.GetNanos() == 0 {
|
if timestamp.GetSeconds() == 0 && timestamp.GetNanos() == 0 {
|
||||||
return timeutil.TimeStamp(0)
|
return timeutil.TimeStamp(0)
|
||||||
|
|
|
@ -50,7 +50,7 @@ type FindTaskOptions struct {
|
||||||
RepoID int64
|
RepoID int64
|
||||||
OwnerID int64
|
OwnerID int64
|
||||||
CommitSHA string
|
CommitSHA string
|
||||||
Status Status
|
Status []Status
|
||||||
UpdatedBefore timeutil.TimeStamp
|
UpdatedBefore timeutil.TimeStamp
|
||||||
StartedBefore timeutil.TimeStamp
|
StartedBefore timeutil.TimeStamp
|
||||||
RunnerID int64
|
RunnerID int64
|
||||||
|
@ -67,8 +67,8 @@ func (opts FindTaskOptions) ToConds() builder.Cond {
|
||||||
if opts.CommitSHA != "" {
|
if opts.CommitSHA != "" {
|
||||||
cond = cond.And(builder.Eq{"commit_sha": opts.CommitSHA})
|
cond = cond.And(builder.Eq{"commit_sha": opts.CommitSHA})
|
||||||
}
|
}
|
||||||
if opts.Status > StatusUnknown {
|
if opts.Status != nil {
|
||||||
cond = cond.And(builder.Eq{"status": opts.Status})
|
cond = cond.And(builder.In("status", opts.Status))
|
||||||
}
|
}
|
||||||
if opts.UpdatedBefore > 0 {
|
if opts.UpdatedBefore > 0 {
|
||||||
cond = cond.And(builder.Lt{"updated": opts.UpdatedBefore})
|
cond = cond.And(builder.Lt{"updated": opts.UpdatedBefore})
|
||||||
|
|
|
@ -83,3 +83,48 @@
|
||||||
status: 1
|
status: 1
|
||||||
started: 1683636528
|
started: 1683636528
|
||||||
stopped: 1683636626
|
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
|
||||||
|
|
|
@ -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.
|
// Contains determines whether a set contains the specified element.
|
||||||
// Returns true if the set contains the specified element; otherwise, false.
|
// Returns true if the set contains the specified element; otherwise, false.
|
||||||
func (s Set[T]) Contains(value T) bool {
|
func (s Set[T]) Contains(value T) bool {
|
||||||
|
|
|
@ -33,4 +33,9 @@ func TestSet(t *testing.T) {
|
||||||
assert.False(t, s.Contains("key1"))
|
assert.False(t, s.Contains("key1"))
|
||||||
assert.True(t, s.Contains("key6"))
|
assert.True(t, s.Contains("key6"))
|
||||||
assert.True(t, s.Contains("key7"))
|
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{}))
|
||||||
}
|
}
|
||||||
|
|
25
modules/structs/action.go
Normal file
25
modules/structs/action.go
Normal file
|
@ -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"`
|
||||||
|
}
|
|
@ -24,3 +24,23 @@ func GetRegistrationToken(ctx *context.APIContext) {
|
||||||
|
|
||||||
shared.GetRegistrationToken(ctx, 0, 0)
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -822,6 +822,7 @@ func Routes() *web.Route {
|
||||||
|
|
||||||
m.Group("/runners", func() {
|
m.Group("/runners", func() {
|
||||||
m.Get("/registration-token", reqToken(), reqChecker, act.GetRegistrationToken)
|
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.Group("/runners", func() {
|
||||||
m.Get("/registration-token", reqToken(), user.GetRegistrationToken)
|
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.Group("/runners", func() {
|
||||||
m.Get("/registration-token", admin.GetRegistrationToken)
|
m.Get("/registration-token", admin.GetRegistrationToken)
|
||||||
|
m.Get("/jobs", admin.SearchActionRunJobs)
|
||||||
})
|
})
|
||||||
if setting.Quota.Enabled {
|
if setting.Quota.Enabled {
|
||||||
m.Group("/quota", func() {
|
m.Group("/quota", func() {
|
||||||
|
|
|
@ -189,6 +189,31 @@ func (Action) GetRegistrationToken(ctx *context.APIContext) {
|
||||||
shared.GetRegistrationToken(ctx, ctx.Org.Organization.ID, 0)
|
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
|
// ListVariables list org-level variables
|
||||||
func (Action) ListVariables(ctx *context.APIContext) {
|
func (Action) ListVariables(ctx *context.APIContext) {
|
||||||
// swagger:operation GET /orgs/{org}/actions/variables organization getOrgVariablesList
|
// swagger:operation GET /orgs/{org}/actions/variables organization getOrgVariablesList
|
||||||
|
|
|
@ -507,6 +507,36 @@ func (Action) GetRegistrationToken(ctx *context.APIContext) {
|
||||||
shared.GetRegistrationToken(ctx, 0, ctx.Repo.Repository.ID)
|
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)
|
var _ actions_service.API = new(Action)
|
||||||
|
|
||||||
// Action implements actions_service.API
|
// Action implements actions_service.API
|
||||||
|
|
|
@ -6,8 +6,11 @@ package shared
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
actions_model "code.gitea.io/gitea/models/actions"
|
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/modules/util"
|
||||||
"code.gitea.io/gitea/services/context"
|
"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})
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -28,3 +28,25 @@ func GetRegistrationToken(ctx *context.APIContext) {
|
||||||
|
|
||||||
shared.GetRegistrationToken(ctx, ctx.Doer.ID, 0)
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -79,7 +79,7 @@ func RunnerDetails(ctx *context.Context, page int, runnerID, ownerID, repoID int
|
||||||
Page: page,
|
Page: page,
|
||||||
PageSize: 30,
|
PageSize: 30,
|
||||||
},
|
},
|
||||||
Status: actions_model.StatusUnknown, // Unknown means all
|
Status: []actions_model.Status{actions_model.StatusUnknown}, // Unknown means all
|
||||||
RunnerID: runner.ID,
|
RunnerID: runner.ID,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ import (
|
||||||
// StopZombieTasks stops the task which have running status, but haven't been updated for a long time
|
// StopZombieTasks stops the task which have running status, but haven't been updated for a long time
|
||||||
func StopZombieTasks(ctx context.Context) error {
|
func StopZombieTasks(ctx context.Context) error {
|
||||||
return stopTasks(ctx, actions_model.FindTaskOptions{
|
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()),
|
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
|
// 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 {
|
func StopEndlessTasks(ctx context.Context) error {
|
||||||
return stopTasks(ctx, actions_model.FindTaskOptions{
|
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()),
|
StartedBefore: timeutil.TimeStamp(time.Now().Add(-setting.Actions.EndlessTaskTimeout).Unix()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,4 +25,6 @@ type API interface {
|
||||||
UpdateVariable(*context.APIContext)
|
UpdateVariable(*context.APIContext)
|
||||||
// GetRegistrationToken get registration token
|
// GetRegistrationToken get registration token
|
||||||
GetRegistrationToken(*context.APIContext)
|
GetRegistrationToken(*context.APIContext)
|
||||||
|
// SearchActionRunJobs get pending Action run jobs
|
||||||
|
SearchActionRunJobs(*context.APIContext)
|
||||||
}
|
}
|
||||||
|
|
202
templates/swagger/v1_json.tmpl
generated
202
templates/swagger/v1_json.tmpl
generated
|
@ -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": {
|
"/admin/runners/registration-token": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"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": {
|
"/orgs/{org}/actions/runners/registration-token": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"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": {
|
"/repos/{owner}/{repo}/actions/runners/registration-token": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"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": {
|
"/user/actions/runners/registration-token": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"produces": [
|
||||||
|
@ -20387,6 +20523,63 @@
|
||||||
},
|
},
|
||||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
"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": {
|
"ActionTask": {
|
||||||
"description": "ActionTask represents a ActionTask",
|
"description": "ActionTask represents a ActionTask",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
@ -28678,6 +28871,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"RunJobList": {
|
||||||
|
"description": "RunJobList is a list of action run jobs",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/ActionRunJob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"SearchResults": {
|
"SearchResults": {
|
||||||
"description": "SearchResults",
|
"description": "SearchResults",
|
||||||
"schema": {
|
"schema": {
|
||||||
|
|
39
tests/integration/api_admin_actions_test.go
Normal file
39
tests/integration/api_admin_actions_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
38
tests/integration/api_org_actions_test.go
Normal file
38
tests/integration/api_org_actions_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
43
tests/integration/api_repo_actions_test.go
Normal file
43
tests/integration/api_repo_actions_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
38
tests/integration/api_user_actions_test.go
Normal file
38
tests/integration/api_user_actions_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue