Add workflow name and target labels
This commit is contained in:
		
							parent
							
								
									0a0be027fd
								
							
						
					
					
						commit
						5585eaf8a8
					
				| 
						 | 
				
			
			@ -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