Go · 1419 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 // Package runnerlabels normalizes Actions runner labels for registration and
4 // heartbeat matching.
5 package runnerlabels
6
7 import (
8 "errors"
9 "fmt"
10 "regexp"
11 "strings"
12 )
13
14 var labelRE = regexp.MustCompile(`^[A-Za-z0-9_.-]+$`)
15
16 var defaultSharedLabels = []string{"self-hosted", "linux", "ubuntu-latest", "x64"}
17
18 // DefaultShared returns the labels for the built-in shared Linux runner pool.
19 func DefaultShared() []string {
20 return append([]string(nil), defaultSharedLabels...)
21 }
22
23 // ParseCSV parses a comma-separated label list.
24 func ParseCSV(raw string) ([]string, error) {
25 raw = strings.TrimSpace(raw)
26 if raw == "" {
27 return []string{}, nil
28 }
29 return Normalize(strings.Split(raw, ","))
30 }
31
32 // Normalize trims, validates, and de-duplicates labels while preserving order.
33 func Normalize(labels []string) ([]string, error) {
34 if len(labels) == 0 {
35 return []string{}, nil
36 }
37 seen := make(map[string]struct{}, len(labels))
38 out := make([]string, 0, len(labels))
39 for _, label := range labels {
40 label = strings.TrimSpace(label)
41 if label == "" {
42 return nil, errors.New("runner labels must not contain empty entries")
43 }
44 if len(label) > 100 || !labelRE.MatchString(label) {
45 return nil, fmt.Errorf("invalid runner label %q", label)
46 }
47 if _, ok := seen[label]; ok {
48 continue
49 }
50 seen[label] = struct{}{}
51 out = append(out, label)
52 }
53 return out, nil
54 }
55