Add workflow name and target labels (#4240)
This commit is contained in:
parent
088e2a3a90
commit
f731873df9
|
|
@ -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:
|
||||
# [
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue