diff --git a/models/actions/run_job.go b/models/actions/run_job.go index edd1170072..78e7ac192d 100644 --- a/models/actions/run_job.go +++ b/models/actions/run_job.go @@ -6,12 +6,15 @@ package actions import ( "context" "fmt" + "html/template" "slices" + "strings" "time" "forgejo.org/models/db" "forgejo.org/modules/container" "forgejo.org/modules/timeutil" + "forgejo.org/modules/translation" "forgejo.org/modules/util" "xorm.io/builder" @@ -229,3 +232,24 @@ func AggregateJobStatus(jobs []*ActionRunJob) Status { return StatusUnknown // it shouldn't happen } } + +// StatusDiagnostics returns optional diagnostic information to display to the user derived from +// ActionRunJob's current status. It should help the user understand in which state the +// ActionRunJob is and why. +func (job *ActionRunJob) StatusDiagnostics(lang translation.Locale) []template.HTML { + diagnostics := []template.HTML{} + + switch job.Status { + case StatusWaiting: + joinedLabels := strings.Join(job.RunsOn, ", ") + diagnostics = append(diagnostics, lang.TrPluralString(len(job.RunsOn), "actions.status.diagnostics.waiting", joinedLabels)) + default: + diagnostics = append(diagnostics, template.HTML(job.Status.LocaleString(lang))) + } + + if job.Run.NeedApproval { + diagnostics = append(diagnostics, template.HTML(lang.TrString("actions.need_approval_desc"))) + } + + return diagnostics +} diff --git a/models/actions/run_job_test.go b/models/actions/run_job_test.go index 6abdb2bf5c..8f9e4b0ba2 100644 --- a/models/actions/run_job_test.go +++ b/models/actions/run_job_test.go @@ -4,10 +4,12 @@ package actions import ( "fmt" + "html/template" "testing" "forgejo.org/models/db" "forgejo.org/models/unittest" + "forgejo.org/modules/translation" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -70,3 +72,79 @@ func TestActionRunJob_HTMLURL(t *testing.T) { }) } } + +func TestActionRunJob_StatusDiagnostics(t *testing.T) { + translation.InitLocales(t.Context()) + english := translation.NewLocale("en-US") + + tests := []struct { + name string + job ActionRunJob + expected []template.HTML + }{ + { + name: "Unknown status", + job: ActionRunJob{RunsOn: []string{"windows"}, Status: StatusUnknown, Run: &ActionRun{NeedApproval: false}}, + expected: []template.HTML{"Unknown"}, + }, + { + name: "Waiting without labels", + job: ActionRunJob{RunsOn: []string{}, Status: StatusWaiting, Run: &ActionRun{NeedApproval: false}}, + expected: []template.HTML{"Waiting for a runner with the following labels: "}, + }, + { + name: "Waiting with one label", + job: ActionRunJob{RunsOn: []string{"freebsd"}, Status: StatusWaiting, Run: &ActionRun{NeedApproval: false}}, + expected: []template.HTML{"Waiting for a runner with the following label: freebsd"}, + }, + { + name: "Waiting with labels, no approval", + job: ActionRunJob{RunsOn: []string{"docker", "ubuntu"}, Status: StatusWaiting, Run: &ActionRun{NeedApproval: false}}, + expected: []template.HTML{"Waiting for a runner with the following labels: docker, ubuntu"}, + }, + { + name: "Waiting with labels, approval", + job: ActionRunJob{RunsOn: []string{"docker", "ubuntu"}, Status: StatusWaiting, Run: &ActionRun{NeedApproval: true}}, + expected: []template.HTML{ + "Waiting for a runner with the following labels: docker, ubuntu", + "Need approval to run workflows for fork pull request.", + }, + }, + { + name: "Running", + job: ActionRunJob{RunsOn: []string{"debian"}, Status: StatusRunning, Run: &ActionRun{NeedApproval: false}}, + expected: []template.HTML{"Running"}, + }, + { + name: "Success", + job: ActionRunJob{RunsOn: []string{"debian"}, Status: StatusSuccess, Run: &ActionRun{NeedApproval: false}}, + expected: []template.HTML{"Success"}, + }, + { + name: "Failure", + job: ActionRunJob{RunsOn: []string{"debian"}, Status: StatusFailure, Run: &ActionRun{NeedApproval: false}}, + expected: []template.HTML{"Failure"}, + }, + { + name: "Cancelled", + job: ActionRunJob{RunsOn: []string{"debian"}, Status: StatusCancelled, Run: &ActionRun{NeedApproval: false}}, + expected: []template.HTML{"Canceled"}, + }, + { + name: "Skipped", + job: ActionRunJob{RunsOn: []string{"debian"}, Status: StatusSkipped, Run: &ActionRun{NeedApproval: false}}, + expected: []template.HTML{"Skipped"}, + }, + { + name: "Blocked", + job: ActionRunJob{RunsOn: []string{"debian"}, Status: StatusBlocked, Run: &ActionRun{NeedApproval: false}}, + expected: []template.HTML{"Blocked"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, tt.job.StatusDiagnostics(english)) + }) + } +} diff --git a/options/locale_next/locale_en-US.json b/options/locale_next/locale_en-US.json index edca1072a8..77aa288f48 100644 --- a/options/locale_next/locale_en-US.json +++ b/options/locale_next/locale_en-US.json @@ -180,6 +180,10 @@ "one": "%s download", "other": "%s downloads" }, + "actions.status.diagnostics.waiting": { + "one": "Waiting for a runner with the following label: %s", + "other": "Waiting for a runner with the following labels: %s" + }, "actions.runs.run_attempt_label": "Run attempt #%[1]s (%[2]s)", "actions.runs.viewing_out_of_date_run": "You are viewing an out-of-date run of this job that was executed %[1]s.", "actions.runs.view_most_recent_run": "View most recent run", diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 55999702d2..d56d993ae5 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -182,10 +182,10 @@ type ViewRunInfo struct { } type ViewCurrentJob struct { - Title string `json:"title"` - Detail string `json:"detail"` - Steps []*ViewJobStep `json:"steps"` - AllAttempts []*TaskAttempt `json:"allAttempts"` + Title string `json:"title"` + Details []template.HTML `json:"details"` + Steps []*ViewJobStep `json:"steps"` + AllAttempts []*TaskAttempt `json:"allAttempts"` } type ViewLogs struct { @@ -358,10 +358,8 @@ func getViewResponse(ctx *context_module.Context, req *ViewRequest, runIndex, jo } resp.State.CurrentJob.Title = current.Name - resp.State.CurrentJob.Detail = current.Status.LocaleString(ctx.Locale) - if run.NeedApproval { - resp.State.CurrentJob.Detail = ctx.Locale.TrString("actions.need_approval_desc") - } + resp.State.CurrentJob.Details = current.StatusDiagnostics(ctx.Locale) + resp.State.CurrentJob.Steps = make([]*ViewJobStep, 0) // marshal to '[]' instead of 'null' in json resp.Logs.StepsLog = make([]*ViewStepLog, 0) // marshal to '[]' instead of 'null' in json // As noted above with TaskID; task will be nil when the job hasn't be picked yet... diff --git a/routers/web/repo/actions/view_test.go b/routers/web/repo/actions/view_test.go index e3f72e9e37..4e2dab676c 100644 --- a/routers/web/repo/actions/view_test.go +++ b/routers/web/repo/actions/view_test.go @@ -181,8 +181,8 @@ func baseExpectedViewResponse() *ViewResponse { }, }, CurrentJob: ViewCurrentJob{ - Title: "job_2", - Detail: "actions.status.success", + Title: "job_2", + Details: []template.HTML{"actions.status.success"}, Steps: []*ViewJobStep{ { Summary: "Set up job", @@ -333,7 +333,7 @@ func TestActionsViewViewPost(t *testing.T) { // Expected blank data in the response because this job isn't picked by a runner yet. Keep details here // in-sync with the RepoActionView 'view non-picked action run job' test. - resp.State.CurrentJob.Detail = "actions.status.waiting" + resp.State.CurrentJob.Details = []template.HTML{"actions.status.diagnostics.waiting"} resp.State.CurrentJob.Steps = []*ViewJobStep{} resp.State.CurrentJob.AllAttempts = nil }, diff --git a/tests/integration/actions_view_test.go b/tests/integration/actions_view_test.go index 8cd643ca4b..8ccaed8f7b 100644 --- a/tests/integration/actions_view_test.go +++ b/tests/integration/actions_view_test.go @@ -153,7 +153,7 @@ func TestActionViewsView(t *testing.T) { re = regexp.MustCompile(pattern) actualClean = re.ReplaceAllString(actualClean, `"time_since_started_html":"_time_"`) - return assert.JSONEq(t, "{\"state\":{\"run\":{\"preExecutionError\":\"\",\"link\":\"/user5/repo4/actions/runs/187\",\"title\":\"update actions\",\"titleHTML\":\"update actions\",\"status\":\"success\",\"canCancel\":false,\"canApprove\":false,\"canRerun\":false,\"canDeleteArtifact\":false,\"done\":true,\"jobs\":[{\"id\":192,\"name\":\"job_2\",\"status\":\"success\",\"canRerun\":false,\"duration\":\"_duration_\"}],\"commit\":{\"localeCommit\":\"Commit\",\"localePushedBy\":\"pushed by\",\"localeWorkflow\":\"Workflow\",\"shortSHA\":\"c2d72f5484\",\"link\":\"/user5/repo4/commit/c2d72f548424103f01ee1dc02889c1e2bff816b0\",\"pusher\":{\"displayName\":\"user1\",\"link\":\"/user1\"},\"branch\":{\"name\":\"master\",\"link\":\"/user5/repo4/src/branch/master\",\"isDeleted\":false}}},\"currentJob\":{\"title\":\"job_2\",\"detail\":\"Success\",\"steps\":[{\"summary\":\"Set up job\",\"duration\":\"_duration_\",\"status\":\"success\"},{\"summary\":\"Complete job\",\"duration\":\"_duration_\",\"status\":\"success\"}],\"allAttempts\":[{\"number\":3,\"time_since_started_html\":\"_time_\",\"status\":\"running\"},{\"number\":2,\"time_since_started_html\":\"_time_\",\"status\":\"success\"},{\"number\":1,\"time_since_started_html\":\"_time_\",\"status\":\"success\"}]}},\"logs\":{\"stepsLog\":[]}}\n", actualClean) + return assert.JSONEq(t, "{\"state\":{\"run\":{\"preExecutionError\":\"\",\"link\":\"/user5/repo4/actions/runs/187\",\"title\":\"update actions\",\"titleHTML\":\"update actions\",\"status\":\"success\",\"canCancel\":false,\"canApprove\":false,\"canRerun\":false,\"canDeleteArtifact\":false,\"done\":true,\"jobs\":[{\"id\":192,\"name\":\"job_2\",\"status\":\"success\",\"canRerun\":false,\"duration\":\"_duration_\"}],\"commit\":{\"localeCommit\":\"Commit\",\"localePushedBy\":\"pushed by\",\"localeWorkflow\":\"Workflow\",\"shortSHA\":\"c2d72f5484\",\"link\":\"/user5/repo4/commit/c2d72f548424103f01ee1dc02889c1e2bff816b0\",\"pusher\":{\"displayName\":\"user1\",\"link\":\"/user1\"},\"branch\":{\"name\":\"master\",\"link\":\"/user5/repo4/src/branch/master\",\"isDeleted\":false}}},\"currentJob\":{\"title\":\"job_2\",\"details\":[\"Success\"],\"steps\":[{\"summary\":\"Set up job\",\"duration\":\"_duration_\",\"status\":\"success\"},{\"summary\":\"Complete job\",\"duration\":\"_duration_\",\"status\":\"success\"}],\"allAttempts\":[{\"number\":3,\"time_since_started_html\":\"_time_\",\"status\":\"running\"},{\"number\":2,\"time_since_started_html\":\"_time_\",\"status\":\"success\"},{\"number\":1,\"time_since_started_html\":\"_time_\",\"status\":\"success\"}]}},\"logs\":{\"stepsLog\":[]}}\n", actualClean) }) htmlDoc.AssertAttrEqual(t, selector, "data-initial-artifacts-response", "{\"artifacts\":[{\"name\":\"multi-file-download\",\"size\":2048,\"status\":\"completed\"}]}\n") } @@ -185,7 +185,7 @@ func TestActionViewsViewAttemptOutOfRange(t *testing.T) { re = regexp.MustCompile(pattern) actualClean = re.ReplaceAllString(actualClean, `"time_since_started_html":"_time_"`) - return assert.JSONEq(t, "{\"state\":{\"run\":{\"preExecutionError\":\"\",\"link\":\"/user5/repo4/actions/runs/190\",\"title\":\"job output\",\"titleHTML\":\"job output\",\"status\":\"success\",\"canCancel\":false,\"canApprove\":false,\"canRerun\":false,\"canDeleteArtifact\":false,\"done\":false,\"jobs\":[{\"id\":396,\"name\":\"job_2\",\"status\":\"waiting\",\"canRerun\":false,\"duration\":\"_duration_\"}],\"commit\":{\"localeCommit\":\"Commit\",\"localePushedBy\":\"pushed by\",\"localeWorkflow\":\"Workflow\",\"shortSHA\":\"c2d72f5484\",\"link\":\"/user5/repo4/commit/c2d72f548424103f01ee1dc02889c1e2bff816b0\",\"pusher\":{\"displayName\":\"user1\",\"link\":\"/user1\"},\"branch\":{\"name\":\"test\",\"link\":\"/user5/repo4/src/branch/test\",\"isDeleted\":true}}},\"currentJob\":{\"title\":\"job_2\",\"detail\":\"Waiting\",\"steps\":[],\"allAttempts\":null}},\"logs\":{\"stepsLog\":[]}}\n", actualClean) + return assert.JSONEq(t, "{\"state\":{\"run\":{\"preExecutionError\":\"\",\"link\":\"/user5/repo4/actions/runs/190\",\"title\":\"job output\",\"titleHTML\":\"job output\",\"status\":\"success\",\"canCancel\":false,\"canApprove\":false,\"canRerun\":false,\"canDeleteArtifact\":false,\"done\":false,\"jobs\":[{\"id\":396,\"name\":\"job_2\",\"status\":\"waiting\",\"canRerun\":false,\"duration\":\"_duration_\"}],\"commit\":{\"localeCommit\":\"Commit\",\"localePushedBy\":\"pushed by\",\"localeWorkflow\":\"Workflow\",\"shortSHA\":\"c2d72f5484\",\"link\":\"/user5/repo4/commit/c2d72f548424103f01ee1dc02889c1e2bff816b0\",\"pusher\":{\"displayName\":\"user1\",\"link\":\"/user1\"},\"branch\":{\"name\":\"test\",\"link\":\"/user5/repo4/src/branch/test\",\"isDeleted\":true}}},\"currentJob\":{\"title\":\"job_2\",\"details\":[\"Waiting for a runner with the following label: fedora\"],\"steps\":[],\"allAttempts\":null}},\"logs\":{\"stepsLog\":[]}}\n", actualClean) }) htmlDoc.AssertAttrEqual(t, selector, "data-initial-artifacts-response", "{\"artifacts\":[]}\n") } diff --git a/web_src/js/components/RepoActionView.test.js b/web_src/js/components/RepoActionView.test.js index b06751b8cf..71ea39a9c2 100644 --- a/web_src/js/components/RepoActionView.test.js +++ b/web_src/js/components/RepoActionView.test.js @@ -614,7 +614,7 @@ test('view non-picked action run job', async () => { }, currentJob: { title: 'check-1', - detail: 'waiting (locale)', // locale-specific, not exact match to backend test + details: ['waiting (locale)'], // locale-specific, not exact match to backend test steps: [], allAttempts: null, }, @@ -624,7 +624,7 @@ test('view non-picked action run job', async () => { }); await flushPromises(); - expect(wrapper.get('.job-info-header-detail').text()).toEqual('waiting (locale)'); + expect(wrapper.get('.job-info-header-detail li:first-child').text()).toEqual('waiting (locale)'); expect(wrapper.get('.job-brief-list .job-brief-item:nth-of-type(1) .job-brief-name').text()).toEqual('check-1'); expect(wrapper.get('.job-brief-list .job-brief-item:nth-of-type(2) .job-brief-name').text()).toEqual('check-2'); expect(wrapper.get('.job-brief-list .job-brief-item:nth-of-type(3) .job-brief-name').text()).toEqual('check-3'); diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue index e8259abd86..88db677243 100644 --- a/web_src/js/components/RepoActionView.vue +++ b/web_src/js/components/RepoActionView.vue @@ -110,7 +110,7 @@ export default { }, currentJob: { title: '', - detail: '', + details: [], steps: [ // { // summary: '', @@ -615,9 +615,11 @@ export default {

{{ currentJob.title }}

-

- {{ currentJob.detail }} -

+