Add workflow name and target labels (#4240)

This commit is contained in:
Yusuke Kuoka 2025-09-30 23:01:51 +09:00 committed by GitHub
parent 088e2a3a90
commit f731873df9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 277 additions and 9 deletions

View File

@ -154,7 +154,7 @@ githubConfigSecret:
# counters:
# gha_started_jobs_total:
# labels:
# ["repository", "organization", "enterprise", "job_name", "event_name", "job_workflow_ref"]
# ["repository", "organization", "enterprise", "job_name", "event_name", "job_workflow_ref", "job_workflow_name", "job_workflow_target"]
# gha_completed_jobs_total:
# labels:
# [
@ -165,6 +165,8 @@ githubConfigSecret:
# "event_name",
# "job_result",
# "job_workflow_ref",
# "job_workflow_name",
# "job_workflow_target",
# ]
# gauges:
# gha_assigned_jobs:
@ -186,7 +188,7 @@ githubConfigSecret:
# histograms:
# gha_job_startup_duration_seconds:
# labels:
# ["repository", "organization", "enterprise", "job_name", "event_name","job_workflow_ref"]
# ["repository", "organization", "enterprise", "job_name", "event_name","job_workflow_ref", "job_workflow_name", "job_workflow_target"]
# buckets:
# [
# 0.01,
@ -244,7 +246,9 @@ githubConfigSecret:
# "job_name",
# "event_name",
# "job_result",
# "job_workflow_ref"
# "job_workflow_ref",
# "job_workflow_name",
# "job_workflow_target"
# ]
# buckets:
# [

View File

@ -22,6 +22,8 @@ const (
labelKeyRepository = "repository"
labelKeyJobName = "job_name"
labelKeyJobWorkflowRef = "job_workflow_ref"
labelKeyJobWorkflowName = "job_workflow_name"
labelKeyJobWorkflowTarget = "job_workflow_target"
labelKeyEventName = "event_name"
labelKeyJobResult = "job_result"
)
@ -75,13 +77,16 @@ var metricsHelp = metricsHelpRegistry{
}
func (e *exporter) jobLabels(jobBase *actions.JobMessageBase) prometheus.Labels {
workflowRefInfo := ParseWorkflowRef(jobBase.JobWorkflowRef)
return prometheus.Labels{
labelKeyEnterprise: e.scaleSetLabels[labelKeyEnterprise],
labelKeyOrganization: jobBase.OwnerName,
labelKeyRepository: jobBase.RepositoryName,
labelKeyJobName: jobBase.JobDisplayName,
labelKeyJobWorkflowRef: jobBase.JobWorkflowRef,
labelKeyEventName: jobBase.EventName,
labelKeyEnterprise: e.scaleSetLabels[labelKeyEnterprise],
labelKeyOrganization: jobBase.OwnerName,
labelKeyRepository: jobBase.RepositoryName,
labelKeyJobName: jobBase.JobDisplayName,
labelKeyJobWorkflowRef: jobBase.JobWorkflowRef,
labelKeyJobWorkflowName: workflowRefInfo.Name,
labelKeyJobWorkflowTarget: workflowRefInfo.Target,
labelKeyEventName: jobBase.EventName,
}
}

View File

@ -0,0 +1,99 @@
package metrics
import (
"testing"
"github.com/actions/actions-runner-controller/github/actions"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/assert"
)
func TestMetricsWithWorkflowRefParsing(t *testing.T) {
// Create a test exporter
exporter := &exporter{
scaleSetLabels: prometheus.Labels{
labelKeyEnterprise: "test-enterprise",
labelKeyOrganization: "test-org",
labelKeyRepository: "test-repo",
labelKeyRunnerScaleSetName: "test-scale-set",
labelKeyRunnerScaleSetNamespace: "test-namespace",
},
}
tests := []struct {
name string
jobBase actions.JobMessageBase
wantName string
wantTarget string
}{
{
name: "main branch workflow",
jobBase: actions.JobMessageBase{
OwnerName: "actions",
RepositoryName: "runner",
JobDisplayName: "Build and Test",
JobWorkflowRef: "actions/runner/.github/workflows/build.yml@refs/heads/main",
EventName: "push",
},
wantName: "build",
wantTarget: "heads/main",
},
{
name: "feature branch workflow",
jobBase: actions.JobMessageBase{
OwnerName: "myorg",
RepositoryName: "myrepo",
JobDisplayName: "CI/CD Pipeline",
JobWorkflowRef: "myorg/myrepo/.github/workflows/ci-cd-pipeline.yml@refs/heads/feature/new-metrics",
EventName: "push",
},
wantName: "ci-cd-pipeline",
wantTarget: "heads/feature/new-metrics",
},
{
name: "pull request workflow",
jobBase: actions.JobMessageBase{
OwnerName: "actions",
RepositoryName: "runner",
JobDisplayName: "PR Checks",
JobWorkflowRef: "actions/runner/.github/workflows/pr-checks.yml@refs/pull/123/merge",
EventName: "pull_request",
},
wantName: "pr-checks",
wantTarget: "pull/123",
},
{
name: "tag workflow",
jobBase: actions.JobMessageBase{
OwnerName: "actions",
RepositoryName: "runner",
JobDisplayName: "Release",
JobWorkflowRef: "actions/runner/.github/workflows/release.yml@refs/tags/v1.2.3",
EventName: "release",
},
wantName: "release",
wantTarget: "tags/v1.2.3",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
labels := exporter.jobLabels(&tt.jobBase)
// Build expected labels
expectedLabels := prometheus.Labels{
labelKeyEnterprise: "test-enterprise",
labelKeyOrganization: tt.jobBase.OwnerName,
labelKeyRepository: tt.jobBase.RepositoryName,
labelKeyJobName: tt.jobBase.JobDisplayName,
labelKeyJobWorkflowRef: tt.jobBase.JobWorkflowRef,
labelKeyJobWorkflowName: tt.wantName,
labelKeyJobWorkflowTarget: tt.wantTarget,
labelKeyEventName: tt.jobBase.EventName,
}
// Assert all expected labels match
assert.Equal(t, expectedLabels, labels, "jobLabels() returned unexpected labels for %s", tt.name)
})
}
}

