Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion models/actions/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import (
// ActionTask represents a distribution of job
type ActionTask struct {
ID int64
JobID int64
JobID int64 `xorm:"index"`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use runner_id=? and job_id=? , then this index is not needed.

Job *ActionRunJob `xorm:"-"`
Steps []*ActionTaskStep `xorm:"-"`
Attempt int64
Expand Down Expand Up @@ -164,6 +164,30 @@ func GetTaskByID(ctx context.Context, id int64) (*ActionTask, error) {
return &task, nil
}

// GetTasksByJobID returns lightweight task metadata for all attempts of a job,
// ordered by attempt ascending. Only fields needed for the attempts list are selected
// to avoid loading LogIndexes (LONGBLOB) on every request.
func GetTasksByJobID(ctx context.Context, jobID int64) ([]*ActionTask, error) {
var tasks []*ActionTask
return tasks, db.GetEngine(ctx).
Cols("id", "job_id", "attempt", "status", "started", "stopped", "log_expired").
Where("job_id=?", jobID).
OrderBy("attempt ASC").
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since attempt is not indexed, we might could sort it in memory?

Find(&tasks)
}

// GetTaskByJobAndAttempt returns the task for a specific attempt of a job.
func GetTaskByJobAndAttempt(ctx context.Context, jobID, attempt int64) (*ActionTask, error) {
var task ActionTask
has, err := db.GetEngine(ctx).Where("job_id=? AND attempt=?", jobID, attempt).Get(&task)
if err != nil {
return nil, err
} else if !has {
return nil, util.NewNotExistErrorf("task with job_id %d attempt %d", jobID, attempt)
}
return &task, nil
}

func GetRunningTaskByToken(ctx context.Context, token string) (*ActionTask, error) {
errNotExist := fmt.Errorf("task with token %q: %w", token, util.ErrNotExist)
if token == "" {
Expand Down
2 changes: 2 additions & 0 deletions options/locale/locale_en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -3779,6 +3779,8 @@
"actions.variables.update.success": "The variable has been edited.",
"actions.logs.always_auto_scroll": "Always auto scroll logs",
"actions.logs.always_expand_running": "Always expand running logs",
"actions.attempt": "Attempt",
"actions.previous_logs": "Previous logs",
"actions.general": "General",
"actions.general.enable_actions": "Enable Actions",
"actions.general.collaborative_owners_management": "Collaborative Owners Management",
Expand Down
13 changes: 12 additions & 1 deletion routers/api/v1/repo/actions_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package repo

import (
"errors"
"net/http"

actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/modules/util"
Expand Down Expand Up @@ -34,6 +35,11 @@ func DownloadActionsRunJobLogs(ctx *context.APIContext) {
// description: id of the job
// type: integer
// required: true
// - name: attempt
// in: query
// description: the attempt number of the job (0 or omit for latest)
// type: integer
// required: false
// responses:
// "200":
// description: output blob content
Expand All @@ -43,6 +49,11 @@ func DownloadActionsRunJobLogs(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"

jobID := ctx.PathParamInt64("job_id")
attempt := ctx.FormInt64("attempt")
if attempt < 0 {
ctx.APIError(http.StatusBadRequest, util.NewInvalidArgumentErrorf("attempt must be >= 0"))
return
}
curJob, err := actions_model.GetRunJobByRepoAndID(ctx, ctx.Repo.Repository.ID, jobID)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
Expand All @@ -57,7 +68,7 @@ func DownloadActionsRunJobLogs(ctx *context.APIContext) {
return
}

err = common.DownloadActionsRunJobLogs(ctx.Base, ctx.Repo.Repository, curJob)
err = common.DownloadActionsRunJobLogs(ctx.Base, ctx.Repo.Repository, curJob, attempt)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIErrorNotFound(err)
Expand Down
28 changes: 18 additions & 10 deletions routers/common/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,33 +15,41 @@ import (
"code.gitea.io/gitea/services/context"
)

func DownloadActionsRunJobLogsWithID(ctx *context.Base, ctxRepo *repo_model.Repository, runID, jobID int64) error {
func DownloadActionsRunJobLogsWithID(ctx *context.Base, ctxRepo *repo_model.Repository, runID, jobID, attempt int64) error {
job, err := actions_model.GetRunJobByRunAndID(ctx, runID, jobID)
if err != nil {
return err
}
if err := job.LoadRepo(ctx); err != nil {
return fmt.Errorf("LoadRepo: %w", err)
}
return DownloadActionsRunJobLogs(ctx, ctxRepo, job)
return DownloadActionsRunJobLogs(ctx, ctxRepo, job, attempt)
}

func DownloadActionsRunJobLogs(ctx *context.Base, ctxRepo *repo_model.Repository, curJob *actions_model.ActionRunJob) error {
func DownloadActionsRunJobLogs(ctx *context.Base, ctxRepo *repo_model.Repository, curJob *actions_model.ActionRunJob, attempt int64) 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)
var task *actions_model.ActionTask
var err error
if attempt > 0 {
task, err = actions_model.GetTaskByJobAndAttempt(ctx, curJob.ID, attempt)
if err != nil {
return fmt.Errorf("GetTaskByJobAndAttempt: %w", err)
}
} else {
if curJob.TaskID == 0 {
return util.NewNotExistErrorf("job not started")
}
task, err = actions_model.GetTaskByID(ctx, curJob.TaskID)
if err != nil {
return fmt.Errorf("GetTaskByID: %w", err)
}
}

if task.LogExpired {
Expand Down
28 changes: 28 additions & 0 deletions routers/web/devtest/mock_actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package devtest

import (
"fmt"
mathRand "math/rand/v2"
"net/http"
"slices"
Expand All @@ -30,6 +31,7 @@ func generateMockStepsLog(logCur actions.LogCursor, opts generateMockStepsLogOpt
mockedLogs = append(mockedLogs, "::group::test group for: step={step}, cursor={cursor}")
mockedLogs = append(mockedLogs, slices.Repeat([]string{"in group msg for: step={step}, cursor={cursor}"}, opts.groupRepeat)...)
mockedLogs = append(mockedLogs, "::endgroup::")
mockedLogs = append(mockedLogs, "::error::error message for: step={step}, cursor={cursor}")
mockedLogs = append(mockedLogs,
"message for: step={step}, cursor={cursor}",
"message for: step={step}, cursor={cursor}",
Expand Down Expand Up @@ -64,6 +66,17 @@ func MockActionsView(ctx *context.Context) {
ctx.HTML(http.StatusOK, "devtest/repo-action-view")
}

func MockActionsJobLogs(ctx *context.Context) {
runID := ctx.PathParamInt64("run")
jobID := ctx.PathParamInt64("job")
attempt := ctx.FormInt64("attempt")
if attempt <= 0 {
attempt = 3
}

ctx.PlainText(http.StatusOK, fmt.Sprintf("mock run=%d job=%d attempt=%d log line 1\nmock run=%d job=%d attempt=%d log line 2\n", runID, jobID, attempt, runID, jobID, attempt))
}

func MockActionsRunsJobs(ctx *context.Context) {
runID := ctx.PathParamInt64("run")

Expand Down Expand Up @@ -122,6 +135,7 @@ func MockActionsRunsJobs(ctx *context.Context) {
Name: "job 100",
Status: actions_model.StatusRunning.String(),
CanRerun: true,
Attempt: 3,
Duration: "1h",
})
resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{
Expand All @@ -130,6 +144,7 @@ func MockActionsRunsJobs(ctx *context.Context) {
Name: "job 101",
Status: actions_model.StatusWaiting.String(),
CanRerun: false,
Attempt: 1,
Duration: "2h",
Needs: []string{"job-100"},
})
Expand All @@ -139,6 +154,7 @@ func MockActionsRunsJobs(ctx *context.Context) {
Name: "ULTRA LOOOOOOOOOOOONG job name 102 that exceeds the limit",
Status: actions_model.StatusFailure.String(),
CanRerun: false,
Attempt: 2,
Duration: "3h",
Needs: []string{"job-100", "job-101"},
})
Expand All @@ -148,6 +164,7 @@ func MockActionsRunsJobs(ctx *context.Context) {
Name: "job 103",
Status: actions_model.StatusCancelled.String(),
CanRerun: false,
Attempt: 1,
Duration: "2m",
Needs: []string{"job-100"},
})
Expand All @@ -161,6 +178,7 @@ func MockActionsRunsJobs(ctx *context.Context) {
Name: "job dup test " + strconv.Itoa(i),
Status: actions_model.StatusSuccess.String(),
CanRerun: false,
Attempt: 1,
Duration: "2m",
Needs: []string{"job-103", "job-101", "job-100"},
})
Expand All @@ -178,6 +196,16 @@ func fillViewRunResponseCurrentJob(ctx *context.Context, resp *actions.ViewRespo
}

req := web.GetForm(ctx).(*actions.ViewRequest)

if ctx.PathParamInt64("run") == 10 && jobID == 100 {
resp.State.CurrentJob.Attempt = 4
resp.State.CurrentJob.AvailableAttempts = []*actions.ViewAttempt{
{Attempt: 1, Status: actions_model.StatusFailure.String(), Started: time.Now().Add(-2 * time.Hour).Unix(), Stopped: time.Now().Add(-110 * time.Minute).Unix()},
{Attempt: 2, Status: actions_model.StatusCancelled.String(), Started: time.Now().Add(-90 * time.Minute).Unix(), Stopped: time.Now().Add(-80 * time.Minute).Unix()},
{Attempt: 3, Status: actions_model.StatusSuccess.String(), Started: time.Now().Add(-10 * time.Minute).Unix(), LogExpired: true},
}
}

var mockLogOptions []generateMockStepsLogOptions
resp.State.CurrentJob.Steps = append(resp.State.CurrentJob.Steps, &actions.ViewJobStep{
Summary: "step 0 (mock slow)",
Expand Down
48 changes: 43 additions & 5 deletions routers/web/repo/actions/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,11 @@ type ViewResponse struct {
TriggerEvent string `json:"triggerEvent"` // e.g. pull_request, push, schedule
} `json:"run"`
CurrentJob struct {
Title string `json:"title"`
Detail string `json:"detail"`
Steps []*ViewJobStep `json:"steps"`
Title string `json:"title"`
Detail string `json:"detail"`
Attempt int64 `json:"attempt"`
Steps []*ViewJobStep `json:"steps"`
AvailableAttempts []*ViewAttempt `json:"availableAttempts"`
} `json:"currentJob"`
} `json:"state"`
Logs struct {
Expand All @@ -167,6 +169,7 @@ type ViewJob struct {
Name string `json:"name"`
Status string `json:"status"`
CanRerun bool `json:"canRerun"`
Attempt int64 `json:"attempt"`
Duration string `json:"duration"`
Needs []string `json:"needs,omitempty"`
}
Expand Down Expand Up @@ -195,6 +198,14 @@ type ViewJobStep struct {
Status string `json:"status"`
}

type ViewAttempt struct {
Attempt int64 `json:"attempt"`
Status string `json:"status"`
Started int64 `json:"started"` // unix seconds
Stopped int64 `json:"stopped"` // unix seconds
LogExpired bool `json:"logExpired"`
}

type ViewStepLog struct {
Step int `json:"step"`
Cursor int64 `json:"cursor"`
Expand Down Expand Up @@ -283,6 +294,7 @@ func fillViewRunResponseSummary(ctx *context_module.Context, resp *ViewResponse,
Name: v.Name,
Status: v.Status.String(),
CanRerun: resp.State.Run.CanRerun,
Attempt: v.Attempt,
Duration: v.Duration().String(),
Needs: v.Needs,
})
Expand Down Expand Up @@ -327,6 +339,26 @@ func fillViewRunResponseCurrentJob(ctx *context_module.Context, resp *ViewRespon
return
}

if current.Attempt > 1 {
allTasks, err := actions_model.GetTasksByJobID(ctx, current.ID)
if err != nil {
ctx.ServerError("actions_model.GetTasksByJobID", err)
return
}
for _, t := range allTasks {
if t.Attempt == current.Attempt {
continue
}
resp.State.CurrentJob.AvailableAttempts = append(resp.State.CurrentJob.AvailableAttempts, &ViewAttempt{
Attempt: t.Attempt,
Status: t.Status.String(),
Started: t.Started.AsTime().Unix(),
Stopped: t.Stopped.AsTime().Unix(),
LogExpired: t.LogExpired,
})
}
}

var task *actions_model.ActionTask
if current.TaskID > 0 {
var err error
Expand All @@ -344,6 +376,7 @@ func fillViewRunResponseCurrentJob(ctx *context_module.Context, resp *ViewRespon

resp.State.CurrentJob.Title = current.Name
resp.State.CurrentJob.Detail = current.Status.LocaleString(ctx.Locale)
resp.State.CurrentJob.Attempt = current.Attempt
if run.NeedApproval {
resp.State.CurrentJob.Detail = ctx.Locale.TrString("actions.need_approval_desc")
}
Expand Down Expand Up @@ -375,7 +408,7 @@ func convertToViewModel(ctx context.Context, locale translation.Locale, cursors
}

for _, cursor := range cursors {
if !cursor.Expanded {
if !cursor.Expanded || cursor.Step < 0 || cursor.Step >= len(steps) {
continue
}

Expand Down Expand Up @@ -514,8 +547,13 @@ func Logs(ctx *context_module.Context) {
return
}
jobID := ctx.PathParamInt64("job")
attempt := ctx.FormInt64("attempt")
if attempt < 0 {
ctx.HTTPError(http.StatusBadRequest, "attempt")
return
}

if err := common.DownloadActionsRunJobLogsWithID(ctx.Base, ctx.Repo.Repository, run.ID, jobID); err != nil {
if err := common.DownloadActionsRunJobLogsWithID(ctx.Base, ctx.Repo.Repository, run.ID, jobID, attempt); err != nil {
ctx.NotFoundOrServerError("DownloadActionsRunJobLogsWithID", func(err error) bool {
return errors.Is(err, util.ErrNotExist)
}, err)
Expand Down
1 change: 1 addition & 0 deletions routers/web/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -1749,6 +1749,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Any("/{sub}", devtest.TmplCommon)
m.Get("/repo-action-view/runs/{run}", devtest.MockActionsView)
m.Get("/repo-action-view/runs/{run}/jobs/{job}", devtest.MockActionsView)
m.Get("/repo-action-view/runs/{run}/jobs/{job}/logs", devtest.MockActionsJobLogs)
m.Post("/repo-action-view/runs/{run}", web.Bind(actions.ViewRequest{}), devtest.MockActionsRunsJobs)
m.Post("/repo-action-view/runs/{run}/jobs/{job}", web.Bind(actions.ViewRequest{}), devtest.MockActionsRunsJobs)
})
Expand Down
2 changes: 2 additions & 0 deletions templates/repo/actions/view_component.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,7 @@
data-locale-download-logs="{{ctx.Locale.Tr "download_logs"}}"
data-locale-logs-always-auto-scroll="{{ctx.Locale.Tr "actions.logs.always_auto_scroll"}}"
data-locale-logs-always-expand-running="{{ctx.Locale.Tr "actions.logs.always_expand_running"}}"
data-locale-attempt="{{ctx.Locale.Tr "actions.attempt"}}"
data-locale-previous-logs="{{ctx.Locale.Tr "actions.previous_logs"}}"
>
</div>
6 changes: 6 additions & 0 deletions templates/swagger/v1_json.tmpl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading