diff --git a/models/actions/run_job.go b/models/actions/run_job.go index 08a157431d..92e27928f1 100644 --- a/models/actions/run_job.go +++ b/models/actions/run_job.go @@ -315,3 +315,24 @@ func GetQueueDepthByLabels(ctx context.Context) ([]QueueDepthByLabel, error) { return result, nil } + +// GetWaitingJobsByLabel returns all waiting/blocked jobs that have the specified label in their RunsOn +func GetWaitingJobsByLabel(ctx context.Context, label string) ([]*ActionRunJob, error) { + var jobs []*ActionRunJob + err := db.GetEngine(ctx).Where("status IN (?, ?)", StatusWaiting, StatusBlocked).Find(&jobs) + if err != nil { + return nil, err + } + + // Filter by label + var filtered []*ActionRunJob + for _, job := range jobs { + for _, l := range job.RunsOn { + if l == label { + filtered = append(filtered, job) + break + } + } + } + return filtered, nil +} diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 5b83804114..740631d4cd 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -4219,6 +4219,9 @@ "actions.runners.current_task": "Current Task", "actions.runners.cleanup_requested": "Cleanup request sent to runner", "actions.runners.cleanup_request_failed": "Failed to send cleanup request", + "actions.runners.waiting_jobs": "Waiting Jobs", + "actions.runners.back_to_runners": "Back to Runners", + "actions.runners.no_waiting_jobs": "No jobs waiting for this label", "admin.ai_learning": "AI Learning", "admin.ai_learning.edit": "Edit Pattern", "admin.ai_learning.total_patterns": "Total Patterns", diff --git a/routers/web/shared/actions/runners.go b/routers/web/shared/actions/runners.go index 04f5656d39..249ee6bb17 100644 --- a/routers/web/shared/actions/runners.go +++ b/routers/web/shared/actions/runners.go @@ -29,14 +29,15 @@ import ( const ( // TODO: Separate secrets from runners when layout is ready - tplRepoRunners templates.TplName = "repo/settings/actions" - tplOrgRunners templates.TplName = "org/settings/actions" - tplAdminRunners templates.TplName = "admin/actions" - tplUserRunners templates.TplName = "user/settings/actions" - tplRepoRunnerEdit templates.TplName = "repo/settings/runner_edit" - tplOrgRunnerEdit templates.TplName = "org/settings/runners_edit" - tplAdminRunnerEdit templates.TplName = "admin/runners/edit" - tplUserRunnerEdit templates.TplName = "user/settings/runner_edit" + tplRepoRunners templates.TplName = "repo/settings/actions" + tplOrgRunners templates.TplName = "org/settings/actions" + tplAdminRunners templates.TplName = "admin/actions" + tplUserRunners templates.TplName = "user/settings/actions" + tplRepoRunnerEdit templates.TplName = "repo/settings/runner_edit" + tplOrgRunnerEdit templates.TplName = "org/settings/runners_edit" + tplAdminRunnerEdit templates.TplName = "admin/runners/edit" + tplUserRunnerEdit templates.TplName = "user/settings/runner_edit" + tplAdminWaitingJobs templates.TplName = "admin/runners/waiting_jobs" ) type runnersCtx struct { @@ -809,3 +810,33 @@ func QueueDepthJSON(ctx *context.Context) { ctx.JSON(http.StatusOK, response) } + +// WaitingJobs shows waiting jobs filtered by label +func WaitingJobs(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("actions.runners.waiting_jobs") + ctx.Data["PageIsAdminRunners"] = true + + label := ctx.FormString("label") + ctx.Data["Label"] = label + + jobs, err := actions_model.GetWaitingJobsByLabel(ctx, label) + if err != nil { + ctx.ServerError("GetWaitingJobsByLabel", err) + return + } + + // Load repo and run info for each job + for _, job := range jobs { + if err := job.LoadRepo(ctx); err != nil { + log.Error("LoadRepo for job %d: %v", job.ID, err) + } + if err := job.LoadRun(ctx); err != nil { + log.Error("LoadRun for job %d: %v", job.ID, err) + } + } + + ctx.Data["Jobs"] = jobs + ctx.Data["Link"] = setting.AppSubURL + "/-/admin/actions/runners" + + ctx.HTML(http.StatusOK, tplAdminWaitingJobs) +} diff --git a/routers/web/web.go b/routers/web/web.go index 689479d58c..a5e5c7c2bc 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -517,6 +517,7 @@ func registerWebRoutes(m *web.Router) { m.Post("/{runnerid}/use-suggested-labels", shared_actions.RunnerUseSuggestedLabels) m.Get("/status", shared_actions.RunnersStatusJSON) m.Get("/queue-depth", shared_actions.QueueDepthJSON) + m.Get("/queue", shared_actions.WaitingJobs) m.Get("/{runnerid}/status", shared_actions.RunnerStatusJSON) m.Post("/reset_registration_token", shared_actions.ResetRunnerRegistrationToken) }) diff --git a/templates/admin/runners/waiting_jobs.tmpl b/templates/admin/runners/waiting_jobs.tmpl new file mode 100644 index 0000000000..451fbf857a --- /dev/null +++ b/templates/admin/runners/waiting_jobs.tmpl @@ -0,0 +1,72 @@ +{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin runners")}} +
+

+ {{ctx.Locale.Tr "actions.runners.waiting_jobs"}} + {{if .Label}} + {{.Label}} + {{end}} + + + +

+
+ {{if .Jobs}} + + + + + + + + + + + + + {{range .Jobs}} + + + + + + + + + {{end}} + +
{{ctx.Locale.Tr "actions.runners.task_list.run"}}{{ctx.Locale.Tr "actions.runners.task_list.repository"}}{{ctx.Locale.Tr "admin.packages.name"}}{{ctx.Locale.Tr "actions.runners.labels"}}{{ctx.Locale.Tr "actions.runners.task_list.status"}}{{ctx.Locale.Tr "created"}}
+ {{if and .Run .Repo}} + + {{.Run.Index}}#{{.ID}} + + {{else}} + {{.ID}} + {{end}} + + {{if .Repo}} + {{.Repo.FullName}} + {{else}} + - + {{end}} + {{.Name}} + {{range .RunsOn}} + {{.}} + {{end}} + + + {{.Status.LocaleString ctx.Locale}} + + {{DateUtils.TimeSince .Created}}
+ {{else}} +
+
+ {{svg "octicon-check-circle" 48}} +
+ {{ctx.Locale.Tr "actions.runners.no_waiting_jobs"}} +
+
+
+ {{end}} +
+
+{{template "admin/layout_footer" .}} diff --git a/templates/shared/actions/runner_list.tmpl b/templates/shared/actions/runner_list.tmpl index 7b47cabe4b..c05c19630e 100644 --- a/templates/shared/actions/runner_list.tmpl +++ b/templates/shared/actions/runner_list.tmpl @@ -134,8 +134,9 @@ for (const item of data) { const labelClass = item.stuck_jobs > 0 ? 'red' : (item.job_count > 5 ? 'yellow' : 'blue'); let waitInfo = item.oldest_wait ? ' (oldest: ' + item.oldest_wait + ')' : ''; - html += '' + - item.label + ': ' + item.job_count + (item.oldest_wait ? ' (' + item.oldest_wait + ')' : '') + ''; + const queueUrl = '{{$.Link}}/queue?label=' + encodeURIComponent(item.label); + html += '' + + item.label + ': ' + item.job_count + (item.oldest_wait ? ' (' + item.oldest_wait + ')' : '') + ''; if (item.stuck_jobs > 0) { hasStuck = true;