Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 2 additions & 32 deletions pkg/cli/health_metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"time"

"github.com/github/gh-aw/pkg/logger"
"github.com/github/gh-aw/pkg/timeutil"
)

var healthMetricsLog = logger.New("cli:health_metrics")
Expand Down Expand Up @@ -120,7 +121,7 @@ func CalculateWorkflowHealth(workflowName string, runs []WorkflowRun, threshold

// Format display values
displayRate := fmt.Sprintf("%.0f%% (%d/%d)", successRate, successCount, totalRuns)
displayDur := formatDuration(avgDuration)
displayDur := timeutil.FormatDuration(avgDuration)
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The replacement of the local formatDuration with timeutil.FormatDuration changes the output format significantly. The original function formatted durations as "2m 30s" or "1h 15m" (integer values with multiple units), while timeutil.FormatDuration formats them as "2.5m" or "1.3h" (decimal values with a single unit). This will change the user-facing display format for health metrics. If this format change is intentional, it should be documented in the PR description. Otherwise, consider keeping the local formatDuration function or adding a new FormatDurationDetailed function to timeutil that preserves the multi-unit format.

Copilot uses AI. Check for mistakes.
displayTokens := formatTokens(avgTokens)
displayCost := formatCost(avgCost)

Expand Down Expand Up @@ -196,37 +197,6 @@ func calculateSuccessRate(runs []WorkflowRun) float64 {
return float64(successCount) / float64(len(runs)) * 100
}

// formatDuration formats a duration in a human-readable format
func formatDuration(d time.Duration) string {
if d == 0 {
return "0s"
}

// Round to seconds
seconds := int(d.Seconds())
if seconds < 60 {
return fmt.Sprintf("%ds", seconds)
}

minutes := seconds / 60
remainingSeconds := seconds % 60

if minutes < 60 {
if remainingSeconds > 0 {
return fmt.Sprintf("%dm %ds", minutes, remainingSeconds)
}
return fmt.Sprintf("%dm", minutes)
}

hours := minutes / 60
remainingMinutes := minutes % 60

if remainingMinutes > 0 {
return fmt.Sprintf("%dh %dm", hours, remainingMinutes)
}
return fmt.Sprintf("%dh", hours)
}

// CalculateHealthSummary calculates aggregated health metrics across all workflows
func CalculateHealthSummary(workflowHealths []WorkflowHealth, period string, threshold float64) HealthSummary {
healthMetricsLog.Printf("Calculating health summary: workflows=%d, period=%s", len(workflowHealths), period)
Expand Down
46 changes: 0 additions & 46 deletions pkg/cli/health_metrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,52 +157,6 @@ func TestCalculateTrend(t *testing.T) {
}
}

func TestFormatDuration(t *testing.T) {
tests := []struct {
name string
duration time.Duration
expected string
}{
{
name: "zero duration",
duration: 0,
expected: "0s",
},
{
name: "seconds only",
duration: 45 * time.Second,
expected: "45s",
},
{
name: "minutes only",
duration: 5 * time.Minute,
expected: "5m",
},
{
name: "minutes and seconds",
duration: 2*time.Minute + 30*time.Second,
expected: "2m 30s",
},
{
name: "hours only",
duration: 2 * time.Hour,
expected: "2h",
},
{
name: "hours and minutes",
duration: 1*time.Hour + 15*time.Minute,
expected: "1h 15m",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := formatDuration(tt.duration)
assert.Equal(t, tt.expected, result, "Formatted duration should match")
})
}
}

