From 868dfc6812a22b970ed89866aff99a77714bd6b9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 May 2026 01:03:21 +0000 Subject: [PATCH 1/4] Initial plan From 38fc7243e1adda7d310f55300e67cf5298815b99 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 May 2026 01:09:05 +0000 Subject: [PATCH 2/4] Initial plan Agent-Logs-Url: https://github.com/github/gh-aw/sessions/df78e305-93b0-4605-b9bc-451845689bfc Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- .github/workflows/metrics-collector.lock.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/metrics-collector.lock.yml b/.github/workflows/metrics-collector.lock.yml index 73a229a1f72..f1713c33de1 100644 --- a/.github/workflows/metrics-collector.lock.yml +++ b/.github/workflows/metrics-collector.lock.yml @@ -1196,6 +1196,7 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Metrics Collector - Infrastructure Agent" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/metrics-collector.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.40" - name: Download agent output artifact id: download-agent-output continue-on-error: true From 2226d7e284bb7f60235061bb143ca84631824af4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 May 2026 01:21:45 +0000 Subject: [PATCH 3/4] refactor: eliminate extractStringSlice duplicate, move formatList/normalizeLeadingWhitespace to pkg/stringutil Agent-Logs-Url: https://github.com/github/gh-aw/sessions/df78e305-93b0-4605-b9bc-451845689bfc Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- .../src/content/docs/agent-factory-status.mdx | 3 + .../docs/reference/frontmatter-full.md | 43 +++++++++- pkg/stringutil/stringutil.go | 71 +++++++++++++++ pkg/stringutil/stringutil_test.go | 86 +++++++++++++++++++ pkg/workflow/compiler_experiments.go | 21 +---- pkg/workflow/strings.go | 14 --- .../tools_validation_github_toolsets.go | 5 +- pkg/workflow/unified_prompt_step.go | 49 +---------- pkg/workflow/unified_prompt_step_test.go | 3 +- 9 files changed, 208 insertions(+), 87 deletions(-) diff --git a/docs/src/content/docs/agent-factory-status.mdx b/docs/src/content/docs/agent-factory-status.mdx index 8759f511dfe..d82cb6e1bd9 100644 --- a/docs/src/content/docs/agent-factory-status.mdx +++ b/docs/src/content/docs/agent-factory-status.mdx @@ -70,6 +70,7 @@ These are experimental agentic workflows used by the GitHub Next team to learn, | [Daily File Diet](https://github.com/github/gh-aw/blob/main/.github/workflows/daily-file-diet.md) | copilot | [![Daily File Diet](https://github.com/github/gh-aw/actions/workflows/daily-file-diet.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/daily-file-diet.lock.yml) | `daily around 13:00 on weekdays` | - | | [Daily Firewall Logs Collector and Reporter](https://github.com/github/gh-aw/blob/main/.github/workflows/daily-firewall-report.md) | copilot | [![Daily Firewall Logs Collector and Reporter](https://github.com/github/gh-aw/actions/workflows/daily-firewall-report.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/daily-firewall-report.lock.yml) | - | - | | [Daily Go Function Namer](https://github.com/github/gh-aw/blob/main/.github/workflows/daily-function-namer.md) | claude | [![Daily Go Function Namer](https://github.com/github/gh-aw/actions/workflows/daily-function-namer.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/daily-function-namer.lock.yml) | - | - | +| [Daily Grafana OTel Instrumentation Advisor](https://github.com/github/gh-aw/blob/main/.github/workflows/daily-grafana-otel-instrumentation-advisor.md) | claude | [![Daily Grafana OTel Instrumentation Advisor](https://github.com/github/gh-aw/actions/workflows/daily-grafana-otel-instrumentation-advisor.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/daily-grafana-otel-instrumentation-advisor.lock.yml) | - | - | | [Daily Hippo Learn](https://github.com/github/gh-aw/blob/main/.github/workflows/daily-hippo-learn.md) | copilot | [![Daily Hippo Learn](https://github.com/github/gh-aw/actions/workflows/daily-hippo-learn.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/daily-hippo-learn.lock.yml) | `daily around 7:00` | - | | [Daily Issues Report Generator](https://github.com/github/gh-aw/blob/main/.github/workflows/daily-issues-report.md) | copilot | [![Daily Issues Report Generator](https://github.com/github/gh-aw/actions/workflows/daily-issues-report.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/daily-issues-report.lock.yml) | - | - | | [Daily Malicious Code Scan Agent](https://github.com/github/gh-aw/blob/main/.github/workflows/daily-malicious-code-scan.md) | copilot | [![Daily Malicious Code Scan Agent](https://github.com/github/gh-aw/actions/workflows/daily-malicious-code-scan.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/daily-malicious-code-scan.lock.yml) | - | - | @@ -118,6 +119,7 @@ These are experimental agentic workflows used by the GitHub Next team to learn, | [Example: Properly Provisioned Permissions](https://github.com/github/gh-aw/blob/main/.github/workflows/example-permissions-warning.md) | copilot | [![Example: Properly Provisioned Permissions](https://github.com/github/gh-aw/actions/workflows/example-permissions-warning.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/example-permissions-warning.lock.yml) | - | - | | [Firewall Test Agent](https://github.com/github/gh-aw/blob/main/.github/workflows/firewall.md) | copilot | [![Firewall Test Agent](https://github.com/github/gh-aw/actions/workflows/firewall.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/firewall.lock.yml) | - | - | | [Functional Pragmatist](https://github.com/github/gh-aw/blob/main/.github/workflows/functional-pragmatist.md) | copilot | [![Functional Pragmatist](https://github.com/github/gh-aw/actions/workflows/functional-pragmatist.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/functional-pragmatist.lock.yml) | `25 9 * * 2,4` | - | +| [GEO Optimizer Daily Audit](https://github.com/github/gh-aw/blob/main/.github/workflows/daily-geo-optimizer.md) | copilot | [![GEO Optimizer Daily Audit](https://github.com/github/gh-aw/actions/workflows/daily-geo-optimizer.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/daily-geo-optimizer.lock.yml) | - | - | | [GitHub API Consumption Report Agent](https://github.com/github/gh-aw/blob/main/.github/workflows/api-consumption-report.md) | claude | [![GitHub API Consumption Report Agent](https://github.com/github/gh-aw/actions/workflows/api-consumption-report.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/api-consumption-report.lock.yml) | - | - | | [GitHub MCP Remote Server Tools Report Generator](https://github.com/github/gh-aw/blob/main/.github/workflows/github-mcp-tools-report.md) | claude | [![GitHub MCP Remote Server Tools Report Generator](https://github.com/github/gh-aw/actions/workflows/github-mcp-tools-report.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/github-mcp-tools-report.lock.yml) | - | - | | [GitHub MCP Structural Analysis](https://github.com/github/gh-aw/blob/main/.github/workflows/github-mcp-structural-analysis.md) | claude | [![GitHub MCP Structural Analysis](https://github.com/github/gh-aw/actions/workflows/github-mcp-structural-analysis.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/github-mcp-structural-analysis.lock.yml) | `daily around 11:00 on weekdays` | - | @@ -137,6 +139,7 @@ These are experimental agentic workflows used by the GitHub Next team to learn, | [jsweep - JavaScript Unbloater](https://github.com/github/gh-aw/blob/main/.github/workflows/jsweep.md) | copilot | [![jsweep - JavaScript Unbloater](https://github.com/github/gh-aw/actions/workflows/jsweep.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/jsweep.lock.yml) | - | - | | [Layout Specification Maintainer](https://github.com/github/gh-aw/blob/main/.github/workflows/layout-spec-maintainer.md) | copilot | [![Layout Specification Maintainer](https://github.com/github/gh-aw/actions/workflows/layout-spec-maintainer.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/layout-spec-maintainer.lock.yml) | `weekly on monday around 7:00` | - | | [Lockfile Statistics Analysis Agent](https://github.com/github/gh-aw/blob/main/.github/workflows/lockfile-stats.md) | claude | [![Lockfile Statistics Analysis Agent](https://github.com/github/gh-aw/actions/workflows/lockfile-stats.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/lockfile-stats.lock.yml) | - | - | +| [Matt Pocock Skills Reviewer](https://github.com/github/gh-aw/blob/main/.github/workflows/mattpocock-skills-reviewer.md) | copilot | [![Matt Pocock Skills Reviewer](https://github.com/github/gh-aw/actions/workflows/mattpocock-skills-reviewer.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/mattpocock-skills-reviewer.lock.yml) | - | - | | [MCP Inspector Agent](https://github.com/github/gh-aw/blob/main/.github/workflows/mcp-inspector.md) | copilot | [![MCP Inspector Agent](https://github.com/github/gh-aw/actions/workflows/mcp-inspector.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/mcp-inspector.lock.yml) | - | - | | [Mergefest](https://github.com/github/gh-aw/blob/main/.github/workflows/mergefest.md) | copilot | [![Mergefest](https://github.com/github/gh-aw/actions/workflows/mergefest.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/mergefest.lock.yml) | - | `/mergefest` | | [Metrics Collector - Infrastructure Agent](https://github.com/github/gh-aw/blob/main/.github/workflows/metrics-collector.md) | copilot | [![Metrics Collector - Infrastructure Agent](https://github.com/github/gh-aw/actions/workflows/metrics-collector.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/metrics-collector.lock.yml) | - | - | diff --git a/docs/src/content/docs/reference/frontmatter-full.md b/docs/src/content/docs/reference/frontmatter-full.md index df7efd4f26d..43e0a1f6440 100644 --- a/docs/src/content/docs/reference/frontmatter-full.md +++ b/docs/src/content/docs/reference/frontmatter-full.md @@ -6028,13 +6028,48 @@ observability: # OTLP (OpenTelemetry Protocol) trace export configuration. # (optional) otlp: - # OTLP collector endpoint URL (e.g. 'https://traces.example.com:4317'). Supports - # GitHub Actions expressions such as ${{ secrets.OTLP_ENDPOINT }}. When a static - # URL is provided, its hostname is automatically added to the network firewall - # allowlist. + # OTLP endpoint configuration. Accepts a plain URL string (backward-compat), a + # single {url, headers} object, or an array of {url, headers} objects for + # multi-endpoint concurrent fan-out. Encoded as GH_AW_OTLP_ENDPOINTS (JSON array). # (optional) + # This field supports multiple formats (oneOf): + + # Option 1: OTLP collector endpoint URL (e.g. 'https://traces.example.com:4317'). + # Supports GitHub Actions expressions such as ${{ secrets.OTLP_ENDPOINT }}. When a + # static URL is provided, its hostname is automatically added to the network + # firewall allowlist. endpoint: "example-value" + # Option 2: A single OTLP endpoint with a URL and optional per-endpoint headers. + endpoint: + # OTLP collector endpoint URL (e.g. 'https://traces.example.com:4317'). Supports + # GitHub Actions expressions such as ${{ secrets.OTLP_ENDPOINT }}. When a static + # URL is provided, its hostname is automatically added to the network firewall + # allowlist. + url: "example-value" + + # (optional) + # This field supports multiple formats (oneOf): + + # Option 1: Map of HTTP header names to values. Values support GitHub Actions + # expressions such as ${{ secrets.TOKEN }}. + headers: + {} + + # Option 2: Deprecated: use the map form instead. Comma-separated list of + # key=value HTTP headers (e.g. 'Authorization=Bearer '). Supports GitHub + # Actions expressions such as ${{ secrets.OTLP_HEADERS }}. + headers: "example-value" + + # Option 3: Multiple OTLP collector endpoints to export traces to concurrently. + # Each entry has its own URL and optional per-endpoint headers. + endpoint: [] + # Array items: A single OTLP endpoint with a URL and optional per-endpoint + # headers. + + # HTTP headers for the backward-compat string endpoint form. Only used when + # endpoint is a plain string; object/array endpoint entries carry their own + # per-endpoint headers. # (optional) # This field supports multiple formats (oneOf): diff --git a/pkg/stringutil/stringutil.go b/pkg/stringutil/stringutil.go index 4dcfeb52280..29066cec5a6 100644 --- a/pkg/stringutil/stringutil.go +++ b/pkg/stringutil/stringutil.go @@ -58,6 +58,77 @@ func ParseVersionValue(version any) string { } } +// FormatList formats a slice of strings as a natural-language comma-separated list +// with an Oxford comma and "and" before the final item. +// +// Examples: +// +// FormatList([]string{}) // returns "" +// FormatList([]string{"a"}) // returns "a" +// FormatList([]string{"a", "b"}) // returns "a and b" +// FormatList([]string{"a", "b", "c"}) // returns "a, b, and c" +func FormatList(items []string) string { + switch len(items) { + case 0: + return "" + case 1: + return items[0] + case 2: + return items[0] + " and " + items[1] + default: + return strings.Join(items[:len(items)-1], ", ") + ", and " + items[len(items)-1] + } +} + +// NormalizeLeadingWhitespace removes consistent leading whitespace from all lines +// of a multi-line string. It finds the minimum indentation across all non-empty +// lines and strips that many leading spaces from every line. +// +// This is useful for cleaning up content generated with extra indentation, +// such as heredoc bodies. +func NormalizeLeadingWhitespace(content string) string { + lines := strings.Split(content, "\n") + if len(lines) == 0 { + return content + } + + // Find minimum leading whitespace (excluding empty lines) + minLeadingSpaces := -1 + for _, line := range lines { + if strings.TrimSpace(line) == "" { + continue // Skip empty lines + } + leadingSpaces := len(line) - len(strings.TrimLeft(line, " ")) + if minLeadingSpaces == -1 || leadingSpaces < minLeadingSpaces { + minLeadingSpaces = leadingSpaces + } + } + + // If no content or no leading spaces, return as-is + if minLeadingSpaces <= 0 { + return content + } + + // Remove the minimum leading whitespace from all lines + var result strings.Builder + for i, line := range lines { + if i > 0 { + result.WriteString("\n") + } + if strings.TrimSpace(line) == "" { + // Keep empty lines as empty + result.WriteString("") + } else if len(line) >= minLeadingSpaces { + // Remove leading whitespace + result.WriteString(line[minLeadingSpaces:]) + } else { + result.WriteString(line) + } + } + + return result.String() +} + // IsPositiveInteger checks if a string is a positive integer. // Returns true for strings like "1", "123", "999" but false for: // - Zero ("0") diff --git a/pkg/stringutil/stringutil_test.go b/pkg/stringutil/stringutil_test.go index ee577db98ac..2ca050871b8 100644 --- a/pkg/stringutil/stringutil_test.go +++ b/pkg/stringutil/stringutil_test.go @@ -414,6 +414,92 @@ func TestParseVersionValue(t *testing.T) { } } +func TestFormatList(t *testing.T) { + tests := []struct { + name string + items []string + expected string + }{ + { + name: "empty slice", + items: []string{}, + expected: "", + }, + { + name: "single item", + items: []string{"a"}, + expected: "a", + }, + { + name: "two items", + items: []string{"a", "b"}, + expected: "a and b", + }, + { + name: "three items", + items: []string{"a", "b", "c"}, + expected: "a, b, and c", + }, + { + name: "four items", + items: []string{"a", "b", "c", "d"}, + expected: "a, b, c, and d", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := FormatList(tt.items) + if result != tt.expected { + t.Errorf("FormatList(%v) = %q; want %q", tt.items, result, tt.expected) + } + }) + } +} + +func TestNormalizeLeadingWhitespace(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "removes consistent leading spaces", + input: " Line 1\n Line 2\n Line 3", + expected: "Line 1\nLine 2\nLine 3", + }, + { + name: "handles no leading spaces", + input: "Line 1\nLine 2", + expected: "Line 1\nLine 2", + }, + { + name: "preserves relative indentation", + input: " Line 1\n Indented Line 2\n Line 3", + expected: "Line 1\n Indented Line 2\nLine 3", + }, + { + name: "handles empty lines", + input: " Line 1\n\n Line 3", + expected: "Line 1\n\nLine 3", + }, + { + name: "empty string", + input: "", + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := NormalizeLeadingWhitespace(tt.input) + if result != tt.expected { + t.Errorf("NormalizeLeadingWhitespace(%q) = %q; want %q", tt.input, result, tt.expected) + } + }) + } +} + func TestIsPositiveInteger(t *testing.T) { tests := []struct { name string diff --git a/pkg/workflow/compiler_experiments.go b/pkg/workflow/compiler_experiments.go index da2665798e0..07aca1d0b8c 100644 --- a/pkg/workflow/compiler_experiments.go +++ b/pkg/workflow/compiler_experiments.go @@ -183,7 +183,7 @@ func extractOneExperimentConfig(name string, val any) *ExperimentConfig { cfg.Hypothesis = h } if smRaw, ok := v["secondary_metrics"]; ok { - cfg.SecondaryMetrics = extractStringSlice(smRaw) + cfg.SecondaryMetrics = parseStringSliceAny(smRaw, nil) } if gmRaw, ok := v["guardrail_metrics"]; ok { cfg.GuardrailMetrics = extractGuardrailMetrics(gmRaw) @@ -195,7 +195,7 @@ func extractOneExperimentConfig(name string, val any) *ExperimentConfig { cfg.AnalysisType = at } if tagsRaw, ok := v["tags"]; ok { - cfg.Tags = extractStringSlice(tagsRaw) + cfg.Tags = parseStringSliceAny(tagsRaw, nil) } if notifyRaw, ok := v["notify"]; ok { if notifyMap, ok := notifyRaw.(map[string]any); ok { @@ -250,23 +250,6 @@ func extractIntField(val any) (int, bool) { return 0, false } -// extractStringSlice converts a raw value to a []string, accepting []any of string values. -func extractStringSlice(raw any) []string { - switch v := raw.(type) { - case []string: - return v - case []any: - var result []string - for _, item := range v { - if s, ok := item.(string); ok { - result = append(result, s) - } - } - return result - } - return nil -} - // extractGuardrailMetrics converts a raw guardrail_metrics value into a []GuardrailMetric. // Each entry must be a map with "name" and "threshold" string fields. func extractGuardrailMetrics(raw any) []GuardrailMetric { diff --git a/pkg/workflow/strings.go b/pkg/workflow/strings.go index a1a30d40ec6..231df47694b 100644 --- a/pkg/workflow/strings.go +++ b/pkg/workflow/strings.go @@ -413,17 +413,3 @@ func SanitizeArtifactIdentifier(name string) string { } return result } - -// formatList formats a list of strings as a comma-separated list with natural language conjunction -func formatList(items []string) string { - if len(items) == 0 { - return "" - } - if len(items) == 1 { - return items[0] - } - if len(items) == 2 { - return items[0] + " and " + items[1] - } - return fmt.Sprintf("%s, and %s", formatList(items[:len(items)-1]), items[len(items)-1]) -} diff --git a/pkg/workflow/tools_validation_github_toolsets.go b/pkg/workflow/tools_validation_github_toolsets.go index f579d9f0705..22dc57011d1 100644 --- a/pkg/workflow/tools_validation_github_toolsets.go +++ b/pkg/workflow/tools_validation_github_toolsets.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/github/gh-aw/pkg/parser" + "github.com/github/gh-aw/pkg/stringutil" ) func validateGitHubToolsAgainstToolsetsImpl(allowedTools []string, enabledToolsets []string) error { @@ -74,7 +75,7 @@ func validateGitHubToolsAgainstToolsetsImpl(allowedTools []string, enabledToolse if len(unknownTools) > 0 { githubToolToToolsetLog.Printf("Found %d unknown tools", len(unknownTools)) var errMsg strings.Builder - errMsg.WriteString(fmt.Sprintf("Unknown GitHub tool(s): %s\n\n", formatList(unknownTools))) + errMsg.WriteString(fmt.Sprintf("Unknown GitHub tool(s): %s\n\n", stringutil.FormatList(unknownTools))) if len(suggestions) > 0 { errMsg.WriteString("Did you mean:\n") @@ -92,7 +93,7 @@ func validateGitHubToolsAgainstToolsetsImpl(allowedTools []string, enabledToolse sort.Strings(validTools) exampleCount := min(10, len(validTools)) - errMsg.WriteString(fmt.Sprintf("Valid GitHub tools include: %s\n\n", formatList(validTools[:exampleCount]))) + errMsg.WriteString(fmt.Sprintf("Valid GitHub tools include: %s\n\n", stringutil.FormatList(validTools[:exampleCount]))) errMsg.WriteString("See all tools: https://github.com/github/gh-aw/blob/main/pkg/workflow/data/github_tool_to_toolset.json") return errors.New(errMsg.String()) diff --git a/pkg/workflow/unified_prompt_step.go b/pkg/workflow/unified_prompt_step.go index c0a72b1bf7b..55a916c2bd3 100644 --- a/pkg/workflow/unified_prompt_step.go +++ b/pkg/workflow/unified_prompt_step.go @@ -25,51 +25,6 @@ type PromptSection struct { EnvVars map[string]string } -// normalizeLeadingWhitespace removes consistent leading whitespace from all lines -// This handles content that was generated with indentation for heredocs -func normalizeLeadingWhitespace(content string) string { - lines := strings.Split(content, "\n") - if len(lines) == 0 { - return content - } - - // Find minimum leading whitespace (excluding empty lines) - minLeadingSpaces := -1 - for _, line := range lines { - if strings.TrimSpace(line) == "" { - continue // Skip empty lines - } - leadingSpaces := len(line) - len(strings.TrimLeft(line, " ")) - if minLeadingSpaces == -1 || leadingSpaces < minLeadingSpaces { - minLeadingSpaces = leadingSpaces - } - } - - // If no content or no leading spaces, return as-is - if minLeadingSpaces <= 0 { - return content - } - - // Remove the minimum leading whitespace from all lines - var result strings.Builder - for i, line := range lines { - if i > 0 { - result.WriteString("\n") - } - if strings.TrimSpace(line) == "" { - // Keep empty lines as empty - result.WriteString("") - } else if len(line) >= minLeadingSpaces { - // Remove leading whitespace - result.WriteString(line[minLeadingSpaces:]) - } else { - result.WriteString(line) - } - } - - return result.String() -} - // removeConsecutiveEmptyLines removes consecutive empty lines, keeping only one func removeConsecutiveEmptyLines(content string) string { lines := strings.Split(content, "\n") @@ -440,7 +395,7 @@ func (c *Compiler) generateUnifiedPromptCreationStep(yaml *strings.Builder, buil } else { // Inline content inside conditional - open heredoc, write content, close yaml.WriteString(" cat << '" + delimiter + "'\n") - normalizedContent := normalizeLeadingWhitespace(section.Content) + normalizedContent := stringutil.NormalizeLeadingWhitespace(section.Content) cleanedContent := removeConsecutiveEmptyLines(normalizedContent) contentLines := strings.SplitSeq(cleanedContent, "\n") for line := range contentLines { @@ -480,7 +435,7 @@ func (c *Compiler) generateUnifiedPromptCreationStep(yaml *strings.Builder, buil } } // Write content directly to open heredoc - normalizedContent := normalizeLeadingWhitespace(section.Content) + normalizedContent := stringutil.NormalizeLeadingWhitespace(section.Content) cleanedContent := removeConsecutiveEmptyLines(normalizedContent) contentLines := strings.SplitSeq(cleanedContent, "\n") for line := range contentLines { diff --git a/pkg/workflow/unified_prompt_step_test.go b/pkg/workflow/unified_prompt_step_test.go index dea1b3e779c..519d6260a11 100644 --- a/pkg/workflow/unified_prompt_step_test.go +++ b/pkg/workflow/unified_prompt_step_test.go @@ -6,6 +6,7 @@ import ( "strings" "testing" + "github.com/github/gh-aw/pkg/stringutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -135,7 +136,7 @@ Line 3`, for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := normalizeLeadingWhitespace(tt.input) + result := stringutil.NormalizeLeadingWhitespace(tt.input) assert.Equal(t, tt.expected, result) }) } From 9a7a99e9b2f39f8cf9e458d3f91b1a41a5ab2b38 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 May 2026 02:24:03 +0000 Subject: [PATCH 4/4] fix: handle tab indentation in NormalizeLeadingWhitespace, revert unrelated doc/lockfile changes Agent-Logs-Url: https://github.com/github/gh-aw/sessions/27539f97-fb71-4d10-a2de-fb1a279cd0a9 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/metrics-collector.lock.yml | 1 - .../src/content/docs/agent-factory-status.mdx | 3 -- .../docs/reference/frontmatter-full.md | 43 ++----------------- pkg/stringutil/stringutil.go | 19 ++++---- pkg/stringutil/stringutil_test.go | 10 +++++ 5 files changed, 24 insertions(+), 52 deletions(-) diff --git a/.github/workflows/metrics-collector.lock.yml b/.github/workflows/metrics-collector.lock.yml index f1713c33de1..73a229a1f72 100644 --- a/.github/workflows/metrics-collector.lock.yml +++ b/.github/workflows/metrics-collector.lock.yml @@ -1196,7 +1196,6 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Metrics Collector - Infrastructure Agent" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/metrics-collector.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.40" - name: Download agent output artifact id: download-agent-output continue-on-error: true diff --git a/docs/src/content/docs/agent-factory-status.mdx b/docs/src/content/docs/agent-factory-status.mdx index d82cb6e1bd9..8759f511dfe 100644 --- a/docs/src/content/docs/agent-factory-status.mdx +++ b/docs/src/content/docs/agent-factory-status.mdx @@ -70,7 +70,6 @@ These are experimental agentic workflows used by the GitHub Next team to learn, | [Daily File Diet](https://github.com/github/gh-aw/blob/main/.github/workflows/daily-file-diet.md) | copilot | [![Daily File Diet](https://github.com/github/gh-aw/actions/workflows/daily-file-diet.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/daily-file-diet.lock.yml) | `daily around 13:00 on weekdays` | - | | [Daily Firewall Logs Collector and Reporter](https://github.com/github/gh-aw/blob/main/.github/workflows/daily-firewall-report.md) | copilot | [![Daily Firewall Logs Collector and Reporter](https://github.com/github/gh-aw/actions/workflows/daily-firewall-report.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/daily-firewall-report.lock.yml) | - | - | | [Daily Go Function Namer](https://github.com/github/gh-aw/blob/main/.github/workflows/daily-function-namer.md) | claude | [![Daily Go Function Namer](https://github.com/github/gh-aw/actions/workflows/daily-function-namer.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/daily-function-namer.lock.yml) | - | - | -| [Daily Grafana OTel Instrumentation Advisor](https://github.com/github/gh-aw/blob/main/.github/workflows/daily-grafana-otel-instrumentation-advisor.md) | claude | [![Daily Grafana OTel Instrumentation Advisor](https://github.com/github/gh-aw/actions/workflows/daily-grafana-otel-instrumentation-advisor.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/daily-grafana-otel-instrumentation-advisor.lock.yml) | - | - | | [Daily Hippo Learn](https://github.com/github/gh-aw/blob/main/.github/workflows/daily-hippo-learn.md) | copilot | [![Daily Hippo Learn](https://github.com/github/gh-aw/actions/workflows/daily-hippo-learn.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/daily-hippo-learn.lock.yml) | `daily around 7:00` | - | | [Daily Issues Report Generator](https://github.com/github/gh-aw/blob/main/.github/workflows/daily-issues-report.md) | copilot | [![Daily Issues Report Generator](https://github.com/github/gh-aw/actions/workflows/daily-issues-report.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/daily-issues-report.lock.yml) | - | - | | [Daily Malicious Code Scan Agent](https://github.com/github/gh-aw/blob/main/.github/workflows/daily-malicious-code-scan.md) | copilot | [![Daily Malicious Code Scan Agent](https://github.com/github/gh-aw/actions/workflows/daily-malicious-code-scan.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/daily-malicious-code-scan.lock.yml) | - | - | @@ -119,7 +118,6 @@ These are experimental agentic workflows used by the GitHub Next team to learn, | [Example: Properly Provisioned Permissions](https://github.com/github/gh-aw/blob/main/.github/workflows/example-permissions-warning.md) | copilot | [![Example: Properly Provisioned Permissions](https://github.com/github/gh-aw/actions/workflows/example-permissions-warning.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/example-permissions-warning.lock.yml) | - | - | | [Firewall Test Agent](https://github.com/github/gh-aw/blob/main/.github/workflows/firewall.md) | copilot | [![Firewall Test Agent](https://github.com/github/gh-aw/actions/workflows/firewall.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/firewall.lock.yml) | - | - | | [Functional Pragmatist](https://github.com/github/gh-aw/blob/main/.github/workflows/functional-pragmatist.md) | copilot | [![Functional Pragmatist](https://github.com/github/gh-aw/actions/workflows/functional-pragmatist.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/functional-pragmatist.lock.yml) | `25 9 * * 2,4` | - | -| [GEO Optimizer Daily Audit](https://github.com/github/gh-aw/blob/main/.github/workflows/daily-geo-optimizer.md) | copilot | [![GEO Optimizer Daily Audit](https://github.com/github/gh-aw/actions/workflows/daily-geo-optimizer.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/daily-geo-optimizer.lock.yml) | - | - | | [GitHub API Consumption Report Agent](https://github.com/github/gh-aw/blob/main/.github/workflows/api-consumption-report.md) | claude | [![GitHub API Consumption Report Agent](https://github.com/github/gh-aw/actions/workflows/api-consumption-report.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/api-consumption-report.lock.yml) | - | - | | [GitHub MCP Remote Server Tools Report Generator](https://github.com/github/gh-aw/blob/main/.github/workflows/github-mcp-tools-report.md) | claude | [![GitHub MCP Remote Server Tools Report Generator](https://github.com/github/gh-aw/actions/workflows/github-mcp-tools-report.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/github-mcp-tools-report.lock.yml) | - | - | | [GitHub MCP Structural Analysis](https://github.com/github/gh-aw/blob/main/.github/workflows/github-mcp-structural-analysis.md) | claude | [![GitHub MCP Structural Analysis](https://github.com/github/gh-aw/actions/workflows/github-mcp-structural-analysis.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/github-mcp-structural-analysis.lock.yml) | `daily around 11:00 on weekdays` | - | @@ -139,7 +137,6 @@ These are experimental agentic workflows used by the GitHub Next team to learn, | [jsweep - JavaScript Unbloater](https://github.com/github/gh-aw/blob/main/.github/workflows/jsweep.md) | copilot | [![jsweep - JavaScript Unbloater](https://github.com/github/gh-aw/actions/workflows/jsweep.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/jsweep.lock.yml) | - | - | | [Layout Specification Maintainer](https://github.com/github/gh-aw/blob/main/.github/workflows/layout-spec-maintainer.md) | copilot | [![Layout Specification Maintainer](https://github.com/github/gh-aw/actions/workflows/layout-spec-maintainer.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/layout-spec-maintainer.lock.yml) | `weekly on monday around 7:00` | - | | [Lockfile Statistics Analysis Agent](https://github.com/github/gh-aw/blob/main/.github/workflows/lockfile-stats.md) | claude | [![Lockfile Statistics Analysis Agent](https://github.com/github/gh-aw/actions/workflows/lockfile-stats.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/lockfile-stats.lock.yml) | - | - | -| [Matt Pocock Skills Reviewer](https://github.com/github/gh-aw/blob/main/.github/workflows/mattpocock-skills-reviewer.md) | copilot | [![Matt Pocock Skills Reviewer](https://github.com/github/gh-aw/actions/workflows/mattpocock-skills-reviewer.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/mattpocock-skills-reviewer.lock.yml) | - | - | | [MCP Inspector Agent](https://github.com/github/gh-aw/blob/main/.github/workflows/mcp-inspector.md) | copilot | [![MCP Inspector Agent](https://github.com/github/gh-aw/actions/workflows/mcp-inspector.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/mcp-inspector.lock.yml) | - | - | | [Mergefest](https://github.com/github/gh-aw/blob/main/.github/workflows/mergefest.md) | copilot | [![Mergefest](https://github.com/github/gh-aw/actions/workflows/mergefest.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/mergefest.lock.yml) | - | `/mergefest` | | [Metrics Collector - Infrastructure Agent](https://github.com/github/gh-aw/blob/main/.github/workflows/metrics-collector.md) | copilot | [![Metrics Collector - Infrastructure Agent](https://github.com/github/gh-aw/actions/workflows/metrics-collector.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/metrics-collector.lock.yml) | - | - | diff --git a/docs/src/content/docs/reference/frontmatter-full.md b/docs/src/content/docs/reference/frontmatter-full.md index 43e0a1f6440..df7efd4f26d 100644 --- a/docs/src/content/docs/reference/frontmatter-full.md +++ b/docs/src/content/docs/reference/frontmatter-full.md @@ -6028,48 +6028,13 @@ observability: # OTLP (OpenTelemetry Protocol) trace export configuration. # (optional) otlp: - # OTLP endpoint configuration. Accepts a plain URL string (backward-compat), a - # single {url, headers} object, or an array of {url, headers} objects for - # multi-endpoint concurrent fan-out. Encoded as GH_AW_OTLP_ENDPOINTS (JSON array). + # OTLP collector endpoint URL (e.g. 'https://traces.example.com:4317'). Supports + # GitHub Actions expressions such as ${{ secrets.OTLP_ENDPOINT }}. When a static + # URL is provided, its hostname is automatically added to the network firewall + # allowlist. # (optional) - # This field supports multiple formats (oneOf): - - # Option 1: OTLP collector endpoint URL (e.g. 'https://traces.example.com:4317'). - # Supports GitHub Actions expressions such as ${{ secrets.OTLP_ENDPOINT }}. When a - # static URL is provided, its hostname is automatically added to the network - # firewall allowlist. endpoint: "example-value" - # Option 2: A single OTLP endpoint with a URL and optional per-endpoint headers. - endpoint: - # OTLP collector endpoint URL (e.g. 'https://traces.example.com:4317'). Supports - # GitHub Actions expressions such as ${{ secrets.OTLP_ENDPOINT }}. When a static - # URL is provided, its hostname is automatically added to the network firewall - # allowlist. - url: "example-value" - - # (optional) - # This field supports multiple formats (oneOf): - - # Option 1: Map of HTTP header names to values. Values support GitHub Actions - # expressions such as ${{ secrets.TOKEN }}. - headers: - {} - - # Option 2: Deprecated: use the map form instead. Comma-separated list of - # key=value HTTP headers (e.g. 'Authorization=Bearer '). Supports GitHub - # Actions expressions such as ${{ secrets.OTLP_HEADERS }}. - headers: "example-value" - - # Option 3: Multiple OTLP collector endpoints to export traces to concurrently. - # Each entry has its own URL and optional per-endpoint headers. - endpoint: [] - # Array items: A single OTLP endpoint with a URL and optional per-endpoint - # headers. - - # HTTP headers for the backward-compat string endpoint form. Only used when - # endpoint is a plain string; object/array endpoint entries carry their own - # per-endpoint headers. # (optional) # This field supports multiple formats (oneOf): diff --git a/pkg/stringutil/stringutil.go b/pkg/stringutil/stringutil.go index 29066cec5a6..4dae4c47c29 100644 --- a/pkg/stringutil/stringutil.go +++ b/pkg/stringutil/stringutil.go @@ -82,7 +82,8 @@ func FormatList(items []string) string { // NormalizeLeadingWhitespace removes consistent leading whitespace from all lines // of a multi-line string. It finds the minimum indentation across all non-empty -// lines and strips that many leading spaces from every line. +// lines and strips that many leading whitespace characters (spaces or tabs) from +// every line. // // This is useful for cleaning up content generated with extra indentation, // such as heredoc bodies. @@ -93,19 +94,19 @@ func NormalizeLeadingWhitespace(content string) string { } // Find minimum leading whitespace (excluding empty lines) - minLeadingSpaces := -1 + minLeading := -1 for _, line := range lines { if strings.TrimSpace(line) == "" { continue // Skip empty lines } - leadingSpaces := len(line) - len(strings.TrimLeft(line, " ")) - if minLeadingSpaces == -1 || leadingSpaces < minLeadingSpaces { - minLeadingSpaces = leadingSpaces + leading := len(line) - len(strings.TrimLeft(line, " \t")) + if minLeading == -1 || leading < minLeading { + minLeading = leading } } - // If no content or no leading spaces, return as-is - if minLeadingSpaces <= 0 { + // If no content or no leading whitespace, return as-is + if minLeading <= 0 { return content } @@ -118,9 +119,9 @@ func NormalizeLeadingWhitespace(content string) string { if strings.TrimSpace(line) == "" { // Keep empty lines as empty result.WriteString("") - } else if len(line) >= minLeadingSpaces { + } else if len(line) >= minLeading { // Remove leading whitespace - result.WriteString(line[minLeadingSpaces:]) + result.WriteString(line[minLeading:]) } else { result.WriteString(line) } diff --git a/pkg/stringutil/stringutil_test.go b/pkg/stringutil/stringutil_test.go index 2ca050871b8..df689330c90 100644 --- a/pkg/stringutil/stringutil_test.go +++ b/pkg/stringutil/stringutil_test.go @@ -488,6 +488,16 @@ func TestNormalizeLeadingWhitespace(t *testing.T) { input: "", expected: "", }, + { + name: "removes consistent leading tabs", + input: "\t\tLine 1\n\t\tLine 2\n\t\tLine 3", + expected: "Line 1\nLine 2\nLine 3", + }, + { + name: "removes consistent mixed tab and space indentation", + input: "\t Line 1\n\t Line 2\n\t Line 3", + expected: "Line 1\nLine 2\nLine 3", + }, } for _, tt := range tests {