Skip to content

Commit 318d171

Browse files
committed
Download actions logs from API
1 parent 91610a9 commit 318d171

File tree

3 files changed

+200
-0
lines changed

3 files changed

+200
-0
lines changed

routers/api/v1/api.go

Lines changed: 4 additions & 0 deletions
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/runs", func() {
1172+
m.Get("/{run_id}/jobs/{job}/logs", repo.DownloadActionsRunLogs)
1173+
}, context.ReferencesGitRepo(), reqToken(), reqRepoReader(unit.TypeActions))
1174+
11711175
m.Group("/hooks/git", func() {
11721176
m.Combo("").Get(repo.ListGitHooks)
11731177
m.Group("/{id}", func() {

routers/api/v1/repo/actions_run.go

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package repo
5+
6+
import (
7+
"errors"
8+
"fmt"
9+
"net/http"
10+
"strings"
11+
12+
actions_model "code.gitea.io/gitea/models/actions"
13+
"code.gitea.io/gitea/modules/actions"
14+
"code.gitea.io/gitea/modules/util"
15+
"code.gitea.io/gitea/services/context"
16+
)
17+
18+
func getRunIndex(ctx *context.APIContext) int64 {
19+
// if run param is "latest", get the latest run index
20+
if ctx.PathParam("run_id") == "latest" {
21+
if run, _ := actions_model.GetLatestRun(ctx, ctx.Repo.Repository.ID); run != nil {
22+
return run.Index
23+
}
24+
}
25+
return ctx.PathParamInt64("run_id")
26+
}
27+
28+
// getRunJobs gets the jobs of runIndex, and returns jobs[jobIndex], jobs.
29+
// Any error will be written to the ctx.
30+
// It never returns a nil job of an empty jobs, if the jobIndex is out of range, it will be treated as 0.
31+
func getRunJobs(ctx *context.APIContext, runIndex, jobIndex int64) (*actions_model.ActionRunJob, []*actions_model.ActionRunJob) {
32+
run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
33+
if err != nil {
34+
if errors.Is(err, util.ErrNotExist) {
35+
ctx.HTTPError(http.StatusNotFound, err.Error())
36+
return nil, nil
37+
}
38+
ctx.HTTPError(http.StatusInternalServerError, err.Error())
39+
return nil, nil
40+
}
41+
run.Repo = ctx.Repo.Repository
42+
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
43+
if err != nil {
44+
ctx.HTTPError(http.StatusInternalServerError, err.Error())
45+
return nil, nil
46+
}
47+
if len(jobs) == 0 {
48+
ctx.HTTPError(http.StatusNotFound)
49+
return nil, nil
50+
}
51+
52+
for _, v := range jobs {
53+
v.Run = run
54+
}
55+
56+
if jobIndex >= 0 && jobIndex < int64(len(jobs)) {
57+
return jobs[jobIndex], jobs
58+
}
59+
return jobs[0], jobs
60+
}
61+
62+
func DownloadActionsRunLogs(ctx *context.APIContext) {
63+
// swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run_id}/logs repository downloadActionsRunLogs
64+
// ---
65+
// summary: Downloads the logs for a workflow run redirects to blob url
66+
// produces:
67+
// - application/json
68+
// parameters:
69+
// - name: owner
70+
// in: path
71+
// description: name of the owner
72+
// type: string
73+
// required: true
74+
// - name: repo
75+
// in: path
76+
// description: name of the repository
77+
// type: string
78+
// required: true
79+
// - name: run_id
80+
// in: path
81+
// description: id of the run, this could be latest
82+
// type: integer
83+
// required: true
84+
// - name: job
85+
// in: path
86+
// description: id of the job
87+
// type: integer
88+
// required: true
89+
// responses:
90+
// "302":
91+
// description: redirect to the blob download
92+
// "400":
93+
// "$ref": "#/responses/error"
94+
// "404":
95+
// "$ref": "#/responses/notFound"
96+
97+
runIndex := getRunIndex(ctx)
98+
jobIndex := ctx.PathParamInt64("job")
99+
100+
job, _ := getRunJobs(ctx, runIndex, jobIndex)
101+
if ctx.Written() {
102+
return
103+
}
104+
if job.TaskID == 0 {
105+
ctx.HTTPError(http.StatusNotFound, "job is not started")
106+
return
107+
}
108+
109+
err := job.LoadRun(ctx)
110+
if err != nil {
111+
ctx.HTTPError(http.StatusInternalServerError, err.Error())
112+
return
113+
}
114+
115+
task, err := actions_model.GetTaskByID(ctx, job.TaskID)
116+
if err != nil {
117+
ctx.HTTPError(http.StatusInternalServerError, err.Error())
118+
return
119+
}
120+
if task.LogExpired {
121+
ctx.HTTPError(http.StatusNotFound, "logs have been cleaned up")
122+
return
123+
}
124+
125+
reader, err := actions.OpenLogs(ctx, task.LogInStorage, task.LogFilename)
126+
if err != nil {
127+
ctx.HTTPError(http.StatusInternalServerError, err.Error())
128+
return
129+
}
130+
defer reader.Close()
131+
132+
workflowName := job.Run.WorkflowID
133+
if p := strings.Index(workflowName, "."); p > 0 {
134+
workflowName = workflowName[0:p]
135+
}
136+
ctx.ServeContent(reader, &context.ServeHeaderOptions{
137+
Filename: fmt.Sprintf("%v-%v-%v.log", workflowName, job.Name, task.ID),
138+
ContentLength: &task.LogSize,
139+
ContentType: "text/plain",
140+
ContentTypeCharset: "utf-8",
141+
Disposition: "attachment",
142+
})
143+
}

templates/swagger/v1_json.tmpl

Lines changed: 53 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)