Skip to content

Commit f731873

Browse files
authored
Add workflow name and target labels (#4240)
1 parent 088e2a3 commit f731873

File tree

5 files changed

+277
-9
lines changed

5 files changed

+277
-9
lines changed

charts/gha-runner-scale-set/values.yaml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ githubConfigSecret:
154154
# counters:
155155
# gha_started_jobs_total:
156156
# labels:
157-
# ["repository", "organization", "enterprise", "job_name", "event_name", "job_workflow_ref"]
157+
# ["repository", "organization", "enterprise", "job_name", "event_name", "job_workflow_ref", "job_workflow_name", "job_workflow_target"]
158158
# gha_completed_jobs_total:
159159
# labels:
160160
# [
@@ -165,6 +165,8 @@ githubConfigSecret:
165165
# "event_name",
166166
# "job_result",
167167
# "job_workflow_ref",
168+
# "job_workflow_name",
169+
# "job_workflow_target",
168170
# ]
169171
# gauges:
170172
# gha_assigned_jobs:
@@ -186,7 +188,7 @@ githubConfigSecret:
186188
# histograms:
187189
# gha_job_startup_duration_seconds:
188190
# labels:
189-
# ["repository", "organization", "enterprise", "job_name", "event_name","job_workflow_ref"]
191+
# ["repository", "organization", "enterprise", "job_name", "event_name","job_workflow_ref", "job_workflow_name", "job_workflow_target"]
190192
# buckets:
191193
# [
192194
# 0.01,
@@ -244,7 +246,9 @@ githubConfigSecret:
244246
# "job_name",
245247
# "event_name",
246248
# "job_result",
247-
# "job_workflow_ref"
249+
# "job_workflow_ref",
250+
# "job_workflow_name",
251+
# "job_workflow_target"
248252
# ]
249253
# buckets:
250254
# [

cmd/ghalistener/metrics/metrics.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ const (
2222
labelKeyRepository = "repository"
2323
labelKeyJobName = "job_name"
2424
labelKeyJobWorkflowRef = "job_workflow_ref"
25+
labelKeyJobWorkflowName = "job_workflow_name"
26+
labelKeyJobWorkflowTarget = "job_workflow_target"
2527
labelKeyEventName = "event_name"
2628
labelKeyJobResult = "job_result"
2729
)
@@ -75,13 +77,16 @@ var metricsHelp = metricsHelpRegistry{
7577
}
7678

7779
func (e *exporter) jobLabels(jobBase *actions.JobMessageBase) prometheus.Labels {
80+
workflowRefInfo := ParseWorkflowRef(jobBase.JobWorkflowRef)
7881
return prometheus.Labels{
79-
labelKeyEnterprise: e.scaleSetLabels[labelKeyEnterprise],
80-
labelKeyOrganization: jobBase.OwnerName,
81-
labelKeyRepository: jobBase.RepositoryName,
82-
labelKeyJobName: jobBase.JobDisplayName,
83-
labelKeyJobWorkflowRef: jobBase.JobWorkflowRef,
84-
labelKeyEventName: jobBase.EventName,
82+
labelKeyEnterprise: e.scaleSetLabels[labelKeyEnterprise],
83+
labelKeyOrganization: jobBase.OwnerName,
84+
labelKeyRepository: jobBase.RepositoryName,
85+
labelKeyJobName: jobBase.JobDisplayName,
86+
labelKeyJobWorkflowRef: jobBase.JobWorkflowRef,
87+
labelKeyJobWorkflowName: workflowRefInfo.Name,
88+
labelKeyJobWorkflowTarget: workflowRefInfo.Target,
89+
labelKeyEventName: jobBase.EventName,
8590
}
8691
}
8792

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package metrics
2+
3+
import (
4+
"testing"
5+
6+
"github.com/actions/actions-runner-controller/github/actions"
7+
"github.com/prometheus/client_golang/prometheus"
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestMetricsWithWorkflowRefParsing(t *testing.T) {
12+
// Create a test exporter
13+
exporter := &exporter{
14+
scaleSetLabels: prometheus.Labels{
15+
labelKeyEnterprise: "test-enterprise",
16+
labelKeyOrganization: "test-org",
17+
labelKeyRepository: "test-repo",
18+
labelKeyRunnerScaleSetName: "test-scale-set",
19+
labelKeyRunnerScaleSetNamespace: "test-namespace",
20+
},
21+
}
22+
23+
tests := []struct {
24+
name string
25+
jobBase actions.JobMessageBase
26+
wantName string
27+
wantTarget string
28+
}{
29+
{
30+
name: "main branch workflow",
31+
jobBase: actions.JobMessageBase{
32+
OwnerName: "actions",
33+
RepositoryName: "runner",
34+
JobDisplayName: "Build and Test",
35+
JobWorkflowRef: "actions/runner/.github/workflows/build.yml@refs/heads/main",
36+
EventName: "push",
37+
},
38+
wantName: "build",
39+
wantTarget: "heads/main",
40+
},
41+
{
42+
name: "feature branch workflow",
43+
jobBase: actions.JobMessageBase{
44+
OwnerName: "myorg",
45+
RepositoryName: "myrepo",
46+
JobDisplayName: "CI/CD Pipeline",
47+
JobWorkflowRef: "myorg/myrepo/.github/workflows/ci-cd-pipeline.yml@refs/heads/feature/new-metrics",
48+
EventName: "push",
49+
},
50+
wantName: "ci-cd-pipeline",
51+
wantTarget: "heads/feature/new-metrics",
52+
},
53+
{
54+
name: "pull request workflow",
55+
jobBase: actions.JobMessageBase{
56+
OwnerName: "actions",
57+
RepositoryName: "runner",
58+
JobDisplayName: "PR Checks",
59+
JobWorkflowRef: "actions/runner/.github/workflows/pr-checks.yml@refs/pull/123/merge",
60+
EventName: "pull_request",
61+
},
62+
wantName: "pr-checks",
63+
wantTarget: "pull/123",
64+
},
65+
{
66+
name: "tag workflow",
67+
jobBase: actions.JobMessageBase{
68+
OwnerName: "actions",
69+
RepositoryName: "runner",
70+
JobDisplayName: "Release",
71+
JobWorkflowRef: "actions/runner/.github/workflows/release.yml@refs/tags/v1.2.3",
72+
EventName: "release",
73+
},
74+
wantName: "release",
75+
wantTarget: "tags/v1.2.3",
76+
},
77+
}
78+
79+
for _, tt := range tests {
80+
t.Run(tt.name, func(t *testing.T) {
81+
labels := exporter.jobLabels(&tt.jobBase)
82+
83+
// Build expected labels
84+
expectedLabels := prometheus.Labels{
85+
labelKeyEnterprise: "test-enterprise",
86+
labelKeyOrganization: tt.jobBase.OwnerName,
87+
labelKeyRepository: tt.jobBase.RepositoryName,
88+
labelKeyJobName: tt.jobBase.JobDisplayName,
89+
labelKeyJobWorkflowRef: tt.jobBase.JobWorkflowRef,
90+
labelKeyJobWorkflowName: tt.wantName,
91+
labelKeyJobWorkflowTarget: tt.wantTarget,
92+
labelKeyEventName: tt.jobBase.EventName,
93+
}
94+
95+
// Assert all expected labels match
96+
assert.Equal(t, expectedLabels, labels, "jobLabels() returned unexpected labels for %s", tt.name)
97+
})
98+
}
99+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package metrics
2+
3+
import (
4+
"path"
5+
"strings"
6+
)
7+
8+
// WorkflowRefInfo contains parsed information from a job_workflow_ref
9+
type WorkflowRefInfo struct {
10+
// Name is the workflow file name without extension
11+
Name string
12+
// Target is the target ref with type prefix retained for clarity
13+
// Examples:
14+
// - heads/main (branch)
15+
// - heads/feature/new-feature (branch)
16+
// - tags/v1.2.3 (tag)
17+
// - pull/123 (pull request)
18+
Target string
19+
}
20+
21+
// ParseWorkflowRef parses a job_workflow_ref string to extract workflow name and target
22+
// Format: {owner}/{repo}/.github/workflows/{workflow_file}@{ref}
23+
// Example: mygithuborg/myrepo/.github/workflows/blank.yml@refs/heads/main
24+
//
25+
// The target field preserves type prefixes to differentiate between:
26+
// - Branch references: "heads/{branch}" (from refs/heads/{branch})
27+
// - Tag references: "tags/{tag}" (from refs/tags/{tag})
28+
// - Pull requests: "pull/{number}" (from refs/pull/{number}/merge)
29+
func ParseWorkflowRef(workflowRef string) WorkflowRefInfo {
30+
info := WorkflowRefInfo{}
31+
32+
if workflowRef == "" {
33+
return info
34+
}
35+
36+
// Split by @ to separate path and ref
37+
parts := strings.Split(workflowRef, "@")
38+
if len(parts) != 2 {
39+
return info
40+
}
41+
42+
workflowPath := parts[0]
43+
ref := parts[1]
44+
45+
// Extract workflow name from path
46+
// The path format is: {owner}/{repo}/.github/workflows/{workflow_file}
47+
workflowFile := path.Base(workflowPath)
48+
// Remove .yml or .yaml extension
49+
info.Name = strings.TrimSuffix(strings.TrimSuffix(workflowFile, ".yml"), ".yaml")
50+
51+
// Extract target from ref based on type
52+
// Branch refs: refs/heads/{branch}
53+
// Tag refs: refs/tags/{tag}
54+
// PR refs: refs/pull/{number}/merge
55+
const (
56+
branchPrefix = "refs/heads/"
57+
tagPrefix = "refs/tags/"
58+
prPrefix = "refs/pull/"
59+
)
60+
61+
switch {
62+
case strings.HasPrefix(ref, branchPrefix):
63+
// Keep "heads/" prefix to indicate branch
64+
info.Target = "heads/" + strings.TrimPrefix(ref, branchPrefix)
65+
case strings.HasPrefix(ref, tagPrefix):
66+
// Keep "tags/" prefix to indicate tag
67+
info.Target = "tags/" + strings.TrimPrefix(ref, tagPrefix)
68+
case strings.HasPrefix(ref, prPrefix):
69+
// Extract PR number from refs/pull/{number}/merge
70+
// Keep "pull/" prefix to indicate pull request
71+
prPart := strings.TrimPrefix(ref, prPrefix)
72+
if idx := strings.Index(prPart, "/"); idx > 0 {
73+
info.Target = "pull/" + prPart[:idx]
74+
}
75+
}
76+
77+
return info
78+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package metrics
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestParseWorkflowRef(t *testing.T) {
10+
tests := []struct {
11+
name string
12+
workflowRef string
13+
wantName string
14+
wantTarget string
15+
}{
16+
{
17+
name: "standard branch reference with yml",
18+
workflowRef: "actions-runner-controller-sandbox/mumoshu-orgrunner-test-01/.github/workflows/blank.yml@refs/heads/main",
19+
wantName: "blank",
20+
wantTarget: "heads/main",
21+
},
22+
{
23+
name: "branch with special characters",
24+
workflowRef: "owner/repo/.github/workflows/ci-cd.yml@refs/heads/feature/new-feature",
25+
wantName: "ci-cd",
26+
wantTarget: "heads/feature/new-feature",
27+
},
28+
{
29+
name: "yaml extension",
30+
workflowRef: "owner/repo/.github/workflows/deploy.yaml@refs/heads/develop",
31+
wantName: "deploy",
32+
wantTarget: "heads/develop",
33+
},
34+
{
35+
name: "tag reference",
36+
workflowRef: "owner/repo/.github/workflows/release.yml@refs/tags/v1.0.0",
37+
wantName: "release",
38+
wantTarget: "tags/v1.0.0",
39+
},
40+
{
41+
name: "pull request reference",
42+
workflowRef: "owner/repo/.github/workflows/test.yml@refs/pull/123/merge",
43+
wantName: "test",
44+
wantTarget: "pull/123",
45+
},
46+
{
47+
name: "empty workflow ref",
48+
workflowRef: "",
49+
wantName: "",
50+
wantTarget: "",
51+
},
52+
{
53+
name: "invalid format - no @ separator",
54+
workflowRef: "owner/repo/.github/workflows/test.yml",
55+
wantName: "",
56+
wantTarget: "",
57+
},
58+
{
59+
name: "workflow with dots in name",
60+
workflowRef: "owner/repo/.github/workflows/build.test.yml@refs/heads/main",
61+
wantName: "build.test",
62+
wantTarget: "heads/main",
63+
},
64+
{
65+
name: "workflow with hyphen and underscore",
66+
workflowRef: "owner/repo/.github/workflows/build-test_deploy.yml@refs/heads/main",
67+
wantName: "build-test_deploy",
68+
wantTarget: "heads/main",
69+
},
70+
}
71+
72+
for _, tt := range tests {
73+
t.Run(tt.name, func(t *testing.T) {
74+
got := ParseWorkflowRef(tt.workflowRef)
75+
expected := WorkflowRefInfo{
76+
Name: tt.wantName,
77+
Target: tt.wantTarget,
78+
}
79+
assert.Equal(t, expected, got, "ParseWorkflowRef(%q) returned unexpected result", tt.workflowRef)
80+
})
81+
}
82+
}

0 commit comments

Comments
 (0)