diff --git a/models/actions/artifact.go b/models/actions/artifact.go index 757bd13acd..ec5cc0e32f 100644 --- a/models/actions/artifact.go +++ b/models/actions/artifact.go @@ -170,10 +170,10 @@ type ActionArtifactMeta struct { } // ListUploadedArtifactsMeta returns all uploaded artifacts meta of a run -func ListUploadedArtifactsMeta(ctx context.Context, runID int64) ([]*ActionArtifactMeta, error) { +func ListUploadedArtifactsMeta(ctx context.Context, repoID, runID int64) ([]*ActionArtifactMeta, error) { arts := make([]*ActionArtifactMeta, 0, 10) return arts, db.GetEngine(ctx).Table("action_artifact"). - Where("run_id=? AND (status=? OR status=?)", runID, ArtifactStatusUploadConfirmed, ArtifactStatusExpired). + Where("repo_id=? AND run_id=? AND (status=? OR status=?)", repoID, runID, ArtifactStatusUploadConfirmed, ArtifactStatusExpired). GroupBy("artifact_name"). Select("artifact_name, sum(file_size) as file_size, max(status) as status"). Find(&arts) diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index c9a13f8e21..80744253d8 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -3709,6 +3709,10 @@ "actions.runs.not_done": "This workflow run is not done.", "actions.runs.view_workflow_file": "View workflow file", "actions.runs.workflow_graph": "Workflow Graph", + "actions.runs.summary": "Summary", + "actions.runs.all_jobs": "All jobs", + "actions.runs.triggered_via": "Triggered via %s", + "actions.runs.total_duration": "Total duration:", "actions.workflow.disable": "Disable Workflow", "actions.workflow.disable_success": "Workflow '%s' disabled successfully.", "actions.workflow.enable": "Enable Workflow", diff --git a/routers/web/devtest/mock_actions.go b/routers/web/devtest/mock_actions.go index ac3483239a..00ca095e71 100644 --- a/routers/web/devtest/mock_actions.go +++ b/routers/web/devtest/mock_actions.go @@ -59,15 +59,14 @@ func generateMockStepsLog(logCur actions.LogCursor, opts generateMockStepsLogOpt } func MockActionsView(ctx *context.Context) { - ctx.Data["RunID"] = ctx.PathParam("run") - ctx.Data["JobID"] = ctx.PathParam("job") + ctx.Data["RunID"] = ctx.PathParamInt64("run") + ctx.Data["JobID"] = ctx.PathParamInt64("job") ctx.HTML(http.StatusOK, "devtest/repo-action-view") } func MockActionsRunsJobs(ctx *context.Context) { runID := ctx.PathParamInt64("run") - req := web.GetForm(ctx).(*actions.ViewRequest) resp := &actions.ViewResponse{} resp.State.Run.TitleHTML = `mock run title link` resp.State.Run.Link = setting.AppSubURL + "/devtest/repo-action-view/runs/" + strconv.FormatInt(runID, 10) @@ -79,6 +78,9 @@ func MockActionsRunsJobs(ctx *context.Context) { resp.State.Run.CanDeleteArtifact = true resp.State.Run.WorkflowID = "workflow-id" resp.State.Run.WorkflowLink = "./workflow-link" + resp.State.Run.Duration = "1h 23m 45s" + resp.State.Run.TriggeredAt = time.Now().Add(-time.Hour).Unix() + resp.State.Run.TriggerEvent = "push" resp.State.Run.Commit = actions.ViewCommit{ ShortSha: "ccccdddd", Link: "./commit-link", @@ -140,6 +142,17 @@ func MockActionsRunsJobs(ctx *context.Context) { Needs: []string{"job-100", "job-101"}, }) + fillViewRunResponseCurrentJob(ctx, resp) + ctx.JSON(http.StatusOK, resp) +} + +func fillViewRunResponseCurrentJob(ctx *context.Context, resp *actions.ViewResponse) { + jobID := ctx.PathParamInt64("job") + if jobID == 0 { + return + } + + req := web.GetForm(ctx).(*actions.ViewRequest) var mockLogOptions []generateMockStepsLogOptions resp.State.CurrentJob.Steps = append(resp.State.CurrentJob.Steps, &actions.ViewJobStep{ Summary: "step 0 (mock slow)", @@ -163,7 +176,6 @@ func MockActionsRunsJobs(ctx *context.Context) { mockLogOptions = append(mockLogOptions, generateMockStepsLogOptions{mockCountFirst: 30, mockCountGeneral: 3, groupRepeat: 3}) if len(req.LogCursors) == 0 { - ctx.JSON(http.StatusOK, resp) return } @@ -189,5 +201,4 @@ func MockActionsRunsJobs(ctx *context.Context) { } else { time.Sleep(time.Duration(100) * time.Millisecond) // actually, frontend reload every 1 second, any smaller delay is fine } - ctx.JSON(http.StatusOK, resp) } diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 5c010dfbed..98d86f0bb3 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -38,41 +38,54 @@ import ( "github.com/nektos/act/pkg/model" ) -func getRunID(ctx *context_module.Context) int64 { - // if run param is "latest", get the latest run id - if ctx.PathParam("run") == "latest" { - if run, _ := actions_model.GetLatestRun(ctx, ctx.Repo.Repository.ID); run != nil { - return run.ID +func findCurrentJobByPathParam(ctx *context_module.Context, jobs []*actions_model.ActionRunJob) (job *actions_model.ActionRunJob, hasPathParam bool) { + selectedJobID := ctx.PathParamInt64("job") + if selectedJobID <= 0 { + return nil, false + } + for _, job = range jobs { + if job.ID == selectedJobID { + return job, true } } - return ctx.PathParamInt64("run") + return nil, true +} + +func getCurrentRunByPathParam(ctx *context_module.Context) (run *actions_model.ActionRun) { + var err error + // if run param is "latest", get the latest run id + if ctx.PathParam("run") == "latest" { + run, err = actions_model.GetLatestRun(ctx, ctx.Repo.Repository.ID) + } else { + run, err = actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("run")) + } + if errors.Is(err, util.ErrNotExist) { + ctx.NotFound(nil) + } else if err != nil { + ctx.ServerError("GetRun:"+ctx.PathParam("run"), err) + } + return run } func View(ctx *context_module.Context) { ctx.Data["PageIsActions"] = true - runID := getRunID(ctx) - - _, _, current := getRunJobsAndCurrentJob(ctx, runID) + run := getCurrentRunByPathParam(ctx) if ctx.Written() { return } - - ctx.Data["RunID"] = runID - ctx.Data["JobID"] = current.ID + ctx.Data["RunID"] = run.ID + ctx.Data["JobID"] = ctx.PathParamInt64("job") // it can be 0 when no job (e.g.: run summary view) ctx.Data["ActionsURL"] = ctx.Repo.RepoLink + "/actions" ctx.HTML(http.StatusOK, tplViewActions) } func ViewWorkflowFile(ctx *context_module.Context) { - runID := getRunID(ctx) - run, err := actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, runID) - if err != nil { - ctx.NotFoundOrServerError("GetRunByRepoAndID", func(err error) bool { - return errors.Is(err, util.ErrNotExist) - }, err) + run := getCurrentRunByPathParam(ctx) + if ctx.Written() { return } + commit, err := ctx.Repo.GitRepo.GetCommit(run.CommitSHA) if err != nil { ctx.NotFoundOrServerError("GetCommit", func(err error) bool { @@ -130,6 +143,10 @@ type ViewResponse struct { IsSchedule bool `json:"isSchedule"` Jobs []*ViewJob `json:"jobs"` Commit ViewCommit `json:"commit"` + // Summary view: run duration and trigger time/event + Duration string `json:"duration"` + TriggeredAt int64 `json:"triggeredAt"` // unix seconds for relative time + TriggerEvent string `json:"triggerEvent"` // e.g. pull_request, push, schedule } `json:"run"` CurrentJob struct { Title string `json:"title"` @@ -190,11 +207,7 @@ type ViewStepLogLine struct { } func getActionsViewArtifacts(ctx context.Context, repoID, runID int64) (artifactsViewItems []*ArtifactsViewItem, err error) { - run, err := actions_model.GetRunByRepoAndID(ctx, repoID, runID) - if err != nil { - return nil, err - } - artifacts, err := actions_model.ListUploadedArtifactsMeta(ctx, run.ID) + artifacts, err := actions_model.ListUploadedArtifactsMeta(ctx, repoID, runID) if err != nil { return nil, err } @@ -209,10 +222,7 @@ func getActionsViewArtifacts(ctx context.Context, repoID, runID int64) (artifact } func ViewPost(ctx *context_module.Context) { - req := web.GetForm(ctx).(*ViewRequest) - runID := getRunID(ctx) - - run, jobs, current := getRunJobsAndCurrentJob(ctx, runID) + run, jobs := getCurrentRunJobsByPathParam(ctx) if ctx.Written() { return } @@ -221,14 +231,24 @@ func ViewPost(ctx *context_module.Context) { return } - var err error resp := &ViewResponse{} - resp.Artifacts, err = getActionsViewArtifacts(ctx, ctx.Repo.Repository.ID, runID) + fillViewRunResponseSummary(ctx, resp, run, jobs) + if ctx.Written() { + return + } + fillViewRunResponseCurrentJob(ctx, resp, run, jobs) + if ctx.Written() { + return + } + ctx.JSON(http.StatusOK, resp) +} + +func fillViewRunResponseSummary(ctx *context_module.Context, resp *ViewResponse, run *actions_model.ActionRun, jobs []*actions_model.ActionRunJob) { + var err error + resp.Artifacts, err = getActionsViewArtifacts(ctx, ctx.Repo.Repository.ID, run.ID) if err != nil { - if !errors.Is(err, util.ErrNotExist) { - ctx.ServerError("getActionsViewArtifacts", err) - return - } + ctx.ServerError("getActionsViewArtifacts", err) + return } // the title for the "run" is from the commit message @@ -289,6 +309,20 @@ func ViewPost(ctx *context_module.Context) { Pusher: pusher, Branch: branch, } + resp.State.Run.Duration = run.Duration().String() + resp.State.Run.TriggeredAt = run.Created.AsTime().Unix() + resp.State.Run.TriggerEvent = run.TriggerEvent +} + +func fillViewRunResponseCurrentJob(ctx *context_module.Context, resp *ViewResponse, run *actions_model.ActionRun, jobs []*actions_model.ActionRunJob) { + req := web.GetForm(ctx).(*ViewRequest) + current, hasPathParam := findCurrentJobByPathParam(ctx, jobs) + if current == nil { + if hasPathParam { + ctx.NotFound(nil) + } + return + } var task *actions_model.ActionTask if current.TaskID > 0 { @@ -321,8 +355,6 @@ func ViewPost(ctx *context_module.Context) { resp.State.CurrentJob.Steps = append(resp.State.CurrentJob.Steps, steps...) resp.Logs.StepsLog = append(resp.Logs.StepsLog, logs...) } - - ctx.JSON(http.StatusOK, resp) } func convertToViewModel(ctx context.Context, locale translation.Locale, cursors []LogCursor, task *actions_model.ActionTask) ([]*ViewJobStep, []*ViewStepLog, error) { @@ -426,19 +458,22 @@ func checkRunRerunAllowed(ctx *context_module.Context, run *actions_model.Action // Rerun will rerun jobs in the given run // If jobIDStr is a blank string, it means rerun all jobs func Rerun(ctx *context_module.Context) { - runID := getRunID(ctx) - - run, jobs, currentJob := getRunJobsAndCurrentJob(ctx, runID) + run, jobs := getCurrentRunJobsByPathParam(ctx) if ctx.Written() { return } - if !checkRunRerunAllowed(ctx, run) { return } + currentJob, hasPathParam := findCurrentJobByPathParam(ctx, jobs) + if hasPathParam && currentJob == nil { + ctx.NotFound(nil) + return + } + var jobsToRerun []*actions_model.ActionRunJob - if ctx.PathParam("job") != "" { + if currentJob != nil { jobsToRerun = actions_service.GetAllRerunJobs(currentJob, jobs) } else { jobsToRerun = jobs @@ -454,13 +489,10 @@ func Rerun(ctx *context_module.Context) { // RerunFailed reruns all failed jobs in the given run func RerunFailed(ctx *context_module.Context) { - runID := getRunID(ctx) - - run, jobs, _ := getRunJobsAndCurrentJob(ctx, runID) + run, jobs := getCurrentRunJobsByPathParam(ctx) if ctx.Written() { return } - if !checkRunRerunAllowed(ctx, run) { return } @@ -474,18 +506,13 @@ func RerunFailed(ctx *context_module.Context) { } func Logs(ctx *context_module.Context) { - runID := getRunID(ctx) - jobID := ctx.PathParamInt64("job") - - run, err := actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, runID) - if err != nil { - ctx.NotFoundOrServerError("GetRunByRepoAndID", func(err error) bool { - return errors.Is(err, util.ErrNotExist) - }, err) + run := getCurrentRunByPathParam(ctx) + if ctx.Written() { return } + jobID := ctx.PathParamInt64("job") - 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); err != nil { ctx.NotFoundOrServerError("DownloadActionsRunJobLogsWithID", func(err error) bool { return errors.Is(err, util.ErrNotExist) }, err) @@ -493,9 +520,7 @@ func Logs(ctx *context_module.Context) { } func Cancel(ctx *context_module.Context) { - runID := getRunID(ctx) - - run, jobs, _ := getRunJobsAndCurrentJob(ctx, runID) + run, jobs := getCurrentRunJobsByPathParam(ctx) if ctx.Written() { return } @@ -529,9 +554,11 @@ func Cancel(ctx *context_module.Context) { } func Approve(ctx *context_module.Context) { - runID := getRunID(ctx) - - approveRuns(ctx, []int64{runID}) + run := getCurrentRunByPathParam(ctx) + if ctx.Written() { + return + } + approveRuns(ctx, []int64{run.ID}) if ctx.Written() { return } @@ -606,16 +633,8 @@ func approveRuns(ctx *context_module.Context, runIDs []int64) { } func Delete(ctx *context_module.Context) { - runID := getRunID(ctx) - repoID := ctx.Repo.Repository.ID - - run, err := actions_model.GetRunByRepoAndID(ctx, repoID, runID) - if err != nil { - if errors.Is(err, util.ErrNotExist) { - ctx.JSONErrorNotFound() - return - } - ctx.ServerError("GetRunByRepoAndID", err) + run := getCurrentRunByPathParam(ctx) + if ctx.Written() { return } @@ -632,59 +651,37 @@ func Delete(ctx *context_module.Context) { ctx.JSONOK() } -// getRunJobsAndCurrentJob loads the run and its jobs for runID, and returns the selected job based on the optional "job" path param (or the first job by default). -// Any error will be written to the ctx, and nils are returned in that case. -func getRunJobsAndCurrentJob(ctx *context_module.Context, runID int64) (*actions_model.ActionRun, []*actions_model.ActionRunJob, *actions_model.ActionRunJob) { - run, err := actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, runID) - if err != nil { - ctx.NotFoundOrServerError("GetRunByRepoAndID", func(err error) bool { - return errors.Is(err, util.ErrNotExist) - }, err) - return nil, nil, nil +// getRunJobs loads the run and its jobs for runID +// Any error will be written to the ctx, empty jobs will also result in 404 error, then the return values are all nil. +func getCurrentRunJobsByPathParam(ctx *context_module.Context) (*actions_model.ActionRun, []*actions_model.ActionRunJob) { + run := getCurrentRunByPathParam(ctx) + if ctx.Written() { + return nil, nil } run.Repo = ctx.Repo.Repository jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID) if err != nil { ctx.ServerError("GetRunJobsByRunID", err) - return nil, nil, nil + return nil, nil } if len(jobs) == 0 { ctx.NotFound(nil) - return nil, nil, nil + return nil, nil } for _, job := range jobs { job.Run = run } - - current := jobs[0] - if ctx.PathParam("job") != "" { - jobID := ctx.PathParamInt64("job") - current, err = actions_model.GetRunJobByRunAndID(ctx, run.ID, jobID) - if err != nil { - ctx.NotFoundOrServerError("GetRunJobByRunAndID", func(err error) bool { - return errors.Is(err, util.ErrNotExist) - }, err) - return nil, nil, nil - } - current.Run = run - } - - return run, jobs, current + return run, jobs } func ArtifactsDeleteView(ctx *context_module.Context) { - runID := getRunID(ctx) - artifactName := ctx.PathParam("artifact_name") - - run, err := actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, runID) - if err != nil { - ctx.NotFoundOrServerError("GetRunByRepoAndID", func(err error) bool { - return errors.Is(err, util.ErrNotExist) - }, err) + run := getCurrentRunByPathParam(ctx) + if ctx.Written() { return } - if err = actions_model.SetArtifactNeedDelete(ctx, run.ID, artifactName); err != nil { + artifactName := ctx.PathParam("artifact_name") + if err := actions_model.SetArtifactNeedDelete(ctx, run.ID, artifactName); err != nil { ctx.ServerError("SetArtifactNeedDelete", err) return } @@ -692,19 +689,12 @@ func ArtifactsDeleteView(ctx *context_module.Context) { } func ArtifactsDownloadView(ctx *context_module.Context) { - runID := getRunID(ctx) - artifactName := ctx.PathParam("artifact_name") - - run, err := actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, runID) - if err != nil { - if errors.Is(err, util.ErrNotExist) { - ctx.HTTPError(http.StatusNotFound, err.Error()) - return - } - ctx.ServerError("GetRunByRepoAndID", err) + run := getCurrentRunByPathParam(ctx) + if ctx.Written() { return } + artifactName := ctx.PathParam("artifact_name") artifacts, err := db.Find[actions_model.ActionArtifact](ctx, actions_model.FindArtifactsOptions{ RunID: run.ID, ArtifactName: artifactName, diff --git a/routers/web/web.go b/routers/web/web.go index f09f1bde8f..6893e380df 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1737,8 +1737,10 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { m.Any("/mail-preview", devtest.MailPreview) m.Any("/mail-preview/*", devtest.MailPreviewRender) 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.Post("/actions-mock/runs/{run}/jobs/{job}", web.Bind(actions.ViewRequest{}), devtest.MockActionsRunsJobs) + 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) }) } diff --git a/templates/devtest/repo-action-view.tmpl b/templates/devtest/repo-action-view.tmpl index b3a52db1e2..46f040d8a6 100644 --- a/templates/devtest/repo-action-view.tmpl +++ b/templates/devtest/repo-action-view.tmpl @@ -1,14 +1,14 @@ {{template "base/head" .}}
-
- Run:CanCancel - Run:CanApprove - Run:CanRerun + {{template "repo/actions/view_component" (dict "RunID" (or .RunID 10) "JobID" (or .JobID 0) - "ActionsURL" (print AppSubUrl "/devtest/actions-mock") + "ActionsURL" (print AppSubUrl "/devtest/repo-action-view") )}}
{{template "base/footer" .}} diff --git a/templates/repo/actions/view_component.tmpl b/templates/repo/actions/view_component.tmpl index 9fa77d8a3f..59b5c9cbf9 100644 --- a/templates/repo/actions/view_component.tmpl +++ b/templates/repo/actions/view_component.tmpl @@ -12,6 +12,10 @@ data-locale-runs-commit="{{ctx.Locale.Tr "actions.runs.commit"}}" data-locale-runs-pushed-by="{{ctx.Locale.Tr "actions.runs.pushed_by"}}" data-locale-runs-workflow-graph="{{ctx.Locale.Tr "actions.runs.workflow_graph"}}" + data-locale-summary="{{ctx.Locale.Tr "actions.runs.summary"}}" + data-locale-all-jobs="{{ctx.Locale.Tr "actions.runs.all_jobs"}}" + data-locale-triggered-via="{{ctx.Locale.Tr "actions.runs.triggered_via"}}" + data-locale-total-duration="{{ctx.Locale.Tr "actions.runs.total_duration"}}" data-locale-status-unknown="{{ctx.Locale.Tr "actions.status.unknown"}}" data-locale-status-waiting="{{ctx.Locale.Tr "actions.status.waiting"}}" data-locale-status-running="{{ctx.Locale.Tr "actions.status.running"}}" diff --git a/web_src/js/components/ActionRunJobView.vue b/web_src/js/components/ActionRunJobView.vue new file mode 100644 index 0000000000..9d8ee0dbde --- /dev/null +++ b/web_src/js/components/ActionRunJobView.vue @@ -0,0 +1,684 @@ + + + + + diff --git a/web_src/js/components/ActionRunSummaryView.vue b/web_src/js/components/ActionRunSummaryView.vue new file mode 100644 index 0000000000..2d79a82288 --- /dev/null +++ b/web_src/js/components/ActionRunSummaryView.vue @@ -0,0 +1,63 @@ + + + diff --git a/web_src/js/components/RepoActionView.test.ts b/web_src/js/components/ActionRunView.test.ts similarity index 93% rename from web_src/js/components/RepoActionView.test.ts rename to web_src/js/components/ActionRunView.test.ts index a0855ecf24..f0e3fa090a 100644 --- a/web_src/js/components/RepoActionView.test.ts +++ b/web_src/js/components/ActionRunView.test.ts @@ -1,4 +1,4 @@ -import {createLogLineMessage, parseLogLineCommand} from './RepoActionView.vue'; +import {createLogLineMessage, parseLogLineCommand} from './ActionRunView.ts'; test('LogLineMessage', () => { const cases = { diff --git a/web_src/js/components/ActionRunView.ts b/web_src/js/components/ActionRunView.ts new file mode 100644 index 0000000000..6ae09a46fe --- /dev/null +++ b/web_src/js/components/ActionRunView.ts @@ -0,0 +1,154 @@ +import {createElementFromAttrs} from '../utils/dom.ts'; +import {renderAnsi} from '../render/ansi.ts'; +import {reactive} from 'vue'; +import type {ActionsArtifact, ActionsJob, ActionsRun, ActionsRunStatus} from '../modules/gitea-actions.ts'; +import type {IntervalId} from '../types.ts'; +import {POST} from '../modules/fetch.ts'; + +// How GitHub Actions logs work: +// * Workflow command outputs log commands like "::group::the-title", "::add-matcher::...." +// * Workflow runner parses and processes the commands to "##[group]", apply "matchers", hide secrets, etc. +// * The reported logs are the processed logs. +// HOWEVER: Gitea runner does not completely process those commands. Many works are done by the frontend at the moment. +const LogLinePrefixCommandMap: Record = { + '::group::': 'group', + '##[group]': 'group', + '::endgroup::': 'endgroup', + '##[endgroup]': 'endgroup', + + '##[error]': 'error', + '[command]': 'command', + + // https://github.com/actions/toolkit/blob/master/docs/commands.md + // https://github.com/actions/runner/blob/main/docs/adrs/0276-problem-matchers.md#registration + '::add-matcher::': 'hidden', + '##[add-matcher]': 'hidden', + '::remove-matcher': 'hidden', // it has arguments +}; + +export type LogLine = { + index: number; + timestamp: number; + message: string; +}; + +export type LogLineCommandName = 'group' | 'endgroup' | 'command' | 'error' | 'hidden'; +export type LogLineCommand = { + name: LogLineCommandName, + prefix: string, +}; + +export function parseLogLineCommand(line: LogLine): LogLineCommand | null { + // TODO: in the future it can be refactored to be a general parser that can parse arguments, drop the "prefix match" + for (const prefix of Object.keys(LogLinePrefixCommandMap)) { + if (line.message.startsWith(prefix)) { + return {name: LogLinePrefixCommandMap[prefix], prefix}; + } + } + return null; +} + +export function createLogLineMessage(line: LogLine, cmd: LogLineCommand | null) { + const logMsgAttrs = {class: 'log-msg'}; + if (cmd?.name) logMsgAttrs.class += ` log-cmd-${cmd?.name}`; // make it easier to add styles to some commands like "error" + + // TODO: for some commands (::group::), the "prefix removal" works well, for some commands with "arguments" (::remove-matcher ...::), + // it needs to do further processing in the future (fortunately, at the moment we don't need to handle these commands) + const msgContent = cmd ? line.message.substring(cmd.prefix.length) : line.message; + + const logMsg = createElementFromAttrs('span', logMsgAttrs); + logMsg.innerHTML = renderAnsi(msgContent); + return logMsg; +} + +export function createEmptyActionsRun(): ActionsRun { + return { + link: '', + title: '', + titleHTML: '', + status: '' as ActionsRunStatus, // do not show the status before initialized, otherwise it would show an incorrect "error" icon + canCancel: false, + canApprove: false, + canRerun: false, + canRerunFailed: false, + canDeleteArtifact: false, + done: false, + workflowID: '', + workflowLink: '', + isSchedule: false, + duration: '', + triggeredAt: 0, + triggerEvent: '', + jobs: [] as Array, + commit: { + localeCommit: '', + localePushedBy: '', + shortSHA: '', + link: '', + pusher: { + displayName: '', + link: '', + }, + branch: { + name: '', + link: '', + isDeleted: false, + }, + }, + }; +} + +export function createActionRunViewStore(actionsUrl: string, runId: number) { + let loadingAbortController: AbortController | null = null; + let intervalID: IntervalId | null = null; + const viewData = reactive({ + currentRun: createEmptyActionsRun(), + runArtifacts: [] as Array, + }); + const loadCurrentRun = async () => { + if (loadingAbortController) return; + const abortController = new AbortController(); + loadingAbortController = abortController; + try { + const url = `${actionsUrl}/runs/${runId}`; + const resp = await POST(url, {signal: abortController.signal, data: {}}); + const runResp = await resp.json(); + if (loadingAbortController !== abortController) return; + + viewData.runArtifacts = runResp.artifacts || []; + viewData.currentRun = runResp.state.run; + // clear the interval timer if the job is done + if (viewData.currentRun.done && intervalID) { + clearInterval(intervalID); + intervalID = null; + } + } catch (e) { + // avoid network error while unloading page, and ignore "abort" error + if (e instanceof TypeError || abortController.signal.aborted) return; + throw e; + } finally { + if (loadingAbortController === abortController) loadingAbortController = null; + } + }; + + return reactive({ + viewData, + + async startPollingCurrentRun() { + await loadCurrentRun(); + intervalID = setInterval(() => loadCurrentRun(), 1000); + }, + async forceReloadCurrentRun() { + loadingAbortController?.abort(); + loadingAbortController = null; + await loadCurrentRun(); + }, + stopPollingCurrentRun() { + if (!intervalID) return; + clearInterval(intervalID); + intervalID = null; + }, + }); +} + +export type ActionRunViewStore = ReturnType; diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue index 3c4f6d5273..4ced86b523 100644 --- a/web_src/js/components/RepoActionView.vue +++ b/web_src/js/components/RepoActionView.vue @@ -1,497 +1,40 @@ -