From fc35915a28239bd40317fa8240d0ae23e01e2086 Mon Sep 17 00:00:00 2001 From: klausfyhn Date: Mon, 2 Jun 2025 22:05:12 +0200 Subject: [PATCH] feat: make action runs available in api (#7699) ## Summary Inspired by https://docs.github.com/en/rest/actions/workflow-runs?apiVersion=2022-11-28#list-workflow-runs-for-a-repository and https://docs.github.com/en/rest/actions/workflow-runs?apiVersion=2022-11-28#get-a-workflow-run ## 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... - [ ] 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/7699): make action runs available in api Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/7699 Reviewed-by: Earl Warren Co-authored-by: klausfyhn Co-committed-by: klausfyhn --- models/actions/run_job_list.go | 8 + models/actions/status.go | 14 ++ models/fixtures/action_run.yml | 61 +++++ models/fixtures/repo_unit.yml | 7 + models/fixtures/repository.yml | 32 +++ models/fixtures/user.yml | 2 +- modules/structs/repo_actions.go | 20 ++ routers/api/v1/api.go | 4 + routers/api/v1/repo/action.go | 158 +++++++++++++ routers/api/v1/swagger/repo.go | 14 ++ services/convert/convert.go | 23 ++ templates/swagger/v1_json.tmpl | 221 ++++++++++++++++++ .../test_action_run_search.git/COMMIT_EDITMSG | 1 + .../test_action_run_search.git/FETCH_HEAD | 0 .../user2/test_action_run_search.git/HEAD | 1 + .../user2/test_action_run_search.git/config | 5 + .../test_action_run_search.git/description | 1 + .../user2/test_action_run_search.git/index | Bin 0 -> 420 bytes .../test_action_run_search.git/info/exclude | 6 + .../test_action_run_search.git/logs/HEAD | 7 + .../logs/refs/heads/bugfix-1 | 2 + .../logs/refs/heads/main | 1 + .../05/daa94891793ad0f183273cfb9902b903782e2a | Bin 0 -> 248 bytes .../18/722d8d68a60af7e66213ef4a15d05dae365f2b | Bin 0 -> 139 bytes .../35/c5cddfc19397501ec8f4f7bb808a7c8f04445f | Bin 0 -> 392 bytes .../76/ebce62dde5d6c40cfdda5782ff171f02a618b9 | Bin 0 -> 53 bytes .../7f/56afb8a0b9ffbc39e8edd537f252412254d112 | Bin 0 -> 52 bytes .../8d/6860a6d6bb80661d308b060509ef41f099c390 | Bin 0 -> 87 bytes .../8f/2069876727126f91aa756d84caa59f93670dfc | Bin 0 -> 149 bytes .../97/f29ee599c373c729132a5c46a046978311e0ee | Bin 0 -> 345 bytes .../a5/3a19de56c7594844459c2951497c15dba8adee | Bin 0 -> 23 bytes .../a6/2cdde48ab95bb49294c30cf0971ce2511008c5 | Bin 0 -> 120 bytes .../refs/heads/bugfix-1 | 1 + .../refs/heads/main | 1 + .../test_action_run_search.git/refs/tags/v1 | 1 + tests/integration/api_repo_actions_test.go | 140 +++++++++++ 36 files changed, 730 insertions(+), 1 deletion(-) create mode 100644 tests/gitea-repositories-meta/user2/test_action_run_search.git/COMMIT_EDITMSG create mode 100644 tests/gitea-repositories-meta/user2/test_action_run_search.git/FETCH_HEAD create mode 100644 tests/gitea-repositories-meta/user2/test_action_run_search.git/HEAD create mode 100644 tests/gitea-repositories-meta/user2/test_action_run_search.git/config create mode 100644 tests/gitea-repositories-meta/user2/test_action_run_search.git/description create mode 100644 tests/gitea-repositories-meta/user2/test_action_run_search.git/index create mode 100644 tests/gitea-repositories-meta/user2/test_action_run_search.git/info/exclude create mode 100644 tests/gitea-repositories-meta/user2/test_action_run_search.git/logs/HEAD create mode 100644 tests/gitea-repositories-meta/user2/test_action_run_search.git/logs/refs/heads/bugfix-1 create mode 100644 tests/gitea-repositories-meta/user2/test_action_run_search.git/logs/refs/heads/main create mode 100644 tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/05/daa94891793ad0f183273cfb9902b903782e2a create mode 100644 tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/18/722d8d68a60af7e66213ef4a15d05dae365f2b create mode 100644 tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/35/c5cddfc19397501ec8f4f7bb808a7c8f04445f create mode 100644 tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/76/ebce62dde5d6c40cfdda5782ff171f02a618b9 create mode 100644 tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/7f/56afb8a0b9ffbc39e8edd537f252412254d112 create mode 100644 tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/8d/6860a6d6bb80661d308b060509ef41f099c390 create mode 100644 tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/8f/2069876727126f91aa756d84caa59f93670dfc create mode 100644 tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/97/f29ee599c373c729132a5c46a046978311e0ee create mode 100644 tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/a5/3a19de56c7594844459c2951497c15dba8adee create mode 100644 tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/a6/2cdde48ab95bb49294c30cf0971ce2511008c5 create mode 100644 tests/gitea-repositories-meta/user2/test_action_run_search.git/refs/heads/bugfix-1 create mode 100644 tests/gitea-repositories-meta/user2/test_action_run_search.git/refs/heads/main create mode 100644 tests/gitea-repositories-meta/user2/test_action_run_search.git/refs/tags/v1 diff --git a/models/actions/run_job_list.go b/models/actions/run_job_list.go index cbcb4beb8e..afc754f26a 100644 --- a/models/actions/run_job_list.go +++ b/models/actions/run_job_list.go @@ -54,6 +54,8 @@ type FindRunJobOptions struct { CommitSHA string Statuses []Status UpdatedBefore timeutil.TimeStamp + Events []string // []webhook_module.HookEventType + RunNumber int64 } func (opts FindRunJobOptions) ToConds() builder.Cond { @@ -76,5 +78,11 @@ func (opts FindRunJobOptions) ToConds() builder.Cond { if opts.UpdatedBefore > 0 { cond = cond.And(builder.Lt{"updated": opts.UpdatedBefore}) } + if len(opts.Events) > 0 { + cond = cond.And(builder.In("event", opts.Events)) + } + if opts.RunNumber > 0 { + cond = cond.And(builder.Eq{"`index`": opts.RunNumber}) + } return cond } diff --git a/models/actions/status.go b/models/actions/status.go index f4357af731..e42c221121 100644 --- a/models/actions/status.go +++ b/models/actions/status.go @@ -34,6 +34,15 @@ var statusNames = map[Status]string{ StatusBlocked: "blocked", } +var nameToStatus = make(map[string]Status, len(statusNames)) + +func init() { + // Populate name to status lookup map + for status, name := range statusNames { + nameToStatus[name] = status + } +} + // String returns the string name of the Status func (s Status) String() string { return statusNames[s] @@ -102,3 +111,8 @@ func (s Status) AsResult() runnerv1.Result { } return runnerv1.Result_RESULT_UNSPECIFIED } + +func StatusFromString(name string) (Status, bool) { + status, exists := nameToStatus[name] + return status, exists +} diff --git a/models/fixtures/action_run.yml b/models/fixtures/action_run.yml index 7a7bf34197..5b6f89ae0e 100644 --- a/models/fixtures/action_run.yml +++ b/models/fixtures/action_run.yml @@ -471,3 +471,64 @@ need_approval: 0 approved_by: 0 event_payload: '{"head_commit":{"id":"5f22f7d0d95d614d25a5b68592adb345a4b5c7fd"}}' + + +# GET action run(s) test +- + id: 892 + title: "successful push run" + repo_id: 63 + owner_id: 2 + workflow_id: "success.yaml" + index: 1 + trigger_user_id: 2 + ref: "refs/heads/main" + commit_sha: "97f29ee599c373c729132a5c46a046978311e0ee" + event: "push" + is_fork_pull_request: 0 + status: 1 # success + started: 1683636528 + stopped: 1683636626 + created: 1683636108 + updated: 1683636626 + need_approval: 0 + approved_by: 0 + +- + id: 893 + title: "failed pull_request run" + repo_id: 63 + owner_id: 2 + workflow_id: "failed.yaml" + index: 2 + trigger_user_id: 2 + ref: "refs/heads/bugfix-1" + commit_sha: "35c5cddfc19397501ec8f4f7bb808a7c8f04445f" + event: "pull_request" + is_fork_pull_request: 0 + status: 2 # failure + started: 1683636528 + stopped: 1683636626 + created: 1683636108 + updated: 1683636626 + need_approval: 0 + approved_by: 0 + +- + id: 894 + title: "running workflow_dispatch run" + repo_id: 63 + owner_id: 2 + workflow_id: "running.yaml" + index: 3 + trigger_user_id: 2 + ref: "refs/heads/main" + commit_sha: "97f29ee599c373c729132a5c46a046978311e0ee" + event: "workflow_dispatch" + is_fork_pull_request: 0 + status: 6 # running + started: 1683636528 + created: 1683636108 + updated: 1683636626 + need_approval: 0 + approved_by: 0 diff --git a/models/fixtures/repo_unit.yml b/models/fixtures/repo_unit.yml index cd49a51796..773f238645 100644 --- a/models/fixtures/repo_unit.yml +++ b/models/fixtures/repo_unit.yml @@ -795,3 +795,10 @@ type: 10 config: "{}" created_unix: 946684810 + +- + id: 115 + repo_id: 63 + type: 10 + config: "{}" + created_unix: 946684810 diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml index 5941f82299..c383fa43ac 100644 --- a/models/fixtures/repository.yml +++ b/models/fixtures/repository.yml @@ -1889,3 +1889,35 @@ is_fsck_enabled: true close_issues_via_commit_in_any_branch: false topics: '[]' + +- + id: 63 + owner_id: 2 + owner_name: user2 + lower_name: test_action_run_search + name: test_action_run_search + default_branch: main + num_watches: 0 + num_stars: 0 + num_forks: 0 + num_issues: 0 + num_closed_issues: 0 + num_pulls: 0 + num_closed_pulls: 0 + num_milestones: 0 + num_closed_milestones: 0 + num_projects: 0 + num_closed_projects: 0 + is_private: true + is_empty: false + is_archived: false + is_mirror: false + status: 0 + is_fork: false + fork_id: 0 + is_template: false + template_id: 0 + size: 0 + is_fsck_enabled: true + close_issues_via_commit_in_any_branch: false + topics: '[]' diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml index 82a1d28023..1a03185cf1 100644 --- a/models/fixtures/user.yml +++ b/models/fixtures/user.yml @@ -70,7 +70,7 @@ num_followers: 2 num_following: 1 num_stars: 2 - num_repos: 17 + num_repos: 18 num_teams: 0 num_members: 0 visibility: 0 diff --git a/modules/structs/repo_actions.go b/modules/structs/repo_actions.go index b13f344738..981baddb91 100644 --- a/modules/structs/repo_actions.go +++ b/modules/structs/repo_actions.go @@ -32,3 +32,23 @@ type ActionTaskResponse struct { Entries []*ActionTask `json:"workflow_runs"` TotalCount int64 `json:"total_count"` } + +// ActionRun represents an ActionRun +type ActionRun struct { + ID int64 `json:"id"` + Name string `json:"name"` + RunNumber int64 `json:"run_number"` + Event string `json:"event"` + Status string `json:"status"` + HeadBranch string `json:"head_branch"` + HeadSHA string `json:"head_sha"` + WorkflowID string `json:"workflow_id"` + URL string `json:"url"` + TriggeringActor *User `json:"triggering_actor"` +} + +// ListActionRunResponse return a list of ActionRun +type ListActionRunResponse struct { + Entries []*ActionRun `json:"workflow_runs"` + TotalCount int64 `json:"total_count"` +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 18624df246..e371ebb28b 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1172,6 +1172,10 @@ func Routes() *web.Route { }, reqToken(), reqAdmin()) m.Group("/actions", func() { m.Get("/tasks", repo.ListActionTasks) + m.Group("/runs", func() { + m.Get("", repo.ListActionRuns) + m.Get("/{run_id}", repo.GetActionRun) + }) m.Group("/workflows", func() { m.Group("/{workflowname}", func() { diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index a39d4836e1..2f0f2c17cd 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -5,6 +5,7 @@ package repo import ( "errors" + "fmt" "net/http" actions_model "forgejo.org/models/actions" @@ -694,3 +695,160 @@ func DispatchWorkflow(ctx *context.APIContext) { ctx.JSON(http.StatusNoContent, nil) } } + +// ListActionRuns return a filtered list of ActionRun +func ListActionRuns(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/runs repository ListActionRuns + // --- + // summary: List a repository's action runs + // 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: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results, default maximum page size is 50 + // type: integer + // - name: event + // in: query + // description: Returns workflow run triggered by the specified events. For example, `push`, `pull_request` or `workflow_dispatch`. + // type: array + // items: + // type: string + // - name: status + // in: query + // description: | + // Returns workflow runs with the check run status or conclusion that is specified. For example, a conclusion can be success or a status can be in_progress. Only Forgejo Actions can set a status of waiting, pending, or requested. + // type: array + // items: + // type: string + // enum: [unknown, waiting, running, success, failure, cancelled, skipped, blocked] + // - name: run_number + // in: query + // description: | + // Returns the workflow run associated with the run number. + // type: integer + // format: int64 + // - name: head_sha + // in: query + // description: Only returns workflow runs that are associated with the specified head_sha. + // type: string + // responses: + // "200": + // "$ref": "#/responses/ActionRunList" + // "400": + // "$ref": "#/responses/error" + // "403": + // "$ref": "#/responses/forbidden" + + statusStrs := ctx.FormStrings("status") + statuses := make([]actions_model.Status, len(statusStrs)) + for i, s := range statusStrs { + if status, exists := actions_model.StatusFromString(s); exists { + statuses[i] = status + } else { + ctx.Error(http.StatusBadRequest, "StatusFromString", fmt.Sprintf("unknown status: %s", s)) + return + } + } + + runs, total, err := db.FindAndCount[actions_model.ActionRun](ctx, &actions_model.FindRunJobOptions{ + ListOptions: utils.GetListOptions(ctx), + OwnerID: ctx.Repo.Owner.ID, + RepoID: ctx.Repo.Repository.ID, + Events: ctx.FormStrings("event"), + Statuses: statuses, + RunNumber: ctx.FormInt64("run_number"), + CommitSHA: ctx.FormString("head_sha"), + }) + if err != nil { + ctx.Error(http.StatusInternalServerError, "ListActionRuns", err) + return + } + + res := new(api.ListActionRunResponse) + res.TotalCount = total + + res.Entries = make([]*api.ActionRun, len(runs)) + for i, r := range runs { + cr, err := convert.ToActionRun(ctx, r) + if err != nil { + ctx.Error(http.StatusInternalServerError, "ToActionRun", err) + return + } + res.Entries[i] = cr + } + + ctx.JSON(http.StatusOK, &res) +} + +// GetActionRun get one action instance +func GetActionRun(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run_id} repository ActionRun + // --- + // summary: Get an action run + // 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: run_id + // in: path + // description: id of the action run + // type: integer + // format: int64 + // required: true + // responses: + // "200": + // "$ref": "#/responses/ActionRun" + // "400": + // "$ref": "#/responses/error" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + + run, err := actions_model.GetRunByID(ctx, ctx.ParamsInt64(":run_id")) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.Error(http.StatusNotFound, "GetRunById", err) + } else { + ctx.Error(http.StatusInternalServerError, "GetRunByID", err) + } + return + } + + if ctx.Repo.Repository.ID != run.RepoID { + ctx.Error(http.StatusNotFound, "GetRunById", util.ErrNotExist) + return + } + + res, err := convert.ToActionRun(ctx, run) + if err != nil { + ctx.Error(http.StatusInternalServerError, "ToActionRun", err) + return + } + + ctx.JSON(http.StatusOK, res) +} diff --git a/routers/api/v1/swagger/repo.go b/routers/api/v1/swagger/repo.go index bf7e2cc0c3..629f207ca3 100644 --- a/routers/api/v1/swagger/repo.go +++ b/routers/api/v1/swagger/repo.go @@ -455,3 +455,17 @@ type swaggerSyncForkInfo struct { // in:body Body []api.SyncForkInfo `json:"body"` } + +// ActionRunList +// swagger:response ActionRunList +type swaggerRepoActionRunList struct { + // in:body + Body api.ListActionRunResponse `json:"body"` +} + +// ActionRun +// swagger:response ActionRun +type swaggerRepoActionRun struct { + // in:body + Body api.ActionRun `json:"body"` +} diff --git a/services/convert/convert.go b/services/convert/convert.go index 2ea24a1b51..45e5a1994b 100644 --- a/services/convert/convert.go +++ b/services/convert/convert.go @@ -222,6 +222,29 @@ func ToActionTask(ctx context.Context, t *actions_model.ActionTask) (*api.Action }, nil } +// ToActionRun convert a actions_model.ActionRun to an api.ActionRun +func ToActionRun(ctx context.Context, r *actions_model.ActionRun) (*api.ActionRun, error) { + if err := r.LoadAttributes(ctx); err != nil { + return nil, err + } + + url := strings.TrimSuffix(setting.AppURL, "/") + r.Link() + actor := ToUser(ctx, r.TriggerUser, nil) + + return &api.ActionRun{ + ID: r.ID, + Name: r.Title, + HeadBranch: r.PrettyRef(), + HeadSHA: r.CommitSHA, + RunNumber: r.Index, + Event: r.TriggerEvent, + Status: r.Status.String(), + WorkflowID: r.WorkflowID, + URL: url, + TriggeringActor: actor, + }, nil +} + // ToVerification convert a git.Commit.Signature to an api.PayloadCommitVerification func ToVerification(ctx context.Context, c *git.Commit) *api.PayloadCommitVerification { verif := asymkey_model.ParseCommitWithSignature(ctx, c) diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 888b39ec82..d2957a0f86 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -4904,6 +4904,148 @@ } } }, + "/repos/{owner}/{repo}/actions/runs": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "List a repository's action runs", + "operationId": "ListActionRuns", + "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": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results, default maximum page size is 50", + "name": "limit", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "description": "Returns workflow run triggered by the specified events. For example, `push`, `pull_request` or `workflow_dispatch`.", + "name": "event", + "in": "query" + }, + { + "type": "array", + "items": { + "enum": [ + "unknown", + "waiting", + "running", + "success", + "failure", + "cancelled", + "skipped", + "blocked" + ], + "type": "string" + }, + "description": "Returns workflow runs with the check run status or conclusion that is specified. For example, a conclusion can be success or a status can be in_progress. Only Forgejo Actions can set a status of waiting, pending, or requested.\n", + "name": "status", + "in": "query" + }, + { + "type": "integer", + "format": "int64", + "description": "Returns the workflow run associated with the run number.\n", + "name": "run_number", + "in": "query" + }, + { + "type": "string", + "description": "Only returns workflow runs that are associated with the specified head_sha.", + "name": "head_sha", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/ActionRunList" + }, + "400": { + "$ref": "#/responses/error" + }, + "403": { + "$ref": "#/responses/forbidden" + } + } + } + }, + "/repos/{owner}/{repo}/actions/runs/{run_id}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Get an action run", + "operationId": "ActionRun", + "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": "integer", + "format": "int64", + "description": "id of the action run", + "name": "run_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/ActionRun" + }, + "400": { + "$ref": "#/responses/error" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/repos/{owner}/{repo}/actions/secrets": { "get": { "produces": [ @@ -20925,6 +21067,54 @@ }, "x-go-package": "forgejo.org/modules/structs" }, + "ActionRun": { + "description": "ActionRun represents an ActionRun", + "type": "object", + "properties": { + "event": { + "type": "string", + "x-go-name": "Event" + }, + "head_branch": { + "type": "string", + "x-go-name": "HeadBranch" + }, + "head_sha": { + "type": "string", + "x-go-name": "HeadSHA" + }, + "id": { + "type": "integer", + "format": "int64", + "x-go-name": "ID" + }, + "name": { + "type": "string", + "x-go-name": "Name" + }, + "run_number": { + "type": "integer", + "format": "int64", + "x-go-name": "RunNumber" + }, + "status": { + "type": "string", + "x-go-name": "Status" + }, + "triggering_actor": { + "$ref": "#/definitions/User" + }, + "url": { + "type": "string", + "x-go-name": "URL" + }, + "workflow_id": { + "type": "string", + "x-go-name": "WorkflowID" + } + }, + "x-go-package": "forgejo.org/modules/structs" + }, "ActionRunJob": { "description": "ActionRunJob represents a job of a run", "type": "object", @@ -25381,6 +25571,25 @@ }, "x-go-package": "forgejo.org/modules/structs" }, + "ListActionRunResponse": { + "description": "ListActionRunResponse return a list of ActionRun", + "type": "object", + "properties": { + "total_count": { + "type": "integer", + "format": "int64", + "x-go-name": "TotalCount" + }, + "workflow_runs": { + "type": "array", + "items": { + "$ref": "#/definitions/ActionRun" + }, + "x-go-name": "Entries" + } + }, + "x-go-package": "forgejo.org/modules/structs" + }, "MarkdownOption": { "description": "MarkdownOption markdown options", "type": "object", @@ -28585,6 +28794,18 @@ } } }, + "ActionRun": { + "description": "ActionRun", + "schema": { + "$ref": "#/definitions/ActionRun" + } + }, + "ActionRunList": { + "description": "ActionRunList", + "schema": { + "$ref": "#/definitions/ListActionRunResponse" + } + }, "ActionVariable": { "description": "ActionVariable", "schema": { diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/COMMIT_EDITMSG b/tests/gitea-repositories-meta/user2/test_action_run_search.git/COMMIT_EDITMSG new file mode 100644 index 0000000000..6be072ebc0 --- /dev/null +++ b/tests/gitea-repositories-meta/user2/test_action_run_search.git/COMMIT_EDITMSG @@ -0,0 +1 @@ +fix: bug diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/FETCH_HEAD b/tests/gitea-repositories-meta/user2/test_action_run_search.git/FETCH_HEAD new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/HEAD b/tests/gitea-repositories-meta/user2/test_action_run_search.git/HEAD new file mode 100644 index 0000000000..b870d82622 --- /dev/null +++ b/tests/gitea-repositories-meta/user2/test_action_run_search.git/HEAD @@ -0,0 +1 @@ +ref: refs/heads/main diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/config b/tests/gitea-repositories-meta/user2/test_action_run_search.git/config new file mode 100644 index 0000000000..515f483629 --- /dev/null +++ b/tests/gitea-repositories-meta/user2/test_action_run_search.git/config @@ -0,0 +1,5 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = false + logallrefupdates = true diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/description b/tests/gitea-repositories-meta/user2/test_action_run_search.git/description new file mode 100644 index 0000000000..498b267a8c --- /dev/null +++ b/tests/gitea-repositories-meta/user2/test_action_run_search.git/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/index b/tests/gitea-repositories-meta/user2/test_action_run_search.git/index new file mode 100644 index 0000000000000000000000000000000000000000..cca2ada8358508f82b1084240cd0c50d3fd59f49 GIT binary patch literal 420 zcmZ?q402{*U|<4b<_v*ZD=mLakA=|;41X9Io*8vAFf=YvV~G=0V-au!s+%rRq}!XZ zjO+WeB;og7q8DP{7^V?9(XLM)1wbEl^rPYOx&FVJ4XEN<%uF%tBkjG+vQE6UYW?nkX0-(7^ zExz)+h0##+eNfG3SlO?T*`BU0ls|D*X>QA@rSm7J^ZvnVesO7Xa%yog%>0lbS684n zVi=4S47uvV*6&!b^Zy>p7jLhce+qI`3b`l*b0Szl+3Ryjcb{H6!t?i5c+-Dzd8TC& zJK;eCRAIoiOy};CuAR|aCQUia^I^Koqd)(zLc^C2iiC 1745918688 +0200 commit (initial): initial +97f29ee599c373c729132a5c46a046978311e0ee 97f29ee599c373c729132a5c46a046978311e0ee User 2 1745921505 +0200 checkout: moving from master to main +97f29ee599c373c729132a5c46a046978311e0ee 97f29ee599c373c729132a5c46a046978311e0ee User 2 1745922102 +0200 checkout: moving from main to bugfix-1 +97f29ee599c373c729132a5c46a046978311e0ee 35c5cddfc19397501ec8f4f7bb808a7c8f04445f User 2 1745922142 +0200 commit: fix: bug +35c5cddfc19397501ec8f4f7bb808a7c8f04445f 97f29ee599c373c729132a5c46a046978311e0ee User 2 1745922150 +0200 checkout: moving from bugfix-1 to main +97f29ee599c373c729132a5c46a046978311e0ee 35c5cddfc19397501ec8f4f7bb808a7c8f04445f User 2 1745922198 +0200 checkout: moving from main to bugfix-1 +35c5cddfc19397501ec8f4f7bb808a7c8f04445f 97f29ee599c373c729132a5c46a046978311e0ee User 2 1745922272 +0200 checkout: moving from bugfix-1 to main diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/logs/refs/heads/bugfix-1 b/tests/gitea-repositories-meta/user2/test_action_run_search.git/logs/refs/heads/bugfix-1 new file mode 100644 index 0000000000..d8977c9d15 --- /dev/null +++ b/tests/gitea-repositories-meta/user2/test_action_run_search.git/logs/refs/heads/bugfix-1 @@ -0,0 +1,2 @@ +0000000000000000000000000000000000000000 97f29ee599c373c729132a5c46a046978311e0ee User 2 1745922102 +0200 branch: Created from HEAD +97f29ee599c373c729132a5c46a046978311e0ee 35c5cddfc19397501ec8f4f7bb808a7c8f04445f User 2 1745922142 +0200 commit: fix: bug diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/logs/refs/heads/main b/tests/gitea-repositories-meta/user2/test_action_run_search.git/logs/refs/heads/main new file mode 100644 index 0000000000..30a780778b --- /dev/null +++ b/tests/gitea-repositories-meta/user2/test_action_run_search.git/logs/refs/heads/main @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 97f29ee599c373c729132a5c46a046978311e0ee User 2 1745921505 +0200 branch: Created from HEAD diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/05/daa94891793ad0f183273cfb9902b903782e2a b/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/05/daa94891793ad0f183273cfb9902b903782e2a new file mode 100644 index 0000000000000000000000000000000000000000..741f3a665f4729c29d852b0c4314120a6ec64af8 GIT binary patch literal 248 zcmV8GH1w3%uTgY2+m1OEl>#0FUn5K$uH;P%FnanQcx&|^5au7 ziwhDky&DXD3Rr8y;53Wf$;Tv_=^ zV0$5UX5}YA98px7SFD?#XQhympPZdq1lC(zl3D;(3o>6<0oj{r`9%uF1*yrIX_=`h y3NV+0b%7KYmF8JN-KkPtt&pBsl2MwZms*yZSE2{;x*o)Sm`;V-S}p)A!D&&UwrEEH literal 0 HcmV?d00001 diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/18/722d8d68a60af7e66213ef4a15d05dae365f2b b/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/18/722d8d68a60af7e66213ef4a15d05dae365f2b new file mode 100644 index 0000000000000000000000000000000000000000..cececf663393589b47fc4dc7f7f787ac5b4737e0 GIT binary patch literal 139 zcmV;60CfL&0ZYosPf{>6HDt(3%uTgYa7)b0QOM6z2q?|TQ3y&cEKMyg;o{2Av*J=v zC@9Uzi7$ePS#fb?mGqOxgH3f;98z-9^SsEG}C7LFim?avRm|2=z z7#kX<8l=cHvh7Ud`9mlW$I=jYlf7@C`y zS{fM{niwf)8yFcFa6w#Hl1h%=^n&!_%yb1^5O8vJ_w-W;a1T%j_H_4i3<(W#1qthN zDR3!ddW5<$2RXsTxfD_gjI`7j)2i~4%`MByUA!VwE%eLWwH%d;LQNbkQ}T?`at!r@O4F)boXia^f{n|H+^Rwi mOUeUnxfE>8%?us6Aii<+bHU~puC&YwD}|)ebS?ntBZ+>)U!__A literal 0 HcmV?d00001 diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/76/ebce62dde5d6c40cfdda5782ff171f02a618b9 b/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/76/ebce62dde5d6c40cfdda5782ff171f02a618b9 new file mode 100644 index 0000000000000000000000000000000000000000..920604316e548e11a6265965318bb618adfde085 GIT binary patch literal 53 zcmV-50LuS(0V^p=O;s>9V=yr=FfdRk&o9bO%gHY}_k_es~z=q;0`9On5jUFK1s L0LM`PU<(n+6DAe9 literal 0 HcmV?d00001 diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/7f/56afb8a0b9ffbc39e8edd537f252412254d112 b/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/7f/56afb8a0b9ffbc39e8edd537f252412254d112 new file mode 100644 index 0000000000000000000000000000000000000000..4dad68d94413aea235c8c6d463940f402c9e0b29 GIT binary patch literal 52 zcmbFfdTiOUo}xPtD3_D0_V_>F(2OM|l3;3UB%^F3+?~ tVyB^jftiVkLQ-jZT4n{qQY*=OVaFpqTwLd9271iH%ClCMt literal 0 HcmV?d00001 diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/8f/2069876727126f91aa756d84caa59f93670dfc b/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/8f/2069876727126f91aa756d84caa59f93670dfc new file mode 100644 index 0000000000000000000000000000000000000000..92663ca7120b1a91eb830438f39f62ca30e2caeb GIT binary patch literal 149 zcmV;G0BZku0ZYosPf{>6vt-Ci%uTgY2rf-dPAx80$j?&ypn;!;p3C@s#g z;^NB6Pbvm+iy^Awv+|QbTm^-q(!65b{5&g#(xlS7l2YBA#FEtF5-tS=1%=|0)B><3 z5Jy)5)tcav)B><7kYrJ5o|Qssaz?&_Qh0t*c3Mt;xdPax)Rfc|kc&XBP*ef{5KlJx D-YiCN literal 0 HcmV?d00001 diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/97/f29ee599c373c729132a5c46a046978311e0ee b/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/97/f29ee599c373c729132a5c46a046978311e0ee new file mode 100644 index 0000000000000000000000000000000000000000..9a835a73c48b9b97a9dfc2982fc696329b140d76 GIT binary patch literal 345 zcmV-f0jB6)Qt-}6EG<@WtIWt#u*uFz%W}xgEYdB?Ps%ST)=SRM zwNo%OH!-y|v@o-vAb@DP($tx;i>VdOJE9gnI^t6k0f$W_gsQr-XQfWcvq~Bu51sM7Sk+l%%AG zIwv|gCz?1qdOHU>!Ns{0Qi_5@90N=W96cgT{6h=%{nGOD9J5lrQaqf3y*(=}!m=$2 zg9>sYGmR{=i%N6SUDI71^`na2D|~`O+>I+NxD?FFJ-o_tDl1*QbE6UsOpPlOjYGVO rlKqS;lEaGA(`~sFYy;dwe7PXLarJY-<`=Hayv&l!#2hXFl#G8~;$D=X literal 0 HcmV?d00001 diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/a5/3a19de56c7594844459c2951497c15dba8adee b/tests/gitea-repositories-meta/user2/test_action_run_search.git/objects/a5/3a19de56c7594844459c2951497c15dba8adee new file mode 100644 index 0000000000000000000000000000000000000000..44b7d5774da5410a1b2bbe107a77f766fb75d2ae GIT binary patch literal 23 fcmb7v}7R4~B^q5#_b>U;Py3OyIOgot?^t7Pric6D|Q;Uls>iQKj a+tbyB@+Yn;&22fgbpGUY-ah~!_Af76fj{v8 literal 0 HcmV?d00001 diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/refs/heads/bugfix-1 b/tests/gitea-repositories-meta/user2/test_action_run_search.git/refs/heads/bugfix-1 new file mode 100644 index 0000000000..ae12ad5d79 --- /dev/null +++ b/tests/gitea-repositories-meta/user2/test_action_run_search.git/refs/heads/bugfix-1 @@ -0,0 +1 @@ +35c5cddfc19397501ec8f4f7bb808a7c8f04445f diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/refs/heads/main b/tests/gitea-repositories-meta/user2/test_action_run_search.git/refs/heads/main new file mode 100644 index 0000000000..d46cce7027 --- /dev/null +++ b/tests/gitea-repositories-meta/user2/test_action_run_search.git/refs/heads/main @@ -0,0 +1 @@ +97f29ee599c373c729132a5c46a046978311e0ee diff --git a/tests/gitea-repositories-meta/user2/test_action_run_search.git/refs/tags/v1 b/tests/gitea-repositories-meta/user2/test_action_run_search.git/refs/tags/v1 new file mode 100644 index 0000000000..d46cce7027 --- /dev/null +++ b/tests/gitea-repositories-meta/user2/test_action_run_search.git/refs/tags/v1 @@ -0,0 +1 @@ +97f29ee599c373c729132a5c46a046978311e0ee diff --git a/tests/integration/api_repo_actions_test.go b/tests/integration/api_repo_actions_test.go index 43de376421..a7b66d6d62 100644 --- a/tests/integration/api_repo_actions_test.go +++ b/tests/integration/api_repo_actions_test.go @@ -101,3 +101,143 @@ jobs: assert.Len(t, run.Jobs, 2) }) } + +func TestAPIGetListActionRun(t *testing.T) { + defer tests.PrepareTestEnv(t)() + var ( + runIDs = []int64{892, 893, 894} + dbRuns = make(map[int64]*actions_model.ActionRun, 3) + ) + + for _, id := range runIDs { + dbRuns[id] = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: id}) + } + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: dbRuns[runIDs[0]].RepoID}) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + token := getUserToken(t, user.LowerName, auth_model.AccessTokenScopeWriteRepository) + + testqueries := []struct { + name string + query string + expectedIDs []int64 + }{ + { + name: "No query parameters", + query: "", + expectedIDs: runIDs, + }, + { + name: "Search for workflow_dispatch events", + query: "?event=workflow_dispatch", + expectedIDs: []int64{894}, + }, + { + name: "Search for multiple events", + query: "?event=workflow_dispatch&event=push", + expectedIDs: []int64{892, 894}, + }, + { + name: "Search for failed status", + query: "?status=failure", + expectedIDs: []int64{893}, + }, + { + name: "Search for multiple statuses", + query: "?status=failure&status=running", + expectedIDs: []int64{893, 894}, + }, + { + name: "Search for num_nr", + query: "?run_number=1", + expectedIDs: []int64{892}, + }, + { + name: "Search for sha", + query: "?head_sha=97f29ee599c373c729132a5c46a046978311e0ee", + expectedIDs: []int64{892, 894}, + }, + } + + for _, tt := range testqueries { + t.Run(tt.name, func(t *testing.T) { + req := NewRequest(t, http.MethodGet, + fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs%s", + repo.OwnerName, repo.Name, tt.query, + ), + ) + req.AddTokenAuth(token) + + res := MakeRequest(t, req, http.StatusOK) + apiRuns := new(api.ListActionRunResponse) + DecodeJSON(t, res, apiRuns) + + assert.Equal(t, int64(len(tt.expectedIDs)), apiRuns.TotalCount) + assert.Len(t, apiRuns.Entries, len(tt.expectedIDs)) + + resultIDs := make([]int64, apiRuns.TotalCount) + for i, run := range apiRuns.Entries { + resultIDs[i] = run.ID + } + + assert.ElementsMatch(t, tt.expectedIDs, resultIDs) + }) + } +} + +func TestAPIGetActionRun(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 63}) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + token := getUserToken(t, user.LowerName, auth_model.AccessTokenScopeWriteRepository) + + testqueries := []struct { + name string + runID int64 + expectedStatus int + }{ + { + name: "existing return ok", + runID: 892, + expectedStatus: http.StatusOK, + }, + { + name: "non existing run", + runID: 9876543210, // I hope this run will not exists, else just change it to another. + expectedStatus: http.StatusNotFound, + }, + { + name: "existing run but wrong repo should not be found", + runID: 891, + expectedStatus: http.StatusNotFound, + }, + } + + for _, tt := range testqueries { + t.Run(tt.name, func(t *testing.T) { + req := NewRequest(t, http.MethodGet, + fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs/%d", + repo.OwnerName, repo.Name, tt.runID, + ), + ) + req.AddTokenAuth(token) + + res := MakeRequest(t, req, tt.expectedStatus) + + // Only interested in the data if 200 OK + if tt.expectedStatus != http.StatusOK { + return + } + + dbRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: tt.runID}) + apiRun := new(api.ActionRun) + DecodeJSON(t, res, apiRun) + + assert.Equal(t, dbRun.Index, apiRun.RunNumber) + assert.Equal(t, dbRun.Status.String(), apiRun.Status) + assert.Equal(t, dbRun.CommitSHA, apiRun.HeadSHA) + assert.Equal(t, dbRun.TriggerUserID, apiRun.TriggeringActor.ID) + }) + } +}