From 46be02bb7f741a48ee6b17c83bdcf7178fda6b9b Mon Sep 17 00:00:00 2001 From: GitCaddy Date: Sun, 11 Jan 2026 17:25:01 +0000 Subject: [PATCH] feat(runners): Add suggested labels and label management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add DistroInfo struct to parse Linux distribution from capabilities - Add runner label management endpoints (add/remove/use-suggested) - Update runner edit UI with: - Clickable labels with X to remove - Suggested labels with + to add individually - Use All Suggested Labels button - Buttons moved to full-width row below columns - Suggested labels derived from OS and distro (linux, linux-latest, debian, debian-latest, etc) 🤖 Generated with Claude Code --- modules/structs/actions_capabilities.go | 9 + options/locale/locale_en-US.json | 2 +- routers/web/shared/actions/runners.go | 172 ++++++++++++++ routers/web/web.go | 3 + templates/shared/actions/runner_edit.tmpl | 268 ++++++++++++++-------- 5 files changed, 351 insertions(+), 103 deletions(-) diff --git a/modules/structs/actions_capabilities.go b/modules/structs/actions_capabilities.go index 2e258047ba..ce579550e1 100644 --- a/modules/structs/actions_capabilities.go +++ b/modules/structs/actions_capabilities.go @@ -21,10 +21,18 @@ type DiskInfo struct { UsedPercent float64 `json:"used_percent"` } +// DistroInfo holds Linux distribution information +type DistroInfo struct { + ID string `json:"id,omitempty"` // e.g., "ubuntu", "debian", "fedora" + VersionID string `json:"version_id,omitempty"` // e.g., "24.04", "12" + PrettyName string `json:"pretty_name,omitempty"` // e.g., "Ubuntu 24.04 LTS" +} + // RunnerCapability represents the detailed capabilities of a runner type RunnerCapability struct { OS string `json:"os"` Arch string `json:"arch"` + Distro *DistroInfo `json:"distro,omitempty"` Docker bool `json:"docker"` DockerCompose bool `json:"docker_compose"` ContainerRuntime string `json:"container_runtime,omitempty"` @@ -34,6 +42,7 @@ type RunnerCapability struct { Limitations []string `json:"limitations,omitempty"` Disk *DiskInfo `json:"disk,omitempty"` Bandwidth *BandwidthInfo `json:"bandwidth,omitempty"` + SuggestedLabels []string `json:"suggested_labels,omitempty"` } // CapabilityFeatures represents feature support flags diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 839ebfba5c..f9c26b7327 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -3716,7 +3716,7 @@ "actions.runners.capabilities.bandwidth": "Network Bandwidth", "actions.runners.bandwidth_test_requested": "Bandwidth test requested. Results will appear on next poll.", "actions.runners.bandwidth_test_request_failed": "Failed to request bandwidth test.", - "actions.runners.check_bandwidth_now": "Check Now", + "actions.runners.check_bandwidth_now": "Check Bandwidth", "actions.runs.all_workflows": "All Workflows", "actions.runs.commit": "Commit", "actions.runs.scheduled": "Scheduled", diff --git a/routers/web/shared/actions/runners.go b/routers/web/shared/actions/runners.go index fa637ac5c8..0b3ad93ebe 100644 --- a/routers/web/shared/actions/runners.go +++ b/routers/web/shared/actions/runners.go @@ -5,6 +5,7 @@ package actions import ( "errors" + "strings" "net/http" "net/url" @@ -408,3 +409,174 @@ func findActionsRunner(ctx *context.Context, rCtx *runnersCtx) *actions_model.Ac return got[0] } +// RunnerAddLabel adds a single label to a runner +func RunnerAddLabel(ctx *context.Context) { + rCtx, err := getRunnersCtx(ctx) + if err != nil { + ctx.ServerError("getRunnersCtx", err) + return + } + + runner := findActionsRunner(ctx, rCtx) + if runner == nil { + return + } + + label := ctx.FormString("label") + if label == "" { + ctx.Flash.Warning("No label specified") + ctx.Redirect(rCtx.RedirectLink + ctx.PathParam("runnerid")) + return + } + + // Check if label already exists + for _, existing := range runner.AgentLabels { + if existing == label { + ctx.Flash.Info("Label already exists") + ctx.Redirect(rCtx.RedirectLink + ctx.PathParam("runnerid")) + return + } + } + + // Add the label + runner.AgentLabels = append(runner.AgentLabels, label) + + err = actions_model.UpdateRunner(ctx, runner, "agent_labels") + if err != nil { + log.Warn("RunnerAddLabel.UpdateRunner failed: %v", err) + ctx.Flash.Warning("Failed to add label") + ctx.Redirect(rCtx.RedirectLink + ctx.PathParam("runnerid")) + return + } + + ctx.Flash.Success("Label added: " + label) + ctx.Redirect(rCtx.RedirectLink + ctx.PathParam("runnerid")) +} + +// RunnerRemoveLabel removes a single label from a runner +func RunnerRemoveLabel(ctx *context.Context) { + rCtx, err := getRunnersCtx(ctx) + if err != nil { + ctx.ServerError("getRunnersCtx", err) + return + } + + runner := findActionsRunner(ctx, rCtx) + if runner == nil { + return + } + + label := ctx.FormString("label") + if label == "" { + ctx.Flash.Warning("No label specified") + ctx.Redirect(rCtx.RedirectLink + ctx.PathParam("runnerid")) + return + } + + // Remove the label + newLabels := make([]string, 0, len(runner.AgentLabels)) + found := false + for _, existing := range runner.AgentLabels { + if existing == label { + found = true + } else { + newLabels = append(newLabels, existing) + } + } + + if !found { + ctx.Flash.Info("Label not found") + ctx.Redirect(rCtx.RedirectLink + ctx.PathParam("runnerid")) + return + } + + runner.AgentLabels = newLabels + + err = actions_model.UpdateRunner(ctx, runner, "agent_labels") + if err != nil { + log.Warn("RunnerRemoveLabel.UpdateRunner failed: %v", err) + ctx.Flash.Warning("Failed to remove label") + ctx.Redirect(rCtx.RedirectLink + ctx.PathParam("runnerid")) + return + } + + ctx.Flash.Success("Label removed: " + label) + ctx.Redirect(rCtx.RedirectLink + ctx.PathParam("runnerid")) +} + +// RunnerUseSuggestedLabels adds all suggested labels based on capabilities +func RunnerUseSuggestedLabels(ctx *context.Context) { + rCtx, err := getRunnersCtx(ctx) + if err != nil { + ctx.ServerError("getRunnersCtx", err) + return + } + + runner := findActionsRunner(ctx, rCtx) + if runner == nil { + return + } + + // Parse capabilities to get suggested labels + if runner.CapabilitiesJSON == "" { + ctx.Flash.Warning("No capabilities data available") + ctx.Redirect(rCtx.RedirectLink + ctx.PathParam("runnerid")) + return + } + + var caps structs.RunnerCapability + if err := json.Unmarshal([]byte(runner.CapabilitiesJSON), &caps); err != nil { + ctx.Flash.Warning("Failed to parse capabilities") + ctx.Redirect(rCtx.RedirectLink + ctx.PathParam("runnerid")) + return + } + + // Build suggested labels + suggestedLabels := []string{} + existingSet := make(map[string]bool) + for _, label := range runner.AgentLabels { + existingSet[label] = true + } + + // OS-based labels + switch caps.OS { + case "linux": + suggestedLabels = append(suggestedLabels, "linux", "linux-latest") + case "windows": + suggestedLabels = append(suggestedLabels, "windows", "windows-latest") + case "darwin": + suggestedLabels = append(suggestedLabels, "macos", "macos-latest") + } + + // Distro-based labels + if caps.Distro != nil && caps.Distro.ID != "" { + suggestedLabels = append(suggestedLabels, caps.Distro.ID, caps.Distro.ID+"-latest") + } + + // Add only new labels + added := []string{} + for _, label := range suggestedLabels { + if !existingSet[label] { + runner.AgentLabels = append(runner.AgentLabels, label) + added = append(added, label) + existingSet[label] = true + } + } + + if len(added) == 0 { + ctx.Flash.Info("All suggested labels already exist") + ctx.Redirect(rCtx.RedirectLink + ctx.PathParam("runnerid")) + return + } + + err = actions_model.UpdateRunner(ctx, runner, "agent_labels") + if err != nil { + log.Warn("RunnerUseSuggestedLabels.UpdateRunner failed: %v", err) + ctx.Flash.Warning("Failed to add labels") + ctx.Redirect(rCtx.RedirectLink + ctx.PathParam("runnerid")) + return + } + + ctx.Flash.Success("Added labels: " + strings.Join(added, ", ")) + ctx.Redirect(rCtx.RedirectLink + ctx.PathParam("runnerid")) +} diff --git a/routers/web/web.go b/routers/web/web.go index 1cc2f6d2a4..5195810fc9 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -511,6 +511,9 @@ func registerWebRoutes(m *web.Router) { Post(web.Bind(forms.EditRunnerForm{}), shared_actions.RunnersEditPost) m.Post("/{runnerid}/delete", shared_actions.RunnerDeletePost) m.Post("/{runnerid}/bandwidth-test", shared_actions.RunnerRequestBandwidthTest) + m.Post("/{runnerid}/add-label", shared_actions.RunnerAddLabel) + m.Post("/{runnerid}/remove-label", shared_actions.RunnerRemoveLabel) + m.Post("/{runnerid}/use-suggested-labels", shared_actions.RunnerUseSuggestedLabels) m.Post("/reset_registration_token", shared_actions.ResetRunnerRegistrationToken) }) } diff --git a/templates/shared/actions/runner_edit.tmpl b/templates/shared/actions/runner_edit.tmpl index dabac76bf8..9412153b5f 100644 --- a/templates/shared/actions/runner_edit.tmpl +++ b/templates/shared/actions/runner_edit.tmpl @@ -3,47 +3,142 @@ {{ctx.Locale.Tr "actions.runners.runner_title"}} {{.Runner.ID}} {{.Runner.Name}}
-
- + +
+
+
Status
+ + {{if .Runner.IsOnline}}{{svg "octicon-check-circle" 16}}{{else}}{{svg "octicon-x-circle" 16}}{{end}} + {{.Runner.StatusLocaleName ctx.Locale}} + +
+ {{if .Runner.LastOnline}}Last seen {{DateUtils.TimeSince .Runner.LastOnline}}{{else}}Never connected{{end}} +
+
+
+
+
+
Disk Space
+ {{if and .RunnerCapabilities .RunnerCapabilities.Disk}} + {{$diskUsed := .RunnerCapabilities.Disk.UsedPercent}} + {{$diskFreeGB := DivideFloat64 (Int64ToFloat64 .RunnerCapabilities.Disk.Free) 1073741824.0}} + {{$diskTotalGB := DivideFloat64 (Int64ToFloat64 .RunnerCapabilities.Disk.Total) 1073741824.0}} + + {{if ge $diskUsed 95.0}}{{svg "octicon-alert" 16}}{{else if ge $diskUsed 85.0}}{{svg "octicon-alert" 16}}{{else}}{{svg "octicon-database" 16}}{{end}} + {{printf "%.0f" $diskUsed}}% used + +
+ {{printf "%.1f" $diskFreeGB}} GB free of {{printf "%.0f" $diskTotalGB}} GB +
+ {{else}} + {{svg "octicon-database" 16}} No data +
Waiting for report
+ {{end}} +
+
+
+
+
Network
+ {{if and .RunnerCapabilities .RunnerCapabilities.Bandwidth}} + + {{svg "octicon-arrow-down" 16}} {{printf "%.0f" .RunnerCapabilities.Bandwidth.DownloadMbps}} Mbps + +
+ {{if gt .RunnerCapabilities.Bandwidth.Latency 0.0}}{{printf "%.0f" .RunnerCapabilities.Bandwidth.Latency}} ms latency{{end}} + {{if .RunnerCapabilities.Bandwidth.TestedAt}}- tested {{DateUtils.TimeSince .RunnerCapabilities.Bandwidth.TestedAt}}{{end}} +
+ {{else}} + {{svg "octicon-globe" 16}} No data +
Waiting for test
+ {{end}} +
+
+
+ +
+ +
+
+
Runner Information
+ + + + + + + + + + + + + + + +
Version{{.Runner.Version}}
Owner{{.Runner.BelongsToOwnerType.LocaleString ctx.Locale}}
Labels + {{range .Runner.AgentLabels}} +
+ {{$.CsrfTokenHtml}} + + +
+ {{end}} + {{if not .Runner.AgentLabels}}No labels{{end}} +
+
+ + + {{if .RunnerCapabilities}} +
+
{{svg "octicon-light-bulb" 16}} Suggested Labels
+

