2
0

feat(actions): add waiting jobs view filtered by runner label

Adds a new page to view all waiting/blocked jobs for a specific runner label. This helps administrators identify which jobs are queued for particular runner labels and diagnose runner capacity issues.
This commit is contained in:
2026-01-25 11:27:07 -05:00
parent d32d3e1360
commit 73ab59f158
6 changed files with 139 additions and 10 deletions

View File

@@ -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
}

View File

@@ -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",

View File

@@ -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)
}

View File

@@ -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)
})

View File

@@ -0,0 +1,72 @@
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin runners")}}
<div class="admin-setting-content">
<h4 class="ui top attached header">
{{ctx.Locale.Tr "actions.runners.waiting_jobs"}}
{{if .Label}}
<span class="ui blue label">{{.Label}}</span>
{{end}}
<a href="{{.Link}}" class="ui right">
<button class="ui tiny button">{{svg "octicon-arrow-left" 14}} {{ctx.Locale.Tr "actions.runners.back_to_runners"}}</button>
</a>
</h4>
<div class="ui attached segment">
{{if .Jobs}}
<table class="ui very basic striped table unstackable">
<thead>
<tr>
<th>{{ctx.Locale.Tr "actions.runners.task_list.run"}}</th>
<th>{{ctx.Locale.Tr "actions.runners.task_list.repository"}}</th>
<th>{{ctx.Locale.Tr "admin.packages.name"}}</th>
<th>{{ctx.Locale.Tr "actions.runners.labels"}}</th>
<th>{{ctx.Locale.Tr "actions.runners.task_list.status"}}</th>
<th>{{ctx.Locale.Tr "created"}}</th>
</tr>
</thead>
<tbody>
{{range .Jobs}}
<tr>
<td>
{{if and .Run .Repo}}
<a href="{{.Repo.Link}}/actions/runs/{{.Run.Index}}/jobs/{{.ID}}" target="_blank">
{{.Run.Index}}#{{.ID}}
</a>
{{else}}
{{.ID}}
{{end}}
</td>
<td>
{{if .Repo}}
<a href="{{.Repo.Link}}" target="_blank">{{.Repo.FullName}}</a>
{{else}}
-
{{end}}
</td>
<td>{{.Name}}</td>
<td>
{{range .RunsOn}}
<span class="ui small label">{{.}}</span>
{{end}}
</td>
<td>
<span class="ui label {{if eq .Status.String "waiting"}}yellow{{else if eq .Status.String "blocked"}}orange{{else}}grey{{end}}">
{{.Status.LocaleString ctx.Locale}}
</span>
</td>
<td>{{DateUtils.TimeSince .Created}}</td>
</tr>
{{end}}
</tbody>
</table>
{{else}}
<div class="ui placeholder segment">
<div class="ui icon header">
{{svg "octicon-check-circle" 48}}
<div class="content">
{{ctx.Locale.Tr "actions.runners.no_waiting_jobs"}}
</div>
</div>
</div>
{{end}}
</div>
</div>
{{template "admin/layout_footer" .}}

View File

@@ -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 += '<span class="ui ' + labelClass + ' label" title="' + item.job_count + ' jobs waiting' + waitInfo + '">' +
item.label + ': ' + item.job_count + (item.oldest_wait ? ' (' + item.oldest_wait + ')' : '') + '</span>';
const queueUrl = '{{$.Link}}/queue?label=' + encodeURIComponent(item.label);
html += '<a href="' + queueUrl + '" class="ui ' + labelClass + ' label" title="' + item.job_count + ' jobs waiting' + waitInfo + ' - Click to view">' +
item.label + ': ' + item.job_count + (item.oldest_wait ? ' (' + item.oldest_wait + ')' : '') + '</a>';
if (item.stuck_jobs > 0) {
hasStuck = true;