View File

@ -0,0 +1,78 @@
package metrics
import (
"path"
"strings"
)
// WorkflowRefInfo contains parsed information from a job_workflow_ref
type WorkflowRefInfo struct {
// Name is the workflow file name without extension
Name string
// Target is the target ref with type prefix retained for clarity
// Examples:
// - heads/main (branch)
// - heads/feature/new-feature (branch)
// - tags/v1.2.3 (tag)
// - pull/123 (pull request)
Target string
}
// ParseWorkflowRef parses a job_workflow_ref string to extract workflow name and target
// Format: {owner}/{repo}/.github/workflows/{workflow_file}@{ref}
// Example: mygithuborg/myrepo/.github/workflows/blank.yml@refs/heads/main
//
// The target field preserves type prefixes to differentiate between:
// - Branch references: "heads/{branch}" (from refs/heads/{branch})
// - Tag references: "tags/{tag}" (from refs/tags/{tag})
// - Pull requests: "pull/{number}" (from refs/pull/{number}/merge)
func ParseWorkflowRef(workflowRef string) WorkflowRefInfo {
info := WorkflowRefInfo{}
if workflowRef == "" {
return info
}
// Split by @ to separate path and ref
parts := strings.Split(workflowRef, "@")
if len(parts) != 2 {
return info
}
workflowPath := parts[0]
ref := parts[1]
// Extract workflow name from path
// The path format is: {owner}/{repo}/.github/workflows/{workflow_file}
workflowFile := path.Base(workflowPath)
// Remove .yml or .yaml extension
info.Name = strings.TrimSuffix(strings.TrimSuffix(workflowFile, ".yml"), ".yaml")
// Extract target from ref based on type
// Branch refs: refs/heads/{branch}
// Tag refs: refs/tags/{tag}
// PR refs: refs/pull/{number}/merge
const (
branchPrefix = "refs/heads/"
tagPrefix = "refs/tags/"
prPrefix = "refs/pull/"
)
switch {
case strings.HasPrefix(ref, branchPrefix):
// Keep "heads/" prefix to indicate branch
info.Target = "heads/" + strings.TrimPrefix(ref, branchPrefix)
case strings.HasPrefix(ref, tagPrefix):
// Keep "tags/" prefix to indicate tag
info.Target = "tags/" + strings.TrimPrefix(ref, tagPrefix)
case strings.HasPrefix(ref, prPrefix):
// Extract PR number from refs/pull/{number}/merge
// Keep "pull/" prefix to indicate pull request
prPart := strings.TrimPrefix(ref, prPrefix)
if idx := strings.Index(prPart, "/"); idx > 0 {
info.Target = "pull/" + prPart[:idx]
}
}
return info
}

View File

@ -0,0 +1,82 @@
package metrics
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseWorkflowRef(t *testing.T) {
tests := []struct {
name string
workflowRef string
wantName string
wantTarget string
}{
{
name: "standard branch reference with yml",
workflowRef: "actions-runner-controller-sandbox/mumoshu-orgrunner-test-01/.github/workflows/blank.yml@refs/heads/main",
wantName: "blank",
wantTarget: "heads/main",
},
{
name: "branch with special characters",
workflowRef: "owner/repo/.github/workflows/ci-cd.yml@refs/heads/feature/new-feature",
wantName: "ci-cd",
wantTarget: "heads/feature/new-feature",
},
{
name: "yaml extension",
workflowRef: "owner/repo/.github/workflows/deploy.yaml@refs/heads/develop",
wantName: "deploy",
wantTarget: "heads/develop",
},
{
name: "tag reference",
workflowRef: "owner/repo/.github/workflows/release.yml@refs/tags/v1.0.0",
wantName: "release",
wantTarget: "tags/v1.0.0",
},
{
name: "pull request reference",
workflowRef: "owner/repo/.github/workflows/test.yml@refs/pull/123/merge",
wantName: "test",
wantTarget: "pull/123",
},
{
name: "empty workflow ref",
workflowRef: "",
wantName: "",
wantTarget: "",
},
{
name: "invalid format - no @ separator",
workflowRef: "owner/repo/.github/workflows/test.yml",
wantName: "",
wantTarget: "",
},
{
name: "workflow with dots in name",
workflowRef: "owner/repo/.github/workflows/build.test.yml@refs/heads/main",
wantName: "build.test",
wantTarget: "heads/main",
},
{
name: "workflow with hyphen and underscore",
workflowRef: "owner/repo/.github/workflows/build-test_deploy.yml@refs/heads/main",
wantName: "build-test_deploy",
wantTarget: "heads/main",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ParseWorkflowRef(tt.workflowRef)
expected := WorkflowRefInfo{
Name: tt.wantName,
Target: tt.wantTarget,
}
assert.Equal(t, expected, got, "ParseWorkflowRef(%q) returned unexpected result", tt.workflowRef)
})
}
}