Based on detected capabilities. Click + to add individually.

+
+ {{$labels := .Runner.AgentLabels}} + {{if eq .RunnerCapabilities.OS "linux"}} + {{if not (SliceUtils.Contains $labels "linux")}} +
{{$.CsrfTokenHtml}}
+ {{else}}linux{{end}} + {{if not (SliceUtils.Contains $labels "linux-latest")}} +
{{$.CsrfTokenHtml}}
+ {{else}}linux-latest{{end}} + {{else if eq .RunnerCapabilities.OS "windows"}} + {{if not (SliceUtils.Contains $labels "windows")}} +
{{$.CsrfTokenHtml}}
+ {{else}}windows{{end}} + {{if not (SliceUtils.Contains $labels "windows-latest")}} +
{{$.CsrfTokenHtml}}
+ {{else}}windows-latest{{end}} + {{else if eq .RunnerCapabilities.OS "darwin"}} + {{if not (SliceUtils.Contains $labels "macos")}} +
{{$.CsrfTokenHtml}}
+ {{else}}macos{{end}} + {{if not (SliceUtils.Contains $labels "macos-latest")}} +
{{$.CsrfTokenHtml}}
+ {{else}}macos-latest{{end}} + {{end}} + {{if and .RunnerCapabilities.Distro .RunnerCapabilities.Distro.ID}} + {{$distro := .RunnerCapabilities.Distro.ID}} + {{$distroLatest := printf "%s-latest" .RunnerCapabilities.Distro.ID}} + {{if not (SliceUtils.Contains $labels $distro)}} +
{{$.CsrfTokenHtml}}
+ {{else}}{{$distro}}{{end}} + {{if not (SliceUtils.Contains $labels $distroLatest)}} +
{{$.CsrfTokenHtml}}
+ {{else}}{{$distroLatest}}{{end}} + {{end}} +
+
+ {{end}} +
{{template "base/disable_form_autofill"}} -
+
+
AI Instructions
+

