Skip to content

Commit 0c6957e

Browse files
lunnyChristopherHXwxiaoguang
authored
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 <[email protected]> Co-authored-by: wxiaoguang <[email protected]>
1 parent e0ad72e commit 0c6957e

File tree

8 files changed

+264
-52
lines changed

8 files changed

+264
-52
lines changed

Diff for: models/actions/run_job.go

+19-6
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"time"
1111

1212
"code.gitea.io/gitea/models/db"
13+
repo_model "code.gitea.io/gitea/models/repo"
1314
"code.gitea.io/gitea/modules/timeutil"
1415
"code.gitea.io/gitea/modules/util"
1516

@@ -19,11 +20,12 @@ import (
1920
// ActionRunJob represents a job of a run
2021
type ActionRunJob struct {
2122
ID int64
22-
RunID int64 `xorm:"index"`
23-
Run *ActionRun `xorm:"-"`
24-
RepoID int64 `xorm:"index"`
25-
OwnerID int64 `xorm:"index"`
26-
CommitSHA string `xorm:"index"`
23+
RunID int64 `xorm:"index"`
24+
Run *ActionRun `xorm:"-"`
25+
RepoID int64 `xorm:"index"`
26+
Repo *repo_model.Repository `xorm:"-"`
27+
OwnerID int64 `xorm:"index"`
28+
CommitSHA string `xorm:"index"`
2729
IsForkPullRequest bool
2830
Name string `xorm:"VARCHAR(255)"`
2931
Attempt int64
@@ -58,6 +60,17 @@ func (job *ActionRunJob) LoadRun(ctx context.Context) error {
5860
return nil
5961
}
6062

63+
func (job *ActionRunJob) LoadRepo(ctx context.Context) error {
64+
if job.Repo == nil {
65+
repo, err := repo_model.GetRepositoryByID(ctx, job.RepoID)
66+
if err != nil {
67+
return err
68+
}
69+
job.Repo = repo
70+
}
71+
return nil
72+
}
73+
6174
// LoadAttributes load Run if not loaded
6275
func (job *ActionRunJob) LoadAttributes(ctx context.Context) error {
6376
if job == nil {
@@ -83,7 +96,7 @@ func GetRunJobByID(ctx context.Context, id int64) (*ActionRunJob, error) {
8396
return &job, nil
8497
}
8598

86-
func GetRunJobsByRunID(ctx context.Context, runID int64) ([]*ActionRunJob, error) {
99+
func GetRunJobsByRunID(ctx context.Context, runID int64) (ActionJobList, error) {
87100
var jobs []*ActionRunJob
88101
if err := db.GetEngine(ctx).Where("run_id=?", runID).OrderBy("id").Find(&jobs); err != nil {
89102
return nil, err

Diff for: models/actions/run_job_list.go

+28-7
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"context"
88

99
"code.gitea.io/gitea/models/db"
10+
repo_model "code.gitea.io/gitea/models/repo"
1011
"code.gitea.io/gitea/modules/container"
1112
"code.gitea.io/gitea/modules/timeutil"
1213

@@ -21,7 +22,33 @@ func (jobs ActionJobList) GetRunIDs() []int64 {
2122
})
2223
}
2324

25+
func (jobs ActionJobList) LoadRepos(ctx context.Context) error {
26+
repoIDs := container.FilterSlice(jobs, func(j *ActionRunJob) (int64, bool) {
27+
return j.RepoID, j.RepoID != 0 && j.Repo == nil
28+
})
29+
if len(repoIDs) == 0 {
30+
return nil
31+
}
32+
33+
repos := make(map[int64]*repo_model.Repository, len(repoIDs))
34+
if err := db.GetEngine(ctx).In("id", repoIDs).Find(&repos); err != nil {
35+
return err
36+
}
37+
for _, j := range jobs {
38+
if j.RepoID > 0 && j.Repo == nil {
39+
j.Repo = repos[j.RepoID]
40+
}
41+
}
42+
return nil
43+
}
44+
2445
func (jobs ActionJobList) LoadRuns(ctx context.Context, withRepo bool) error {
46+
if withRepo {
47+
if err := jobs.LoadRepos(ctx); err != nil {
48+
return err
49+
}
50+
}
51+
2552
runIDs := jobs.GetRunIDs()
2653
runs := make(map[int64]*ActionRun, len(runIDs))
2754
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 {
3057
for _, j := range jobs {
3158
if j.RunID > 0 && j.Run == nil {
3259
j.Run = runs[j.RunID]
60+
j.Run.Repo = j.Repo
3361
}
3462
}
35-
if withRepo {
36-
var runsList RunList = make([]*ActionRun, 0, len(runs))
37-
for _, r := range runs {
38-
runsList = append(runsList, r)
39-
}
40-
return runsList.LoadRepos(ctx)
41-
}
4263
return nil
4364
}
4465

Diff for: routers/api/v1/api.go

+4
Original file line numberDiff line numberDiff line change
@@ -1168,6 +1168,10 @@ func Routes() *web.Router {
11681168
m.Post("/{workflow_id}/dispatches", reqRepoWriter(unit.TypeActions), bind(api.CreateActionWorkflowDispatch{}), repo.ActionsDispatchWorkflow)
11691169
}, context.ReferencesGitRepo(), reqToken(), reqRepoReader(unit.TypeActions))
11701170

1171+
m.Group("/actions/jobs", func() {
1172+
m.Get("/{job_id}/logs", repo.DownloadActionsRunJobLogs)
1173+
}, reqToken(), reqRepoReader(unit.TypeActions))
1174+
11711175
m.Group("/hooks/git", func() {
11721176
m.Combo("").Get(repo.ListGitHooks)
11731177
m.Group("/{id}", func() {

Diff for: routers/api/v1/repo/actions_run.go

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package repo
5+
6+
import (
7+
"errors"
8+
9+
actions_model "code.gitea.io/gitea/models/actions"
10+
"code.gitea.io/gitea/modules/util"
11+
"code.gitea.io/gitea/routers/common"
12+
"code.gitea.io/gitea/services/context"
13+
)
14+
15+
func DownloadActionsRunJobLogs(ctx *context.APIContext) {
16+
// swagger:operation GET /repos/{owner}/{repo}/actions/jobs/{job_id}/logs repository downloadActionsRunJobLogs
17+
// ---
18+
// summary: Downloads the job logs for a workflow run
19+
// produces:
20+
// - application/json
21+
// parameters:
22+
// - name: owner
23+
// in: path
24+
// description: name of the owner
25+
// type: string
26+
// required: true
27+
// - name: repo
28+
// in: path
29+
// description: name of the repository
30+
// type: string
31+
// required: true
32+
// - name: job_id
33+
// in: path
34+
// description: id of the job
35+
// type: integer
36+
// required: true
37+
// responses:
38+
// "200":
39+
// description: output blob content
40+
// "400":
41+
// "$ref": "#/responses/error"
42+
// "404":
43+
// "$ref": "#/responses/notFound"
44+
45+
jobID := ctx.PathParamInt64("job_id")
46+
curJob, err := actions_model.GetRunJobByID(ctx, jobID)
47+
if err != nil {
48+
ctx.APIErrorInternal(err)
49+
return
50+
}
51+
if err = curJob.LoadRepo(ctx); err != nil {
52+
ctx.APIErrorInternal(err)
53+
return
54+
}
55+
56+
err = common.DownloadActionsRunJobLogs(ctx.Base, ctx.Repo.Repository, curJob)
57+
if err != nil {
58+
if errors.Is(err, util.ErrNotExist) {
59+
ctx.APIErrorNotFound(err)
60+
} else {
61+
ctx.APIErrorInternal(err)
62+
}
63+
}
64+
}

Diff for: routers/common/actions.go

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package common
5+
6+
import (
7+
"fmt"
8+
"strings"
9+
10+
actions_model "code.gitea.io/gitea/models/actions"
11+
repo_model "code.gitea.io/gitea/models/repo"
12+
"code.gitea.io/gitea/modules/actions"
13+
"code.gitea.io/gitea/modules/util"
14+
"code.gitea.io/gitea/services/context"
15+
)
16+
17+
func DownloadActionsRunJobLogsWithIndex(ctx *context.Base, ctxRepo *repo_model.Repository, runID, jobIndex int64) error {
18+
runJobs, err := actions_model.GetRunJobsByRunID(ctx, runID)
19+
if err != nil {
20+
return fmt.Errorf("GetRunJobsByRunID: %w", err)
21+
}
22+
if err = runJobs.LoadRepos(ctx); err != nil {
23+
return fmt.Errorf("LoadRepos: %w", err)
24+
}
25+
if 0 < jobIndex || jobIndex >= int64(len(runJobs)) {
26+
return util.NewNotExistErrorf("job index is out of range: %d", jobIndex)
27+
}
28+
return DownloadActionsRunJobLogs(ctx, ctxRepo, runJobs[jobIndex])
29+
}
30+
31+
func DownloadActionsRunJobLogs(ctx *context.Base, ctxRepo *repo_model.Repository, curJob *actions_model.ActionRunJob) error {
32+
if curJob.Repo.ID != ctxRepo.ID {
33+
return util.NewNotExistErrorf("job not found")
34+
}
35+
36+
if curJob.TaskID == 0 {
37+
return util.NewNotExistErrorf("job not started")
38+
}
39+
40+
if err := curJob.LoadRun(ctx); err != nil {
41+
return fmt.Errorf("LoadRun: %w", err)
42+
}
43+
44+
task, err := actions_model.GetTaskByID(ctx, curJob.TaskID)
45+
if err != nil {
46+
return fmt.Errorf("GetTaskByID: %w", err)
47+
}
48+
49+
if task.LogExpired {
50+
return util.NewNotExistErrorf("logs have been cleaned up")
51+
}
52+
53+
reader, err := actions.OpenLogs(ctx, task.LogInStorage, task.LogFilename)
54+
if err != nil {
55+
return fmt.Errorf("OpenLogs: %w", err)
56+
}
57+
defer reader.Close()
58+
59+
workflowName := curJob.Run.WorkflowID
60+
if p := strings.Index(workflowName, "."); p > 0 {
61+
workflowName = workflowName[0:p]
62+
}
63+
ctx.ServeContent(reader, &context.ServeHeaderOptions{
64+
Filename: fmt.Sprintf("%v-%v-%v.log", workflowName, curJob.Name, task.ID),
65+
ContentLength: &task.LogSize,
66+
ContentType: "text/plain",
67+
ContentTypeCharset: "utf-8",
68+
Disposition: "attachment",
69+
})
70+
return nil
71+
}

Diff for: routers/web/repo/actions/view.go

+9-39
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import (
1414
"net/http"
1515
"net/url"
1616
"strconv"
17-
"strings"
1817
"time"
1918

2019
actions_model "code.gitea.io/gitea/models/actions"
@@ -31,6 +30,7 @@ import (
3130
"code.gitea.io/gitea/modules/timeutil"
3231
"code.gitea.io/gitea/modules/util"
3332
"code.gitea.io/gitea/modules/web"
33+
"code.gitea.io/gitea/routers/common"
3434
actions_service "code.gitea.io/gitea/services/actions"
3535
context_module "code.gitea.io/gitea/services/context"
3636
notify_service "code.gitea.io/gitea/services/notify"
@@ -469,49 +469,19 @@ func Logs(ctx *context_module.Context) {
469469
runIndex := getRunIndex(ctx)
470470
jobIndex := ctx.PathParamInt64("job")
471471

472-
job, _ := getRunJobs(ctx, runIndex, jobIndex)
473-
if ctx.Written() {
474-
return
475-
}
476-
if job.TaskID == 0 {
477-
ctx.HTTPError(http.StatusNotFound, "job is not started")
478-
return
479-
}
480-
481-
err := job.LoadRun(ctx)
482-
if err != nil {
483-
ctx.HTTPError(http.StatusInternalServerError, err.Error())
484-
return
485-
}
486-
487-
task, err := actions_model.GetTaskByID(ctx, job.TaskID)
488-
if err != nil {
489-
ctx.HTTPError(http.StatusInternalServerError, err.Error())
490-
return
491-
}
492-
if task.LogExpired {
493-
ctx.HTTPError(http.StatusNotFound, "logs have been cleaned up")
494-
return
495-
}
496-
497-
reader, err := actions.OpenLogs(ctx, task.LogInStorage, task.LogFilename)
472+
run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
498473
if err != nil {
499-
ctx.HTTPError(http.StatusInternalServerError, err.Error())
474+
ctx.NotFoundOrServerError("GetRunByIndex", func(err error) bool {
475+
return errors.Is(err, util.ErrNotExist)
476+
}, err)
500477
return
501478
}
502-
defer reader.Close()
503479

504-
workflowName := job.Run.WorkflowID
505-
if p := strings.Index(workflowName, "."); p > 0 {
506-
workflowName = workflowName[0:p]
480+
if err = common.DownloadActionsRunJobLogsWithIndex(ctx.Base, ctx.Repo.Repository, run.ID, jobIndex); err != nil {
481+
ctx.NotFoundOrServerError("DownloadActionsRunJobLogsWithIndex", func(err error) bool {
482+
return errors.Is(err, util.ErrNotExist)
483+
}, err)
507484
}
508-
ctx.ServeContent(reader, &context_module.ServeHeaderOptions{
509-
Filename: fmt.Sprintf("%v-%v-%v.log", workflowName, job.Name, task.ID),
510-
ContentLength: &task.LogSize,
511-
ContentType: "text/plain",
512-
ContentTypeCharset: "utf-8",
513-
Disposition: "attachment",
514-
})
515485
}
516486

517487
func Cancel(ctx *context_module.Context) {

Diff for: templates/swagger/v1_json.tmpl

+46
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)