From 318d171b5f664eda10ab80c103e8e5b8c097a274 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Tue, 11 Mar 2025 16:23:21 -0700 Subject: [PATCH 01/12] Download actions logs from API --- routers/api/v1/api.go | 4 + routers/api/v1/repo/actions_run.go | 143 +++++++++++++++++++++++++++++ templates/swagger/v1_json.tmpl | 53 +++++++++++ 3 files changed, 200 insertions(+) create mode 100644 routers/api/v1/repo/actions_run.go diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index bc76b5285e5a2..5a106dbeca183 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1168,6 +1168,10 @@ func Routes() *web.Router { m.Post("/{workflow_id}/dispatches", reqRepoWriter(unit.TypeActions), bind(api.CreateActionWorkflowDispatch{}), repo.ActionsDispatchWorkflow) }, context.ReferencesGitRepo(), reqToken(), reqRepoReader(unit.TypeActions)) + m.Group("/actions/runs", func() { + m.Get("/{run_id}/jobs/{job}/logs", repo.DownloadActionsRunLogs) + }, context.ReferencesGitRepo(), reqToken(), reqRepoReader(unit.TypeActions)) + m.Group("/hooks/git", func() { m.Combo("").Get(repo.ListGitHooks) m.Group("/{id}", func() { diff --git a/routers/api/v1/repo/actions_run.go b/routers/api/v1/repo/actions_run.go new file mode 100644 index 0000000000000..ec4ef164ec288 --- /dev/null +++ b/routers/api/v1/repo/actions_run.go @@ -0,0 +1,143 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "errors" + "fmt" + "net/http" + "strings" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/modules/actions" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/context" +) + +func getRunIndex(ctx *context.APIContext) int64 { + // if run param is "latest", get the latest run index + if ctx.PathParam("run_id") == "latest" { + if run, _ := actions_model.GetLatestRun(ctx, ctx.Repo.Repository.ID); run != nil { + return run.Index + } + } + return ctx.PathParamInt64("run_id") +} + +// getRunJobs gets the jobs of runIndex, and returns jobs[jobIndex], jobs. +// Any error will be written to the ctx. +// It never returns a nil job of an empty jobs, if the jobIndex is out of range, it will be treated as 0. +func getRunJobs(ctx *context.APIContext, runIndex, jobIndex int64) (*actions_model.ActionRunJob, []*actions_model.ActionRunJob) { + run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.HTTPError(http.StatusNotFound, err.Error()) + return nil, nil + } + ctx.HTTPError(http.StatusInternalServerError, err.Error()) + return nil, nil + } + run.Repo = ctx.Repo.Repository + jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID) + if err != nil { + ctx.HTTPError(http.StatusInternalServerError, err.Error()) + return nil, nil + } + if len(jobs) == 0 { + ctx.HTTPError(http.StatusNotFound) + return nil, nil + } + + for _, v := range jobs { + v.Run = run + } + + if jobIndex >= 0 && jobIndex < int64(len(jobs)) { + return jobs[jobIndex], jobs + } + return jobs[0], jobs +} + +func DownloadActionsRunLogs(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run_id}/logs repository downloadActionsRunLogs + // --- + // summary: Downloads the logs for a workflow run redirects to blob url + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: name of the owner + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repository + // type: string + // required: true + // - name: run_id + // in: path + // description: id of the run, this could be latest + // type: integer + // required: true + // - name: job + // in: path + // description: id of the job + // type: integer + // required: true + // responses: + // "302": + // description: redirect to the blob download + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + runIndex := getRunIndex(ctx) + jobIndex := ctx.PathParamInt64("job") + + job, _ := getRunJobs(ctx, runIndex, jobIndex) + if ctx.Written() { + return + } + if job.TaskID == 0 { + ctx.HTTPError(http.StatusNotFound, "job is not started") + return + } + + err := job.LoadRun(ctx) + if err != nil { + ctx.HTTPError(http.StatusInternalServerError, err.Error()) + return + } + + task, err := actions_model.GetTaskByID(ctx, job.TaskID) + if err != nil { + ctx.HTTPError(http.StatusInternalServerError, err.Error()) + return + } + if task.LogExpired { + ctx.HTTPError(http.StatusNotFound, "logs have been cleaned up") + return + } + + reader, err := actions.OpenLogs(ctx, task.LogInStorage, task.LogFilename) + if err != nil { + ctx.HTTPError(http.StatusInternalServerError, err.Error()) + return + } + defer reader.Close() + + workflowName := job.Run.WorkflowID + if p := strings.Index(workflowName, "."); p > 0 { + workflowName = workflowName[0:p] + } + ctx.ServeContent(reader, &context.ServeHeaderOptions{ + Filename: fmt.Sprintf("%v-%v-%v.log", workflowName, job.Name, task.ID), + ContentLength: &task.LogSize, + ContentType: "text/plain", + ContentTypeCharset: "utf-8", + Disposition: "attachment", + }) +} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 48ed958ca2fa7..e905a2b479185 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -4220,6 +4220,59 @@ } } }, + "/repos/{owner}/{repo}/actions/runs/{run_id}/logs": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Downloads the logs for a workflow run redirects to blob url", + "operationId": "downloadActionsRunLogs", + "parameters": [ + { + "type": "string", + "description": "name of the owner", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repository", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "id of the run, this could be latest", + "name": "run_id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "id of the job", + "name": "job", + "in": "path", + "required": true + } + ], + "responses": { + "302": { + "description": "redirect to the blob download" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/repos/{owner}/{repo}/actions/runs/{run}/artifacts": { "get": { "produces": [ From 14b6ce7560ee4de6a06feab10a5acd12dccf7545 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Tue, 11 Mar 2025 16:28:32 -0700 Subject: [PATCH 02/12] Fix swagger --- routers/api/v1/repo/actions_run.go | 2 +- templates/swagger/v1_json.tmpl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/routers/api/v1/repo/actions_run.go b/routers/api/v1/repo/actions_run.go index ec4ef164ec288..7bd39bc2f9a4c 100644 --- a/routers/api/v1/repo/actions_run.go +++ b/routers/api/v1/repo/actions_run.go @@ -60,7 +60,7 @@ func getRunJobs(ctx *context.APIContext, runIndex, jobIndex int64) (*actions_mod } func DownloadActionsRunLogs(ctx *context.APIContext) { - // swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run_id}/logs repository downloadActionsRunLogs + // swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run_id}/jobs/{job}/logs repository downloadActionsRunLogs // --- // summary: Downloads the logs for a workflow run redirects to blob url // produces: diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index e905a2b479185..8602c32c8afeb 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -4220,7 +4220,7 @@ } } }, - "/repos/{owner}/{repo}/actions/runs/{run_id}/logs": { + "/repos/{owner}/{repo}/actions/runs/{run_id}/jobs/{job}/logs": { "get": { "produces": [ "application/json" From 9ec87c77ef6de1e28994bae27e5be57d77801297 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 12 Mar 2025 20:56:08 -0700 Subject: [PATCH 03/12] Add test for API actions log download --- tests/integration/actions_log_test.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/integration/actions_log_test.go b/tests/integration/actions_log_test.go index fd055fc4c4f9e..e767ad7c7c5bc 100644 --- a/tests/integration/actions_log_test.go +++ b/tests/integration/actions_log_test.go @@ -149,6 +149,20 @@ jobs: ) } + // download task logs from API and check content + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs/%s/jobs/0/logs", user2.Name, repo.Name, runIndex)). + AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + logTextLines = strings.Split(strings.TrimSpace(resp.Body.String()), "\n") + assert.Len(t, logTextLines, len(tc.outcome.logRows)) + for idx, lr := range tc.outcome.logRows { + assert.Equal( + t, + fmt.Sprintf("%s %s", lr.Time.AsTime().Format("2006-01-02T15:04:05.0000000Z07:00"), lr.Content), + logTextLines[idx], + ) + } + resetFunc() }) } From 56282336710c14d55c749735044f71e8ac31a6e8 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Thu, 13 Mar 2025 15:49:24 -0700 Subject: [PATCH 04/12] Extract actions log download function --- models/actions/run_job.go | 14 ++-- models/actions/run_job_list.go | 35 ++++++++-- routers/api/v1/api.go | 4 +- routers/api/v1/repo/actions_run.go | 107 ++++------------------------- routers/common/actions.go | 91 ++++++++++++++++++++++++ routers/web/repo/actions/view.go | 46 ++----------- 6 files changed, 147 insertions(+), 150 deletions(-) create mode 100644 routers/common/actions.go diff --git a/models/actions/run_job.go b/models/actions/run_job.go index de4b6aab66701..14855d5546599 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" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -19,11 +20,12 @@ import ( // ActionRunJob represents a job of a run type ActionRunJob struct { ID int64 - RunID int64 `xorm:"index"` - Run *ActionRun `xorm:"-"` - RepoID int64 `xorm:"index"` - OwnerID int64 `xorm:"index"` - CommitSHA string `xorm:"index"` + RunID int64 `xorm:"index"` + Run *ActionRun `xorm:"-"` + RepoID int64 `xorm:"index"` + Repo *repo_model.Repository `xorm:"-"` + OwnerID int64 `xorm:"index"` + CommitSHA string `xorm:"index"` IsForkPullRequest bool Name string `xorm:"VARCHAR(255)"` Attempt int64 @@ -83,7 +85,7 @@ func GetRunJobByID(ctx context.Context, id int64) (*ActionRunJob, error) { return &job, nil } -func GetRunJobsByRunID(ctx context.Context, runID int64) ([]*ActionRunJob, error) { +func GetRunJobsByRunID(ctx context.Context, runID int64) (ActionJobList, error) { var jobs []*ActionRunJob if err := db.GetEngine(ctx).Where("run_id=?", runID).OrderBy("id").Find(&jobs); err != nil { return nil, err diff --git a/models/actions/run_job_list.go b/models/actions/run_job_list.go index 6c5d3b3252ebf..8d8e3f726a081 100644 --- a/models/actions/run_job_list.go +++ b/models/actions/run_job_list.go @@ -7,6 +7,7 @@ import ( "context" "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/timeutil" @@ -21,7 +22,33 @@ func (jobs ActionJobList) GetRunIDs() []int64 { }) } +func (jobs ActionJobList) LoadRepos(ctx context.Context) error { + repoIDs := container.FilterSlice(jobs, func(j *ActionRunJob) (int64, bool) { + return j.RepoID, j.RepoID != 0 + }) + if len(repoIDs) == 0 { + return nil + } + + repos := make(map[int64]*repo_model.Repository, len(repoIDs)) + if err := db.GetEngine(ctx).In("id", repoIDs).Find(&repos); err != nil { + return err + } + for _, j := range jobs { + if j.RepoID > 0 && j.Repo == nil { + j.Repo = repos[j.RepoID] + } + } + return nil +} + func (jobs ActionJobList) LoadRuns(ctx context.Context, withRepo bool) error { + if withRepo { + if err := jobs.LoadRepos(ctx); err != nil { + return err + } + } + runIDs := jobs.GetRunIDs() runs := make(map[int64]*ActionRun, len(runIDs)) if err := db.GetEngine(ctx).In("id", runIDs).Find(&runs); err != nil { @@ -30,15 +57,9 @@ func (jobs ActionJobList) LoadRuns(ctx context.Context, withRepo bool) error { for _, j := range jobs { if j.RunID > 0 && j.Run == nil { j.Run = runs[j.RunID] + j.Run.Repo = j.Repo } } - if withRepo { - var runsList RunList = make([]*ActionRun, 0, len(runs)) - for _, r := range runs { - runsList = append(runsList, r) - } - return runsList.LoadRepos(ctx) - } return nil } diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 5a106dbeca183..8c8a58fb0140d 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1169,8 +1169,8 @@ func Routes() *web.Router { }, context.ReferencesGitRepo(), reqToken(), reqRepoReader(unit.TypeActions)) m.Group("/actions/runs", func() { - m.Get("/{run_id}/jobs/{job}/logs", repo.DownloadActionsRunLogs) - }, context.ReferencesGitRepo(), reqToken(), reqRepoReader(unit.TypeActions)) + m.Get("/{run}/jobs/{job}/logs", repo.DownloadActionsRunJobLogs) + }, reqToken(), reqRepoReader(unit.TypeActions)) m.Group("/hooks/git", func() { m.Combo("").Get(repo.ListGitHooks) diff --git a/routers/api/v1/repo/actions_run.go b/routers/api/v1/repo/actions_run.go index 7bd39bc2f9a4c..fc4a6d08fe5cd 100644 --- a/routers/api/v1/repo/actions_run.go +++ b/routers/api/v1/repo/actions_run.go @@ -4,63 +4,23 @@ package repo import ( - "errors" - "fmt" - "net/http" - "strings" - actions_model "code.gitea.io/gitea/models/actions" - "code.gitea.io/gitea/modules/actions" - "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/services/context" ) -func getRunIndex(ctx *context.APIContext) int64 { +func getRunID(ctx *context.APIContext) int64 { // if run param is "latest", get the latest run index - if ctx.PathParam("run_id") == "latest" { + if ctx.PathParam("run") == "latest" { if run, _ := actions_model.GetLatestRun(ctx, ctx.Repo.Repository.ID); run != nil { - return run.Index - } - } - return ctx.PathParamInt64("run_id") -} - -// getRunJobs gets the jobs of runIndex, and returns jobs[jobIndex], jobs. -// Any error will be written to the ctx. -// It never returns a nil job of an empty jobs, if the jobIndex is out of range, it will be treated as 0. -func getRunJobs(ctx *context.APIContext, runIndex, jobIndex int64) (*actions_model.ActionRunJob, []*actions_model.ActionRunJob) { - run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex) - if err != nil { - if errors.Is(err, util.ErrNotExist) { - ctx.HTTPError(http.StatusNotFound, err.Error()) - return nil, nil + return run.ID } - ctx.HTTPError(http.StatusInternalServerError, err.Error()) - return nil, nil - } - run.Repo = ctx.Repo.Repository - jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID) - if err != nil { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) - return nil, nil } - if len(jobs) == 0 { - ctx.HTTPError(http.StatusNotFound) - return nil, nil - } - - for _, v := range jobs { - v.Run = run - } - - if jobIndex >= 0 && jobIndex < int64(len(jobs)) { - return jobs[jobIndex], jobs - } - return jobs[0], jobs + return ctx.PathParamInt64("run") } -func DownloadActionsRunLogs(ctx *context.APIContext) { - // swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run_id}/jobs/{job}/logs repository downloadActionsRunLogs +func DownloadActionsRunJobLogs(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run}/jobs/{job}/logs repository downloadActionsRunJobLogs // --- // summary: Downloads the logs for a workflow run redirects to blob url // produces: @@ -76,7 +36,7 @@ func DownloadActionsRunLogs(ctx *context.APIContext) { // description: name of the repository // type: string // required: true - // - name: run_id + // - name: run // in: path // description: id of the run, this could be latest // type: integer @@ -87,57 +47,14 @@ func DownloadActionsRunLogs(ctx *context.APIContext) { // type: integer // required: true // responses: - // "302": - // description: redirect to the blob download + // "200": + // description: output blob content // "400": // "$ref": "#/responses/error" // "404": // "$ref": "#/responses/notFound" - runIndex := getRunIndex(ctx) + runID := getRunID(ctx) jobIndex := ctx.PathParamInt64("job") - - job, _ := getRunJobs(ctx, runIndex, jobIndex) - if ctx.Written() { - return - } - if job.TaskID == 0 { - ctx.HTTPError(http.StatusNotFound, "job is not started") - return - } - - err := job.LoadRun(ctx) - if err != nil { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) - return - } - - task, err := actions_model.GetTaskByID(ctx, job.TaskID) - if err != nil { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) - return - } - if task.LogExpired { - ctx.HTTPError(http.StatusNotFound, "logs have been cleaned up") - return - } - - reader, err := actions.OpenLogs(ctx, task.LogInStorage, task.LogFilename) - if err != nil { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) - return - } - defer reader.Close() - - workflowName := job.Run.WorkflowID - if p := strings.Index(workflowName, "."); p > 0 { - workflowName = workflowName[0:p] - } - ctx.ServeContent(reader, &context.ServeHeaderOptions{ - Filename: fmt.Sprintf("%v-%v-%v.log", workflowName, job.Name, task.ID), - ContentLength: &task.LogSize, - ContentType: "text/plain", - ContentTypeCharset: "utf-8", - Disposition: "attachment", - }) + common.DownloadActionsRunJobLogs(ctx.Base, ctx.Repo.Repository, runID, jobIndex) } diff --git a/routers/common/actions.go b/routers/common/actions.go new file mode 100644 index 0000000000000..63534edf5cdfa --- /dev/null +++ b/routers/common/actions.go @@ -0,0 +1,91 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package common + +import ( + "fmt" + "net/http" + "strings" + + actions_model "code.gitea.io/gitea/models/actions" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/actions" + "code.gitea.io/gitea/services/context" +) + +func DownloadActionsRunJobLogs(ctx *context.Base, ctxRepo *repo_model.Repository, runID, jobIndex int64) { + if runID == 0 { + ctx.HTTPError(http.StatusBadRequest, "invalid run id") + return + } + + runJobs, err := actions_model.GetRunJobsByRunID(ctx, runID) + if err != nil { + ctx.HTTPError(http.StatusInternalServerError, err.Error()) + return + } + if len(runJobs) == 0 { + ctx.HTTPError(http.StatusNotFound) + return + } + if err := runJobs.LoadRepos(ctx); err != nil { + ctx.HTTPError(http.StatusInternalServerError, err.Error()) + return + } + if runJobs[0].Repo.ID != ctxRepo.ID { + ctx.HTTPError(http.StatusNotFound) + return + } + + var curJob *actions_model.ActionRunJob + for _, job := range runJobs { + if job.ID == jobIndex { + curJob = job + break + } + } + if curJob == nil { + ctx.HTTPError(http.StatusNotFound) + return + } + + if curJob.TaskID == 0 { + ctx.HTTPError(http.StatusNotFound, "job is not started") + return + } + + if err := curJob.LoadRun(ctx); err != nil { + ctx.HTTPError(http.StatusInternalServerError, err.Error()) + return + } + + task, err := actions_model.GetTaskByID(ctx, curJob.TaskID) + if err != nil { + ctx.HTTPError(http.StatusInternalServerError, err.Error()) + return + } + if task.LogExpired { + ctx.HTTPError(http.StatusNotFound, "logs have been cleaned up") + return + } + + reader, err := actions.OpenLogs(ctx, task.LogInStorage, task.LogFilename) + if err != nil { + ctx.HTTPError(http.StatusInternalServerError, err.Error()) + return + } + defer reader.Close() + + workflowName := curJob.Run.WorkflowID + if p := strings.Index(workflowName, "."); p > 0 { + workflowName = workflowName[0:p] + } + ctx.ServeContent(reader, &context.ServeHeaderOptions{ + Filename: fmt.Sprintf("%v-%v-%v.log", workflowName, curJob.Name, task.ID), + ContentLength: &task.LogSize, + ContentType: "text/plain", + ContentTypeCharset: "utf-8", + Disposition: "attachment", + }) +} diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 41f0d2d0ec249..2d4d5c231dade 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -14,7 +14,6 @@ import ( "net/http" "net/url" "strconv" - "strings" "time" actions_model "code.gitea.io/gitea/models/actions" @@ -31,6 +30,7 @@ import ( "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/common" actions_service "code.gitea.io/gitea/services/actions" context_module "code.gitea.io/gitea/services/context" notify_service "code.gitea.io/gitea/services/notify" @@ -469,49 +469,15 @@ func Logs(ctx *context_module.Context) { runIndex := getRunIndex(ctx) jobIndex := ctx.PathParamInt64("job") - job, _ := getRunJobs(ctx, runIndex, jobIndex) - if ctx.Written() { - return - } - if job.TaskID == 0 { - ctx.HTTPError(http.StatusNotFound, "job is not started") - return - } - - err := job.LoadRun(ctx) - if err != nil { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) - return - } - - task, err := actions_model.GetTaskByID(ctx, job.TaskID) - if err != nil { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) - return - } - if task.LogExpired { - ctx.HTTPError(http.StatusNotFound, "logs have been cleaned up") - return - } - - reader, err := actions.OpenLogs(ctx, task.LogInStorage, task.LogFilename) + run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex) if err != nil { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) + ctx.NotFoundOrServerError("GetRunByIndex", func(err error) bool { + return errors.Is(err, util.ErrNotExist) + }, err) return } - defer reader.Close() - workflowName := job.Run.WorkflowID - if p := strings.Index(workflowName, "."); p > 0 { - workflowName = workflowName[0:p] - } - ctx.ServeContent(reader, &context_module.ServeHeaderOptions{ - Filename: fmt.Sprintf("%v-%v-%v.log", workflowName, job.Name, task.ID), - ContentLength: &task.LogSize, - ContentType: "text/plain", - ContentTypeCharset: "utf-8", - Disposition: "attachment", - }) + common.DownloadActionsRunJobLogs(ctx.Base, ctx.Repo.Repository, run.ID, jobIndex) } func Cancel(ctx *context_module.Context) { From bf5ccf83912cfabc220c15b3de3a158072ed156e Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Thu, 13 Mar 2025 15:51:10 -0700 Subject: [PATCH 05/12] Update swagger documentation --- templates/swagger/v1_json.tmpl | 42 +++++++++++++++++----------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 8602c32c8afeb..5a5b027d6ac4d 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -4220,7 +4220,7 @@ } } }, - "/repos/{owner}/{repo}/actions/runs/{run_id}/jobs/{job}/logs": { + "/repos/{owner}/{repo}/actions/runs/{run}/artifacts": { "get": { "produces": [ "application/json" @@ -4228,8 +4228,8 @@ "tags": [ "repository" ], - "summary": "Downloads the logs for a workflow run redirects to blob url", - "operationId": "downloadActionsRunLogs", + "summary": "Lists all artifacts for a repository run", + "operationId": "getArtifactsOfRun", "parameters": [ { "type": "string", @@ -4247,22 +4247,21 @@ }, { "type": "integer", - "description": "id of the run, this could be latest", - "name": "run_id", + "description": "runid of the workflow run", + "name": "run", "in": "path", "required": true }, { - "type": "integer", - "description": "id of the job", - "name": "job", - "in": "path", - "required": true + "type": "string", + "description": "name of the artifact", + "name": "name", + "in": "query" } ], "responses": { - "302": { - "description": "redirect to the blob download" + "200": { + "$ref": "#/responses/ArtifactsList" }, "400": { "$ref": "#/responses/error" @@ -4273,7 +4272,7 @@ } } }, - "/repos/{owner}/{repo}/actions/runs/{run}/artifacts": { + "/repos/{owner}/{repo}/actions/runs/{run}/jobs/{job}/logs": { "get": { "produces": [ "application/json" @@ -4281,8 +4280,8 @@ "tags": [ "repository" ], - "summary": "Lists all artifacts for a repository run", - "operationId": "getArtifactsOfRun", + "summary": "Downloads the logs for a workflow run redirects to blob url", + "operationId": "downloadActionsRunJobLogs", "parameters": [ { "type": "string", @@ -4300,21 +4299,22 @@ }, { "type": "integer", - "description": "runid of the workflow run", + "description": "id of the run, this could be latest", "name": "run", "in": "path", "required": true }, { - "type": "string", - "description": "name of the artifact", - "name": "name", - "in": "query" + "type": "integer", + "description": "id of the job", + "name": "job", + "in": "path", + "required": true } ], "responses": { "200": { - "$ref": "#/responses/ArtifactsList" + "description": "output blob content" }, "400": { "$ref": "#/responses/error" From 75cab36aebebce026fc53f4a2151e338d6d16c49 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Thu, 13 Mar 2025 17:33:29 -0700 Subject: [PATCH 06/12] Fix bug and test --- routers/common/actions.go | 7 ++----- tests/integration/actions_log_test.go | 4 +++- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/routers/common/actions.go b/routers/common/actions.go index 63534edf5cdfa..c2a0b251e6310 100644 --- a/routers/common/actions.go +++ b/routers/common/actions.go @@ -39,11 +39,8 @@ func DownloadActionsRunJobLogs(ctx *context.Base, ctxRepo *repo_model.Repository } var curJob *actions_model.ActionRunJob - for _, job := range runJobs { - if job.ID == jobIndex { - curJob = job - break - } + if jobIndex >= 0 && jobIndex < int64(len(runJobs)) { + curJob = runJobs[jobIndex] } if curJob == nil { ctx.HTTPError(http.StatusNotFound) diff --git a/tests/integration/actions_log_test.go b/tests/integration/actions_log_test.go index e767ad7c7c5bc..32364f738dee9 100644 --- a/tests/integration/actions_log_test.go +++ b/tests/integration/actions_log_test.go @@ -7,6 +7,7 @@ import ( "fmt" "net/http" "net/url" + "strconv" "strings" "testing" "time" @@ -149,8 +150,9 @@ jobs: ) } + runID, _ := strconv.ParseInt(task.Context.GetFields()["run_id"].GetStringValue(), 10, 64) // download task logs from API and check content - req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs/%s/jobs/0/logs", user2.Name, repo.Name, runIndex)). + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs/%d/jobs/0/logs", user2.Name, repo.Name, runID)). AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) logTextLines = strings.Split(strings.TrimSpace(resp.Body.String()), "\n") From 940bfe92209cbba0a1fbfaa06d480845aa5625a1 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 17 Mar 2025 12:15:34 -0700 Subject: [PATCH 07/12] Use job_id for the api endpoint --- models/actions/run_job.go | 11 ++++ routers/api/v1/api.go | 4 +- routers/api/v1/repo/actions_run.go | 28 ++++++---- routers/common/actions.go | 15 ++++-- routers/web/repo/actions/view.go | 2 +- templates/swagger/v1_json.tmpl | 75 ++++++++++++--------------- tests/integration/actions_log_test.go | 13 +++-- 7 files changed, 86 insertions(+), 62 deletions(-) diff --git a/models/actions/run_job.go b/models/actions/run_job.go index 14855d5546599..d0dfd10db6b61 100644 --- a/models/actions/run_job.go +++ b/models/actions/run_job.go @@ -60,6 +60,17 @@ func (job *ActionRunJob) LoadRun(ctx context.Context) error { return nil } +func (job *ActionRunJob) LoadRepo(ctx context.Context) error { + if job.Repo == nil { + repo, err := repo_model.GetRepositoryByID(ctx, job.RepoID) + if err != nil { + return err + } + job.Repo = repo + } + return nil +} + // LoadAttributes load Run if not loaded func (job *ActionRunJob) LoadAttributes(ctx context.Context) error { if job == nil { diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 8c8a58fb0140d..c49152a64dc38 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1168,8 +1168,8 @@ func Routes() *web.Router { m.Post("/{workflow_id}/dispatches", reqRepoWriter(unit.TypeActions), bind(api.CreateActionWorkflowDispatch{}), repo.ActionsDispatchWorkflow) }, context.ReferencesGitRepo(), reqToken(), reqRepoReader(unit.TypeActions)) - m.Group("/actions/runs", func() { - m.Get("/{run}/jobs/{job}/logs", repo.DownloadActionsRunJobLogs) + m.Group("/actions/jobs", func() { + m.Get("/{job_id}/logs", repo.DownloadActionsRunJobLogs) }, reqToken(), reqRepoReader(unit.TypeActions)) m.Group("/hooks/git", func() { diff --git a/routers/api/v1/repo/actions_run.go b/routers/api/v1/repo/actions_run.go index fc4a6d08fe5cd..d547720b5f180 100644 --- a/routers/api/v1/repo/actions_run.go +++ b/routers/api/v1/repo/actions_run.go @@ -20,7 +20,7 @@ func getRunID(ctx *context.APIContext) int64 { } func DownloadActionsRunJobLogs(ctx *context.APIContext) { - // swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run}/jobs/{job}/logs repository downloadActionsRunJobLogs + // swagger:operation GET /repos/{owner}/{repo}/actions/jobs/{job_id}/logs repository downloadActionsRunJobLogs // --- // summary: Downloads the logs for a workflow run redirects to blob url // produces: @@ -36,12 +36,7 @@ func DownloadActionsRunJobLogs(ctx *context.APIContext) { // description: name of the repository // type: string // required: true - // - name: run - // in: path - // description: id of the run, this could be latest - // type: integer - // required: true - // - name: job + // - name: job_id // in: path // description: id of the job // type: integer @@ -54,7 +49,20 @@ func DownloadActionsRunJobLogs(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - runID := getRunID(ctx) - jobIndex := ctx.PathParamInt64("job") - common.DownloadActionsRunJobLogs(ctx.Base, ctx.Repo.Repository, runID, jobIndex) + jobID := ctx.PathParamInt64("job_id") + if jobID == 0 { + ctx.APIError(400, "invalid job id") + return + } + curJob, err := actions_model.GetRunJobByID(ctx, jobID) + if err != nil { + ctx.APIErrorInternal(err) + return + } + if err := curJob.LoadRepo(ctx); err != nil { + ctx.APIErrorInternal(err) + return + } + + common.DownloadActionsRunJobLogs(ctx.Base, ctx.Repo.Repository, curJob) } diff --git a/routers/common/actions.go b/routers/common/actions.go index c2a0b251e6310..ed72057f0cf57 100644 --- a/routers/common/actions.go +++ b/routers/common/actions.go @@ -14,7 +14,7 @@ import ( "code.gitea.io/gitea/services/context" ) -func DownloadActionsRunJobLogs(ctx *context.Base, ctxRepo *repo_model.Repository, runID, jobIndex int64) { +func DownloadActionsRunJobLogsWithIndex(ctx *context.Base, ctxRepo *repo_model.Repository, runID, jobIndex int64) { if runID == 0 { ctx.HTTPError(http.StatusBadRequest, "invalid run id") return @@ -33,10 +33,6 @@ func DownloadActionsRunJobLogs(ctx *context.Base, ctxRepo *repo_model.Repository ctx.HTTPError(http.StatusInternalServerError, err.Error()) return } - if runJobs[0].Repo.ID != ctxRepo.ID { - ctx.HTTPError(http.StatusNotFound) - return - } var curJob *actions_model.ActionRunJob if jobIndex >= 0 && jobIndex < int64(len(runJobs)) { @@ -47,6 +43,15 @@ func DownloadActionsRunJobLogs(ctx *context.Base, ctxRepo *repo_model.Repository return } + DownloadActionsRunJobLogs(ctx, ctxRepo, curJob) +} + +func DownloadActionsRunJobLogs(ctx *context.Base, ctxRepo *repo_model.Repository, curJob *actions_model.ActionRunJob) { + if curJob.Repo.ID != ctxRepo.ID { + ctx.HTTPError(http.StatusNotFound) + return + } + if curJob.TaskID == 0 { ctx.HTTPError(http.StatusNotFound, "job is not started") return diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 2d4d5c231dade..6ac6fd4c6398b 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -477,7 +477,7 @@ func Logs(ctx *context_module.Context) { return } - common.DownloadActionsRunJobLogs(ctx.Base, ctx.Repo.Repository, run.ID, jobIndex) + common.DownloadActionsRunJobLogsWithIndex(ctx.Base, ctx.Repo.Repository, run.ID, jobIndex) } func Cancel(ctx *context_module.Context) { diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 5a5b027d6ac4d..d018d35d199c4 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -4187,7 +4187,7 @@ } } }, - "/repos/{owner}/{repo}/actions/runners/registration-token": { + "/repos/{owner}/{repo}/actions/jobs/{job_id}/logs": { "get": { "produces": [ "application/json" @@ -4195,32 +4195,45 @@ "tags": [ "repository" ], - "summary": "Get a repository's actions runner registration token", - "operationId": "repoGetRunnerRegistrationToken", + "summary": "Downloads the logs for a workflow run redirects to blob url", + "operationId": "downloadActionsRunJobLogs", "parameters": [ { "type": "string", - "description": "owner of the repo", + "description": "name of the owner", "name": "owner", "in": "path", "required": true }, { "type": "string", - "description": "name of the repo", + "description": "name of the repository", "name": "repo", "in": "path", "required": true + }, + { + "type": "integer", + "description": "id of the job", + "name": "job_id", + "in": "path", + "required": true } ], "responses": { "200": { - "$ref": "#/responses/RegistrationToken" + "description": "output blob content" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" } } } }, - "/repos/{owner}/{repo}/actions/runs/{run}/artifacts": { + "/repos/{owner}/{repo}/actions/runners/registration-token": { "get": { "produces": [ "application/json" @@ -4228,51 +4241,32 @@ "tags": [ "repository" ], - "summary": "Lists all artifacts for a repository run", - "operationId": "getArtifactsOfRun", + "summary": "Get a repository's actions runner registration token", + "operationId": "repoGetRunnerRegistrationToken", "parameters": [ { "type": "string", - "description": "name of the owner", + "description": "owner of the repo", "name": "owner", "in": "path", "required": true }, { "type": "string", - "description": "name of the repository", + "description": "name of the repo", "name": "repo", "in": "path", "required": true - }, - { - "type": "integer", - "description": "runid of the workflow run", - "name": "run", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "name of the artifact", - "name": "name", - "in": "query" } ], "responses": { "200": { - "$ref": "#/responses/ArtifactsList" - }, - "400": { - "$ref": "#/responses/error" - }, - "404": { - "$ref": "#/responses/notFound" + "$ref": "#/responses/RegistrationToken" } } } }, - "/repos/{owner}/{repo}/actions/runs/{run}/jobs/{job}/logs": { + "/repos/{owner}/{repo}/actions/runs/{run}/artifacts": { "get": { "produces": [ "application/json" @@ -4280,8 +4274,8 @@ "tags": [ "repository" ], - "summary": "Downloads the logs for a workflow run redirects to blob url", - "operationId": "downloadActionsRunJobLogs", + "summary": "Lists all artifacts for a repository run", + "operationId": "getArtifactsOfRun", "parameters": [ { "type": "string", @@ -4299,22 +4293,21 @@ }, { "type": "integer", - "description": "id of the run, this could be latest", + "description": "runid of the workflow run", "name": "run", "in": "path", "required": true }, { - "type": "integer", - "description": "id of the job", - "name": "job", - "in": "path", - "required": true + "type": "string", + "description": "name of the artifact", + "name": "name", + "in": "query" } ], "responses": { "200": { - "description": "output blob content" + "$ref": "#/responses/ArtifactsList" }, "400": { "$ref": "#/responses/error" diff --git a/tests/integration/actions_log_test.go b/tests/integration/actions_log_test.go index 32364f738dee9..af2a532ac4d7a 100644 --- a/tests/integration/actions_log_test.go +++ b/tests/integration/actions_log_test.go @@ -12,6 +12,7 @@ import ( "testing" "time" + 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" @@ -36,7 +37,7 @@ func TestDownloadTaskLogs(t *testing.T) { { treePath: ".gitea/workflows/download-task-logs-zstd.yml", fileContent: `name: download-task-logs-zstd -on: +on: push: paths: - '.gitea/workflows/download-task-logs-zstd.yml' @@ -68,7 +69,7 @@ jobs: { treePath: ".gitea/workflows/download-task-logs-no-zstd.yml", fileContent: `name: download-task-logs-no-zstd -on: +on: push: paths: - '.gitea/workflows/download-task-logs-no-zstd.yml' @@ -151,8 +152,14 @@ jobs: } runID, _ := strconv.ParseInt(task.Context.GetFields()["run_id"].GetStringValue(), 10, 64) + + jobs, err := actions_model.GetRunJobsByRunID(t.Context(), runID) + assert.NoError(t, err) + assert.Len(t, jobs, 1) + jobID := jobs[0].ID + // download task logs from API and check content - req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs/%d/jobs/0/logs", user2.Name, repo.Name, runID)). + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/jobs/%d/logs", user2.Name, repo.Name, jobID)). AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) logTextLines = strings.Split(strings.TrimSpace(resp.Body.String()), "\n") From 898ffff1644a1c52b3466aba59f459bebdb0b979 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 17 Mar 2025 12:20:41 -0700 Subject: [PATCH 08/12] Remove unused function --- routers/api/v1/repo/actions_run.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/routers/api/v1/repo/actions_run.go b/routers/api/v1/repo/actions_run.go index d547720b5f180..e53cc4d338cbb 100644 --- a/routers/api/v1/repo/actions_run.go +++ b/routers/api/v1/repo/actions_run.go @@ -9,16 +9,6 @@ import ( "code.gitea.io/gitea/services/context" ) -func getRunID(ctx *context.APIContext) int64 { - // if run param is "latest", get the latest run index - if ctx.PathParam("run") == "latest" { - if run, _ := actions_model.GetLatestRun(ctx, ctx.Repo.Repository.ID); run != nil { - return run.ID - } - } - return ctx.PathParamInt64("run") -} - func DownloadActionsRunJobLogs(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/actions/jobs/{job_id}/logs repository downloadActionsRunJobLogs // --- From 470d05e50016222dcf78eba47d9095894e12a8e2 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 17 Mar 2025 13:05:03 -0700 Subject: [PATCH 09/12] Update routers/api/v1/repo/actions_run.go Co-authored-by: ChristopherHX --- routers/api/v1/repo/actions_run.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routers/api/v1/repo/actions_run.go b/routers/api/v1/repo/actions_run.go index e53cc4d338cbb..72bdc549f56fe 100644 --- a/routers/api/v1/repo/actions_run.go +++ b/routers/api/v1/repo/actions_run.go @@ -12,7 +12,7 @@ import ( func DownloadActionsRunJobLogs(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/actions/jobs/{job_id}/logs repository downloadActionsRunJobLogs // --- - // summary: Downloads the logs for a workflow run redirects to blob url + // summary: Downloads the job logs for a workflow run // produces: // - application/json // parameters: From 520e732bd6d348f88d4242e762a16be1e11b205a Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 17 Mar 2025 13:05:21 -0700 Subject: [PATCH 10/12] Update models/actions/run_job_list.go Co-authored-by: ChristopherHX --- models/actions/run_job_list.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/actions/run_job_list.go b/models/actions/run_job_list.go index 8d8e3f726a081..1d50c9c8dd054 100644 --- a/models/actions/run_job_list.go +++ b/models/actions/run_job_list.go @@ -24,7 +24,7 @@ func (jobs ActionJobList) GetRunIDs() []int64 { func (jobs ActionJobList) LoadRepos(ctx context.Context) error { repoIDs := container.FilterSlice(jobs, func(j *ActionRunJob) (int64, bool) { - return j.RepoID, j.RepoID != 0 + return j.RepoID, j.RepoID != 0 && j.Repo == nil }) if len(repoIDs) == 0 { return nil From 8cbf5f2b32968007f175fa4e908465e11d555ead Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Tue, 18 Mar 2025 10:05:30 -0700 Subject: [PATCH 11/12] Fix lint --- templates/swagger/v1_json.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 935ebcfc733d0..de7c8dc6f09d5 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -4195,7 +4195,7 @@ "tags": [ "repository" ], - "summary": "Downloads the logs for a workflow run redirects to blob url", + "summary": "Downloads the job logs for a workflow run", "operationId": "downloadActionsRunJobLogs", "parameters": [ { From e712ff0e2d67e63badb853fe99247fa715d07939 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Wed, 26 Mar 2025 15:51:20 +0800 Subject: [PATCH 12/12] fix --- routers/api/v1/repo/actions_run.go | 18 ++++++---- routers/common/actions.go | 56 +++++++++--------------------- routers/web/repo/actions/view.go | 6 +++- 3 files changed, 34 insertions(+), 46 deletions(-) diff --git a/routers/api/v1/repo/actions_run.go b/routers/api/v1/repo/actions_run.go index 72bdc549f56fe..c6d18af6aa70a 100644 --- a/routers/api/v1/repo/actions_run.go +++ b/routers/api/v1/repo/actions_run.go @@ -4,7 +4,10 @@ package repo import ( + "errors" + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/services/context" ) @@ -40,19 +43,22 @@ func DownloadActionsRunJobLogs(ctx *context.APIContext) { // "$ref": "#/responses/notFound" jobID := ctx.PathParamInt64("job_id") - if jobID == 0 { - ctx.APIError(400, "invalid job id") - return - } curJob, err := actions_model.GetRunJobByID(ctx, jobID) if err != nil { ctx.APIErrorInternal(err) return } - if err := curJob.LoadRepo(ctx); err != nil { + if err = curJob.LoadRepo(ctx); err != nil { ctx.APIErrorInternal(err) return } - common.DownloadActionsRunJobLogs(ctx.Base, ctx.Repo.Repository, curJob) + err = common.DownloadActionsRunJobLogs(ctx.Base, ctx.Repo.Repository, curJob) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.APIErrorNotFound(err) + } else { + ctx.APIErrorInternal(err) + } + } } diff --git a/routers/common/actions.go b/routers/common/actions.go index ed72057f0cf57..3e9198b4f43da 100644 --- a/routers/common/actions.go +++ b/routers/common/actions.go @@ -5,77 +5,54 @@ package common import ( "fmt" - "net/http" "strings" actions_model "code.gitea.io/gitea/models/actions" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/actions" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" ) -func DownloadActionsRunJobLogsWithIndex(ctx *context.Base, ctxRepo *repo_model.Repository, runID, jobIndex int64) { - if runID == 0 { - ctx.HTTPError(http.StatusBadRequest, "invalid run id") - return - } - +func DownloadActionsRunJobLogsWithIndex(ctx *context.Base, ctxRepo *repo_model.Repository, runID, jobIndex int64) error { runJobs, err := actions_model.GetRunJobsByRunID(ctx, runID) if err != nil { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) - return + return fmt.Errorf("GetRunJobsByRunID: %w", err) } - if len(runJobs) == 0 { - ctx.HTTPError(http.StatusNotFound) - return + if err = runJobs.LoadRepos(ctx); err != nil { + return fmt.Errorf("LoadRepos: %w", err) } - if err := runJobs.LoadRepos(ctx); err != nil { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) - return - } - - var curJob *actions_model.ActionRunJob - if jobIndex >= 0 && jobIndex < int64(len(runJobs)) { - curJob = runJobs[jobIndex] + if 0 < jobIndex || jobIndex >= int64(len(runJobs)) { + return util.NewNotExistErrorf("job index is out of range: %d", jobIndex) } - if curJob == nil { - ctx.HTTPError(http.StatusNotFound) - return - } - - DownloadActionsRunJobLogs(ctx, ctxRepo, curJob) + return DownloadActionsRunJobLogs(ctx, ctxRepo, runJobs[jobIndex]) } -func DownloadActionsRunJobLogs(ctx *context.Base, ctxRepo *repo_model.Repository, curJob *actions_model.ActionRunJob) { +func DownloadActionsRunJobLogs(ctx *context.Base, ctxRepo *repo_model.Repository, curJob *actions_model.ActionRunJob) error { if curJob.Repo.ID != ctxRepo.ID { - ctx.HTTPError(http.StatusNotFound) - return + return util.NewNotExistErrorf("job not found") } if curJob.TaskID == 0 { - ctx.HTTPError(http.StatusNotFound, "job is not started") - return + return util.NewNotExistErrorf("job not started") } if err := curJob.LoadRun(ctx); err != nil { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) - return + return fmt.Errorf("LoadRun: %w", err) } task, err := actions_model.GetTaskByID(ctx, curJob.TaskID) if err != nil { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) - return + return fmt.Errorf("GetTaskByID: %w", err) } + if task.LogExpired { - ctx.HTTPError(http.StatusNotFound, "logs have been cleaned up") - return + return util.NewNotExistErrorf("logs have been cleaned up") } reader, err := actions.OpenLogs(ctx, task.LogInStorage, task.LogFilename) if err != nil { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) - return + return fmt.Errorf("OpenLogs: %w", err) } defer reader.Close() @@ -90,4 +67,5 @@ func DownloadActionsRunJobLogs(ctx *context.Base, ctxRepo *repo_model.Repository ContentTypeCharset: "utf-8", Disposition: "attachment", }) + return nil } diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 6ac6fd4c6398b..eb6fc6ded69c2 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -477,7 +477,11 @@ func Logs(ctx *context_module.Context) { return } - common.DownloadActionsRunJobLogsWithIndex(ctx.Base, ctx.Repo.Repository, run.ID, jobIndex) + if err = common.DownloadActionsRunJobLogsWithIndex(ctx.Base, ctx.Repo.Repository, run.ID, jobIndex); err != nil { + ctx.NotFoundOrServerError("DownloadActionsRunJobLogsWithIndex", func(err error) bool { + return errors.Is(err, util.ErrNotExist) + }, err) + } } func Cancel(ctx *context_module.Context) {