Additional context for AI when selecting this runner for jobs.

- - {{.Runner.StatusLocaleName ctx.Locale}} +
-
- - {{if .Runner.LastOnline}}{{DateUtils.TimeSince .Runner.LastOnline}}{{else}}{{ctx.Locale.Tr "never"}}{{end}} -
-
- - - {{range .Runner.AgentLabels}} - {{.}} - {{end}} - -
-
- - {{.Runner.BelongsToOwnerType.LocaleString ctx.Locale}} -
-
- -
- -
- - -
- -
- -
- -
@@ -57,16 +152,19 @@ {{if .RunnerCapabilities.OS}}
- {{.RunnerCapabilities.OS}}/{{.RunnerCapabilities.Arch}} + {{.RunnerCapabilities.OS}}/{{.RunnerCapabilities.Arch}} + {{if and .RunnerCapabilities.Distro .RunnerCapabilities.Distro.PrettyName}} + {{.RunnerCapabilities.Distro.PrettyName}} + {{end}}
{{end}}
{{if .RunnerCapabilities.Docker}} - {{svg "octicon-check" 14}} {{ctx.Locale.Tr "actions.runners.capabilities.available"}} + {{svg "octicon-check" 14}} Available {{else}} - {{svg "octicon-x" 14}} Not available + {{svg "octicon-x" 14}} Not available {{end}}
@@ -75,7 +173,7 @@
{{range .RunnerCapabilities.Shell}} - {{.}} + {{.}} {{end}}
@@ -86,72 +184,12 @@
{{range $tool, $versions := .RunnerCapabilities.Tools}} - {{$tool}} {{range $versions}}{{.}} {{end}} + {{$tool}} {{range $versions}}{{.}} {{end}} {{end}}
{{end}} - {{if .RunnerCapabilities.Disk}} -
- - {{$diskUsed := .RunnerCapabilities.Disk.UsedPercent}} - {{$diskFreeGB := DivideFloat64 (Int64ToFloat64 .RunnerCapabilities.Disk.Free) 1073741824.0}} - {{$diskTotalGB := DivideFloat64 (Int64ToFloat64 .RunnerCapabilities.Disk.Total) 1073741824.0}} - {{$diskUsedInt := printf "%.0f" $diskUsed}} -
-
-
{{printf "%.1f" $diskUsed}}%
-
-
-
- {{printf "%.1f" $diskFreeGB}} GB free / {{printf "%.1f" $diskTotalGB}} GB total - {{if ge $diskUsed 95.0}} - {{svg "octicon-alert" 14}} - {{else if ge $diskUsed 85.0}} - {{svg "octicon-alert" 14}} - {{end}} -
-
- {{end}} - -
- - {{if .RunnerCapabilities.Bandwidth}} -
- - {{svg "octicon-arrow-down" 14}} {{printf "%.1f" .RunnerCapabilities.Bandwidth.DownloadMbps}} Mbps - - {{if gt .RunnerCapabilities.Bandwidth.Latency 0.0}} - - {{svg "octicon-clock" 14}} {{printf "%.0f" .RunnerCapabilities.Bandwidth.Latency}} ms - - {{end}} -
- {{.CsrfTokenHtml}} - -
-
- {{if .RunnerCapabilities.Bandwidth.TestedAt}} -
- Tested {{DateUtils.TimeSince .RunnerCapabilities.Bandwidth.TestedAt}} -
- {{end}} - {{else}} -
- No data yet -
- {{.CsrfTokenHtml}} - -
-
- {{end}} -
- {{if .RunnerCapabilities.Limitations}}
@@ -169,11 +207,37 @@ {{else}}
{{ctx.Locale.Tr "actions.runners.capabilities"}}
-

No capabilities reported

+

No capabilities reported

{{end}}
+ + +
+ + {{if .RunnerCapabilities}} + + {{end}} + + +
+ + + +