1
1
mirror of https://github.com/go-gitea/gitea synced 2025-07-22 18:28:37 +00:00

Download actions job logs from API (#33858)

Related to #33709, #31416

It's similar with
https://docs.github.com/en/rest/actions/workflow-jobs?apiVersion=2022-11-28#download-job-logs-for-a-workflow-run--code-samples.

This use `job_id` as path parameter which is consistent with Github's
APIs.

---------

Co-authored-by: ChristopherHX <christopher.homberger@web.de>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
Lunny Xiao
2025-03-26 11:30:52 -07:00
committed by GitHub
parent e0ad72e223
commit 0c6957ef8d
8 changed files with 264 additions and 52 deletions

View File

@@ -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/jobs", func() {
m.Get("/{job_id}/logs", repo.DownloadActionsRunJobLogs)
}, reqToken(), reqRepoReader(unit.TypeActions))
m.Group("/hooks/git", func() {
m.Combo("").Get(repo.ListGitHooks)
m.Group("/{id}", func() {

View File

@@ -0,0 +1,64 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
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"
)
func DownloadActionsRunJobLogs(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/jobs/{job_id}/logs repository downloadActionsRunJobLogs
// ---
// summary: Downloads the job logs for a workflow run
// 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: job_id
// in: path
// description: id of the job
// type: integer
// required: true
// responses:
// "200":
// description: output blob content
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
jobID := ctx.PathParamInt64("job_id")
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
}
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)
}
}
}

71
routers/common/actions.go Normal file
View File

@@ -0,0 +1,71 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package common
import (
"fmt"
"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) error {
runJobs, err := actions_model.GetRunJobsByRunID(ctx, runID)
if err != nil {
return fmt.Errorf("GetRunJobsByRunID: %w", err)
}
if err = runJobs.LoadRepos(ctx); err != nil {
return fmt.Errorf("LoadRepos: %w", err)
}
if 0 < jobIndex || jobIndex >= int64(len(runJobs)) {
return util.NewNotExistErrorf("job index is out of range: %d", jobIndex)
}
return DownloadActionsRunJobLogs(ctx, ctxRepo, runJobs[jobIndex])
}
func DownloadActionsRunJobLogs(ctx *context.Base, ctxRepo *repo_model.Repository, curJob *actions_model.ActionRunJob) error {
if curJob.Repo.ID != ctxRepo.ID {
return util.NewNotExistErrorf("job not found")
}
if curJob.TaskID == 0 {
return util.NewNotExistErrorf("job not started")
}
if err := curJob.LoadRun(ctx); err != nil {
return fmt.Errorf("LoadRun: %w", err)
}
task, err := actions_model.GetTaskByID(ctx, curJob.TaskID)
if err != nil {
return fmt.Errorf("GetTaskByID: %w", err)
}
if task.LogExpired {
return util.NewNotExistErrorf("logs have been cleaned up")
}
reader, err := actions.OpenLogs(ctx, task.LogInStorage, task.LogFilename)
if err != nil {
return fmt.Errorf("OpenLogs: %w", err)
}
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",
})
return nil
}

View File

@@ -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,19 @@ 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)
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
}
task, err := actions_model.GetTaskByID(ctx, job.TaskID)
if err != nil {
ctx.HTTPError(http.StatusInternalServerError, err.Error())
return
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)
}
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_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",
})
}
func Cancel(ctx *context_module.Context) {