mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-08 18:04:14 +01:00
Support downloading raw task logs (#24451)
Hi! This pull request adds support for downloading raw task logs for Gitea Actions, similar to Github Actions It looks like the following: ![image](https://user-images.githubusercontent.com/945339/235376746-405d5019-710b-468b-8113-9e82eab8e752.png)
This commit is contained in:
parent
b08647f0b9
commit
f0b773e0ce
7 changed files with 62 additions and 2 deletions
|
@ -73,7 +73,7 @@ func WriteLogs(ctx context.Context, filename string, offset int64, rows []*runne
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReadLogs(ctx context.Context, inStorage bool, filename string, offset, limit int64) ([]*runnerv1.LogRow, error) {
|
func ReadLogs(ctx context.Context, inStorage bool, filename string, offset, limit int64) ([]*runnerv1.LogRow, error) {
|
||||||
f, err := openLogs(ctx, inStorage, filename)
|
f, err := OpenLogs(ctx, inStorage, filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -141,7 +141,7 @@ func RemoveLogs(ctx context.Context, inStorage bool, filename string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func openLogs(ctx context.Context, inStorage bool, filename string) (io.ReadSeekCloser, error) {
|
func OpenLogs(ctx context.Context, inStorage bool, filename string) (io.ReadSeekCloser, error) {
|
||||||
if !inStorage {
|
if !inStorage {
|
||||||
name := DBFSPrefix + filename
|
name := DBFSPrefix + filename
|
||||||
f, err := dbfs.Open(ctx, name)
|
f, err := dbfs.Open(ctx, name)
|
||||||
|
|
|
@ -129,6 +129,7 @@ concept_user_organization = Organization
|
||||||
show_timestamps = Show timestamps
|
show_timestamps = Show timestamps
|
||||||
show_log_seconds = Show seconds
|
show_log_seconds = Show seconds
|
||||||
show_full_screen = Show full screen
|
show_full_screen = Show full screen
|
||||||
|
download_logs = Download logs
|
||||||
|
|
||||||
confirm_delete_selected = Confirm to delete all selected items?
|
confirm_delete_selected = Confirm to delete all selected items?
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
actions_model "code.gitea.io/gitea/models/actions"
|
actions_model "code.gitea.io/gitea/models/actions"
|
||||||
|
@ -310,6 +311,55 @@ func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob) erro
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Logs(ctx *context_module.Context) {
|
||||||
|
runIndex := ctx.ParamsInt64("run")
|
||||||
|
jobIndex := ctx.ParamsInt64("job")
|
||||||
|
|
||||||
|
job, _ := getRunJobs(ctx, runIndex, jobIndex)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if job.TaskID == 0 {
|
||||||
|
ctx.Error(http.StatusNotFound, "job is not started")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := job.LoadRun(ctx)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
task, err := actions_model.GetTaskByID(ctx, job.TaskID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if task.LogExpired {
|
||||||
|
ctx.Error(http.StatusNotFound, "logs have been cleaned up")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, err := actions.OpenLogs(ctx, task.LogInStorage, task.LogFilename)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(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) {
|
func Cancel(ctx *context_module.Context) {
|
||||||
runIndex := ctx.ParamsInt64("run")
|
runIndex := ctx.ParamsInt64("run")
|
||||||
|
|
||||||
|
|
|
@ -1207,6 +1207,7 @@ func registerRoutes(m *web.Route) {
|
||||||
Get(actions.View).
|
Get(actions.View).
|
||||||
Post(web.Bind(actions.ViewRequest{}), actions.ViewPost)
|
Post(web.Bind(actions.ViewRequest{}), actions.ViewPost)
|
||||||
m.Post("/rerun", reqRepoActionsWriter, actions.RerunOne)
|
m.Post("/rerun", reqRepoActionsWriter, actions.RerunOne)
|
||||||
|
m.Get("/logs", actions.Logs)
|
||||||
})
|
})
|
||||||
m.Post("/cancel", reqRepoActionsWriter, actions.Cancel)
|
m.Post("/cancel", reqRepoActionsWriter, actions.Cancel)
|
||||||
m.Post("/approve", reqRepoActionsWriter, actions.Approve)
|
m.Post("/approve", reqRepoActionsWriter, actions.Approve)
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
data-locale-show-timestamps="{{.locale.Tr "show_timestamps"}}"
|
data-locale-show-timestamps="{{.locale.Tr "show_timestamps"}}"
|
||||||
data-locale-show-log-seconds="{{.locale.Tr "show_log_seconds"}}"
|
data-locale-show-log-seconds="{{.locale.Tr "show_log_seconds"}}"
|
||||||
data-locale-show-full-screen="{{.locale.Tr "show_full_screen"}}"
|
data-locale-show-full-screen="{{.locale.Tr "show_full_screen"}}"
|
||||||
|
data-locale-download-logs="{{.locale.Tr "download_logs"}}"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -74,6 +74,10 @@
|
||||||
<SvgIcon name="octicon-gear" :size="18"/>
|
<SvgIcon name="octicon-gear" :size="18"/>
|
||||||
</button>
|
</button>
|
||||||
<div class="menu transition action-job-menu" :class="{visible: menuVisible}" v-if="menuVisible" v-cloak>
|
<div class="menu transition action-job-menu" :class="{visible: menuVisible}" v-if="menuVisible" v-cloak>
|
||||||
|
<a class="item" :href="run.link+'/jobs/'+jobIndex+'/logs'" target="_blank">
|
||||||
|
<i class="icon"><SvgIcon name="octicon-download"/></i>
|
||||||
|
{{ locale.downloadLogs }}
|
||||||
|
</a>
|
||||||
<a class="item" @click="toggleTimeDisplay('seconds')">
|
<a class="item" @click="toggleTimeDisplay('seconds')">
|
||||||
<i class="icon"><SvgIcon v-show="timeVisible['log-time-seconds']" name="octicon-check"/></i>
|
<i class="icon"><SvgIcon v-show="timeVisible['log-time-seconds']" name="octicon-check"/></i>
|
||||||
{{ locale.showLogSeconds }}
|
{{ locale.showLogSeconds }}
|
||||||
|
@ -453,6 +457,7 @@ export function initRepositoryActionView() {
|
||||||
showTimeStamps: el.getAttribute('data-locale-show-timestamps'),
|
showTimeStamps: el.getAttribute('data-locale-show-timestamps'),
|
||||||
showLogSeconds: el.getAttribute('data-locale-show-log-seconds'),
|
showLogSeconds: el.getAttribute('data-locale-show-log-seconds'),
|
||||||
showFullScreen: el.getAttribute('data-locale-show-full-screen'),
|
showFullScreen: el.getAttribute('data-locale-show-full-screen'),
|
||||||
|
downloadLogs: el.getAttribute('data-locale-download-logs'),
|
||||||
status: {
|
status: {
|
||||||
unknown: el.getAttribute('data-locale-status-unknown'),
|
unknown: el.getAttribute('data-locale-status-unknown'),
|
||||||
waiting: el.getAttribute('data-locale-status-waiting'),
|
waiting: el.getAttribute('data-locale-status-waiting'),
|
||||||
|
|
|
@ -22,6 +22,7 @@ import octiconDiffModified from '../../public/img/svg/octicon-diff-modified.svg'
|
||||||
import octiconDiffRemoved from '../../public/img/svg/octicon-diff-removed.svg';
|
import octiconDiffRemoved from '../../public/img/svg/octicon-diff-removed.svg';
|
||||||
import octiconDiffRenamed from '../../public/img/svg/octicon-diff-renamed.svg';
|
import octiconDiffRenamed from '../../public/img/svg/octicon-diff-renamed.svg';
|
||||||
import octiconDotFill from '../../public/img/svg/octicon-dot-fill.svg';
|
import octiconDotFill from '../../public/img/svg/octicon-dot-fill.svg';
|
||||||
|
import octiconDownload from '../../public/img/svg/octicon-download.svg';
|
||||||
import octiconEye from '../../public/img/svg/octicon-eye.svg';
|
import octiconEye from '../../public/img/svg/octicon-eye.svg';
|
||||||
import octiconFile from '../../public/img/svg/octicon-file.svg';
|
import octiconFile from '../../public/img/svg/octicon-file.svg';
|
||||||
import octiconFileDirectoryFill from '../../public/img/svg/octicon-file-directory-fill.svg';
|
import octiconFileDirectoryFill from '../../public/img/svg/octicon-file-directory-fill.svg';
|
||||||
|
@ -91,6 +92,7 @@ const svgs = {
|
||||||
'octicon-diff-removed': octiconDiffRemoved,
|
'octicon-diff-removed': octiconDiffRemoved,
|
||||||
'octicon-diff-renamed': octiconDiffRenamed,
|
'octicon-diff-renamed': octiconDiffRenamed,
|
||||||
'octicon-dot-fill': octiconDotFill,
|
'octicon-dot-fill': octiconDotFill,
|
||||||
|
'octicon-download': octiconDownload,
|
||||||
'octicon-eye': octiconEye,
|
'octicon-eye': octiconEye,
|
||||||
'octicon-file': octiconFile,
|
'octicon-file': octiconFile,
|
||||||
'octicon-file-directory-fill': octiconFileDirectoryFill,
|
'octicon-file-directory-fill': octiconFileDirectoryFill,
|
||||||
|
|
Loading…
Reference in a new issue