func TestGroupRunsByWorkflow(t *testing.T) {
runs := []WorkflowRun{
{WorkflowName: "workflow-a", Conclusion: "success"},
Expand Down
71 changes: 0 additions & 71 deletions pkg/parser/content_extractor.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,22 +54,6 @@ func extractToolsFromContent(content string) (string, error) {
return strings.TrimSpace(string(extractedJSON)), nil
}

// extractSafeOutputsFromContent extracts safe-outputs section from frontmatter as JSON string
func extractSafeOutputsFromContent(content string) (string, error) {
contentExtractorLog.Print("Extracting safe-outputs from content")
return extractFrontmatterField(content, "safe-outputs", "{}")
}

// extractSafeInputsFromContent extracts safe-inputs section from frontmatter as JSON string
func extractSafeInputsFromContent(content string) (string, error) {
return extractFrontmatterField(content, "safe-inputs", "{}")
}

// extractMCPServersFromContent extracts mcp-servers section from frontmatter as JSON string
func extractMCPServersFromContent(content string) (string, error) {
return extractFrontmatterField(content, "mcp-servers", "{}")
}

// extractStepsFromContent extracts steps section from frontmatter as YAML string
func extractStepsFromContent(content string) (string, error) {
result, err := ExtractFrontmatterFromContent(content)
Expand All @@ -92,16 +76,6 @@ func extractStepsFromContent(content string) (string, error) {
return strings.TrimSpace(string(stepsYAML)), nil
}

// extractEngineFromContent extracts engine section from frontmatter as JSON string
func extractEngineFromContent(content string) (string, error) {
return extractFrontmatterField(content, "engine", "")
}

// extractRuntimesFromContent extracts runtimes section from frontmatter as JSON string
func extractRuntimesFromContent(content string) (string, error) {
return extractFrontmatterField(content, "runtimes", "{}")
}

// extractServicesFromContent extracts services section from frontmatter as YAML string
func extractServicesFromContent(content string) (string, error) {
result, err := ExtractFrontmatterFromContent(content)
Expand All @@ -124,41 +98,11 @@ func extractServicesFromContent(content string) (string, error) {
return strings.TrimSpace(string(servicesYAML)), nil
}

// extractNetworkFromContent extracts network section from frontmatter as JSON string
func extractNetworkFromContent(content string) (string, error) {
return extractFrontmatterField(content, "network", "{}")
}

// ExtractPermissionsFromContent extracts permissions section from frontmatter as JSON string
func ExtractPermissionsFromContent(content string) (string, error) {
return extractFrontmatterField(content, "permissions", "{}")
}

// extractSecretMaskingFromContent extracts secret-masking section from frontmatter as JSON string
func extractSecretMaskingFromContent(content string) (string, error) {
return extractFrontmatterField(content, "secret-masking", "{}")
}

// extractBotsFromContent extracts bots section from frontmatter as JSON string
func extractBotsFromContent(content string) (string, error) {
return extractFrontmatterField(content, "bots", "[]")
}

// extractSkipRolesFromContent extracts skip-roles from on: section as JSON string
func extractSkipRolesFromContent(content string) (string, error) {
return extractOnSectionField(content, "skip-roles")
}

// extractSkipBotsFromContent extracts skip-bots from on: section as JSON string
func extractSkipBotsFromContent(content string) (string, error) {
return extractOnSectionField(content, "skip-bots")
}

// extractPluginsFromContent extracts plugins section from frontmatter as JSON string
func extractPluginsFromContent(content string) (string, error) {
return extractFrontmatterField(content, "plugins", "[]")
}

// extractPostStepsFromContent extracts post-steps section from frontmatter as YAML string
func extractPostStepsFromContent(content string) (string, error) {
result, err := ExtractFrontmatterFromContent(content)
Expand All @@ -181,21 +125,6 @@ func extractPostStepsFromContent(content string) (string, error) {
return strings.TrimSpace(string(postStepsYAML)), nil
}

// extractLabelsFromContent extracts labels section from frontmatter as JSON string
func extractLabelsFromContent(content string) (string, error) {
return extractFrontmatterField(content, "labels", "[]")
}

// extractCacheFromContent extracts cache section from frontmatter as JSON string
func extractCacheFromContent(content string) (string, error) {
return extractFrontmatterField(content, "cache", "{}")
}

// extractFeaturesFromContent extracts features section from frontmatter as JSON string
func extractFeaturesFromContent(content string) (string, error) {
return extractFrontmatterField(content, "features", "{}")
}

// extractFrontmatterField extracts a specific field from frontmatter as JSON string
func extractFrontmatterField(content, fieldName, emptyValue string) (string, error) {
contentExtractorLog.Printf("Extracting field: %s", fieldName)
Expand Down
22 changes: 11 additions & 11 deletions pkg/parser/content_extractor_features_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ features:

# Test Workflow
`
result, err := extractFeaturesFromContent(content)
result, err := extractFrontmatterField(content, "features", "{}")
require.NoError(t, err, "Should not error on valid features")
assert.NotEmpty(t, result, "Should return non-empty result")
assert.Contains(t, result, "feature1", "Should contain feature1")
Expand All @@ -35,7 +35,7 @@ on: issues

# Test Workflow
`
result, err := extractFeaturesFromContent(content)
result, err := extractFrontmatterField(content, "features", "{}")
require.NoError(t, err, "Should not error when no features")
assert.Equal(t, "{}", result, "Should return empty object when no features")
}
Expand All @@ -47,7 +47,7 @@ features: {}

# Test Workflow
`
result, err := extractFeaturesFromContent(content)
result, err := extractFrontmatterField(content, "features", "{}")
require.NoError(t, err, "Should not error on empty features")
assert.Equal(t, "{}", result, "Should return empty object for empty features")
}
Expand All @@ -57,7 +57,7 @@ func TestExtractFeaturesFromContent_NoFrontmatter(t *testing.T) {

This workflow has no frontmatter.
`
result, err := extractFeaturesFromContent(content)
result, err := extractFrontmatterField(content, "features", "{}")
require.NoError(t, err, "Should not error when no frontmatter")
assert.Equal(t, "{}", result, "Should return empty object when no frontmatter")
}
Expand All @@ -78,7 +78,7 @@ features:

# Test Workflow
`
result, err := extractFeaturesFromContent(content)
result, err := extractFrontmatterField(content, "features", "{}")
require.NoError(t, err, "Should not error on complex features")
assert.NotEmpty(t, result, "Should return non-empty result")
assert.Contains(t, result, "string-feature", "Should contain string-feature")
Expand All @@ -95,7 +95,7 @@ features: [this is not valid yaml

# Test Workflow
`
result, err := extractFeaturesFromContent(content)
result, err := extractFrontmatterField(content, "features", "{}")
// Should return empty object on error, not propagate error
require.NoError(t, err, "Should not error on malformed YAML")
assert.Equal(t, "{}", result, "Should return empty object on malformed YAML")
Expand All @@ -112,7 +112,7 @@ features:

# Test Workflow
`
result, err := extractFeaturesFromContent(content)
result, err := extractFrontmatterField(content, "features", "{}")
require.NoError(t, err, "Should not error on features with special characters")
assert.NotEmpty(t, result, "Should return non-empty result")
// The exact keys depend on YAML parsing, but we should get some features
Expand All @@ -127,7 +127,7 @@ features:

# Test Workflow
`
result, err := extractFeaturesFromContent(content)
result, err := extractFrontmatterField(content, "features", "{}")
require.NoError(t, err, "Should not error")
assert.NotEmpty(t, result, "Should return features")
assert.Contains(t, result, "test-feature", "Should contain test-feature")
Expand All @@ -146,7 +146,7 @@ permissions:

# Test Workflow
`
result, err := extractFeaturesFromContent(content)
result, err := extractFrontmatterField(content, "features", "{}")
require.NoError(t, err, "Should not error")
assert.NotEmpty(t, result, "Should return features")
assert.Contains(t, result, "test-feature", "Should contain test-feature")
Expand All @@ -169,7 +169,7 @@ features:

# Test Workflow
`
result, err := extractFeaturesFromContent(content)
result, err := extractFrontmatterField(content, "features", "{}")
require.NoError(t, err, "Should not error")
assert.NotEmpty(t, result, "Should return features")
// All these should be parsed as boolean features
Expand All @@ -188,7 +188,7 @@ features:

# Test Workflow
`
result, err := extractFeaturesFromContent(content)
result, err := extractFrontmatterField(content, "features", "{}")
require.NoError(t, err, "Should not error")
assert.NotEmpty(t, result, "Should return features")
assert.Contains(t, result, "int-feature", "Should contain int-feature")
Expand Down
2 changes: 1 addition & 1 deletion pkg/parser/engine_includes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ Just markdown content.

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := extractEngineFromContent(tt.content)
result, err := extractFrontmatterField(tt.content, "engine", "")
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
Expand Down
Loading
Loading