From 6e7dc3acaadb5c039efebfff7b2df5f6416a02dc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 May 2026 00:45:08 +0000 Subject: [PATCH 1/4] Initial plan From 68ca2f9a4e38c9276f5f627c1ab8a7bc33680d0f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 May 2026 00:50:13 +0000 Subject: [PATCH 2/4] Initial plan for multi-line engine.env fix Agent-Logs-Url: https://github.com/github/gh-aw/sessions/76ac73b1-b929-4138-a006-d8ebfdde7295 Co-authored-by: pelikhan <4175913+pelikhan@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 6f71e4873002c49d3472b96c20bcac76bb6208cd 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:36 +0000 Subject: [PATCH 3/4] fix: handle multi-line engine.env values as literal block scalars Add appendEnvVarLine helper that trims trailing newlines and emits multi-line values as YAML literal block scalars (|) instead of single-line key: value pairs which break YAML parsing. Update FormatStepWithCommandAndEnv and GenerateMultiSecretValidationStep to use the new helper. Fixes multi-line >- block scalar expressions in engine.env values producing invalid compiled .lock.yml files. Agent-Logs-Url: https://github.com/github/gh-aw/sessions/76ac73b1-b929-4138-a006-d8ebfdde7295 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../src/content/docs/agent-factory-status.mdx | 3 + .../docs/reference/frontmatter-full.md | 43 +++++- pkg/workflow/engine_helpers.go | 26 +++- pkg/workflow/engine_helpers_test.go | 126 ++++++++++++++++++ pkg/workflow/secret_validation_test.go | 29 ++++ 5 files changed, 221 insertions(+), 6 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/workflow/engine_helpers.go b/pkg/workflow/engine_helpers.go index a2a7c1e6884..24887af8c44 100644 --- a/pkg/workflow/engine_helpers.go +++ b/pkg/workflow/engine_helpers.go @@ -167,7 +167,7 @@ func GenerateMultiSecretValidationStep(secretNames []string, engineName, docsURL expr = override } } - stepLines = append(stepLines, fmt.Sprintf(" %s: %s", secretName, expr)) + stepLines = appendEnvVarLine(stepLines, secretName, expr) } return GitHubActionStep(stepLines) @@ -263,13 +263,35 @@ func FormatStepWithCommandAndEnv(stepLines []string, command string, env map[str for _, key := range envKeys { value := env[key] - stepLines = append(stepLines, fmt.Sprintf(" %s: %s", key, yamlStringValue(value))) + stepLines = appendEnvVarLine(stepLines, key, value) } } return stepLines } +// appendEnvVarLine appends a YAML env var entry to lines. +// If the value contains embedded newlines (e.g. from a multi-line YAML block scalar +// like >- with extra-indented continuation lines), it is emitted as a YAML literal +// block scalar (|) with proper indentation. A trailing newline produced by block +// scalars is trimmed before processing. +func appendEnvVarLine(lines []string, key, value string) []string { + // Trim trailing newline added by YAML | or > block scalars + value = strings.TrimRight(value, "\n") + + if !strings.Contains(value, "\n") { + // Single-line: emit inline with YAML-safe quoting + return append(lines, fmt.Sprintf(" %s: %s", key, yamlStringValue(value))) + } + + // Multi-line: emit as a literal block scalar so embedded newlines are preserved + lines = append(lines, fmt.Sprintf(" %s: |", key)) + for line := range strings.SplitSeq(value, "\n") { + lines = append(lines, " "+line) + } + return lines +} + // yamlStringValue returns a YAML-safe representation of a string value. // If the value starts with a YAML flow indicator ('{' or '[') or other characters // that would cause it to be misinterpreted by YAML parsers, it wraps the value diff --git a/pkg/workflow/engine_helpers_test.go b/pkg/workflow/engine_helpers_test.go index 032f10d4f1b..63e2929f43e 100644 --- a/pkg/workflow/engine_helpers_test.go +++ b/pkg/workflow/engine_helpers_test.go @@ -339,4 +339,130 @@ func TestFormatStepWithCommandAndEnvYAMLSafe(t *testing.T) { t.Errorf("Expected unquoted github expression in env, got:\n%s", output) } }) + + t.Run("multi-line env var emitted as literal block scalar", func(t *testing.T) { + stepLines := []string{" - name: Test step"} + multiLineValue := "${{ secrets.PAT_1 != '' && secrets.PAT_1 ||\n secrets.PAT_2 != '' && secrets.PAT_2 ||\n secrets.PAT_3 }}" + env := map[string]string{ + "COPILOT_GITHUB_TOKEN": multiLineValue, + } + result := FormatStepWithCommandAndEnv(stepLines, "echo test", env) + output := strings.Join(result, "\n") + + // Multi-line value must be emitted as a literal block scalar + if !strings.Contains(output, " COPILOT_GITHUB_TOKEN: |") { + t.Errorf("Expected literal block scalar indicator, got:\n%s", output) + } + if !strings.Contains(output, " ${{ secrets.PAT_1 != '' && secrets.PAT_1 ||") { + t.Errorf("Expected first line of multi-line value, got:\n%s", output) + } + if !strings.Contains(output, " secrets.PAT_3 }}") { + t.Errorf("Expected last line of multi-line value, got:\n%s", output) + } + }) + + t.Run("trailing newline in env var is trimmed", func(t *testing.T) { + stepLines := []string{" - name: Test step"} + env := map[string]string{ + "MY_TOKEN": "${{ secrets.TOKEN }}\n", + } + result := FormatStepWithCommandAndEnv(stepLines, "echo test", env) + output := strings.Join(result, "\n") + + // Trailing newline should be trimmed; value should be emitted inline + if !strings.Contains(output, "MY_TOKEN: ${{ secrets.TOKEN }}") { + t.Errorf("Expected trailing newline to be trimmed, got:\n%s", output) + } + // Should not emit a block scalar for a value that only had a trailing newline + if strings.Contains(output, "MY_TOKEN: |") { + t.Errorf("Expected inline emission (not block scalar) after trimming trailing newline, got:\n%s", output) + } + }) +} + +func TestAppendEnvVarLine(t *testing.T) { + tests := []struct { + name string + key string + value string + expectedContent []string + notExpected []string + }{ + { + name: "single-line value emitted inline", + key: "MY_VAR", + value: "simple value", + expectedContent: []string{ + " MY_VAR: simple value", + }, + }, + { + name: "github expression emitted inline without extra quoting", + key: "TOKEN", + value: "${{ secrets.TOKEN }}", + expectedContent: []string{ + " TOKEN: ${{ secrets.TOKEN }}", + }, + }, + { + name: "json value gets single-quoted inline", + key: "CONFIG", + value: `{"key":"value"}`, + expectedContent: []string{ + ` CONFIG: '{"key":"value"}'`, + }, + }, + { + name: "multi-line value emitted as literal block scalar", + key: "COPILOT_GITHUB_TOKEN", + value: "${{ secrets.PAT_1 != '' && secrets.PAT_1 ||\n secrets.PAT_2 }}", + expectedContent: []string{ + " COPILOT_GITHUB_TOKEN: |", + " ${{ secrets.PAT_1 != '' && secrets.PAT_1 ||", + " secrets.PAT_2 }}", + }, + notExpected: []string{ + "COPILOT_GITHUB_TOKEN: ${{ secrets.PAT_1", + }, + }, + { + name: "trailing newline is trimmed before deciding inline vs block", + key: "TRIMMED", + value: "${{ secrets.TOKEN }}\n", + expectedContent: []string{ + " TRIMMED: ${{ secrets.TOKEN }}", + }, + notExpected: []string{ + "TRIMMED: |", + }, + }, + { + name: "multi-line value with trailing newline trimmed", + key: "MULTI", + value: "line one\nline two\n", + expectedContent: []string{ + " MULTI: |", + " line one", + " line two", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := appendEnvVarLine([]string{}, tt.key, tt.value) + output := strings.Join(result, "\n") + + for _, expected := range tt.expectedContent { + if !strings.Contains(output, expected) { + t.Errorf("Expected result to contain %q\nGot:\n%s", expected, output) + } + } + for _, notExp := range tt.notExpected { + if strings.Contains(output, notExp) { + t.Errorf("Expected result NOT to contain %q\nGot:\n%s", notExp, output) + } + } + }) + } } diff --git a/pkg/workflow/secret_validation_test.go b/pkg/workflow/secret_validation_test.go index e04125b8bb4..0868f8fc1f0 100644 --- a/pkg/workflow/secret_validation_test.go +++ b/pkg/workflow/secret_validation_test.go @@ -250,6 +250,35 @@ func TestGenerateMultiSecretValidationStepWithEnvOverrides(t *testing.T) { t.Errorf("Expected default OPENAI_API_KEY expression (not overridden), got:\n%s", stepContent) } }) + + t.Run("multi-line override emitted as literal block scalar", func(t *testing.T) { + multiLineExpr := "${{ secrets.GH_AW_PAT_1 != '' && secrets.GH_AW_PAT_1 ||\n secrets.GH_AW_PAT_2 != '' && secrets.GH_AW_PAT_2 ||\n secrets.GH_AW_PAT_3 }}" + overrides := map[string]string{ + "COPILOT_GITHUB_TOKEN": multiLineExpr, + } + step := GenerateMultiSecretValidationStep( + []string{"COPILOT_GITHUB_TOKEN"}, + "GitHub Copilot CLI", + "https://docs.example.com", + overrides, + ) + stepContent := strings.Join(step, "\n") + + // Multi-line value must be emitted as a literal block scalar + if !strings.Contains(stepContent, " COPILOT_GITHUB_TOKEN: |") { + t.Errorf("Expected literal block scalar indicator, got:\n%s", stepContent) + } + if !strings.Contains(stepContent, " ${{ secrets.GH_AW_PAT_1 != '' && secrets.GH_AW_PAT_1 ||") { + t.Errorf("Expected first line of multi-line expression, got:\n%s", stepContent) + } + if !strings.Contains(stepContent, " secrets.GH_AW_PAT_3 }}") { + t.Errorf("Expected last line of multi-line expression, got:\n%s", stepContent) + } + // Should not emit the raw multi-line value inline + if strings.Contains(stepContent, "COPILOT_GITHUB_TOKEN: ${{ secrets.GH_AW_PAT_1") { + t.Errorf("Expected block scalar, not inline multi-line value, got:\n%s", stepContent) + } + }) } func TestValidationStepUsesEngineEnvOverride(t *testing.T) { From 1a913ca780f7a56d09284c3b9d529ff24a7cc96d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 May 2026 02:40:17 +0000 Subject: [PATCH 4/4] fix: trim at most one trailing newline, precise test assertions, revert unrelated changes - Use strings.TrimSuffix instead of strings.TrimRight to trim at most one trailing newline in appendEnvVarLine (preserves multiple intentional trailing newlines) - Fix test assertions to use correct 16-space indentation for continuation lines (12 base + 4 preserved prefix) - Add test case proving only one trailing newline is trimmed - Revert unrelated docs and lockfile changes Agent-Logs-Url: https://github.com/github/gh-aw/sessions/6cc3894b-66d5-41c8-8840-0eda6deb1dfe Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/metrics-collector.lock.yml | 1 - .../docs/reference/frontmatter-full.md | 43 ++----------------- pkg/workflow/engine_helpers.go | 11 +++-- pkg/workflow/engine_helpers_test.go | 28 +++++++++--- pkg/workflow/secret_validation_test.go | 7 ++- 5 files changed, 39 insertions(+), 51 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/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/workflow/engine_helpers.go b/pkg/workflow/engine_helpers.go index 24887af8c44..77880880456 100644 --- a/pkg/workflow/engine_helpers.go +++ b/pkg/workflow/engine_helpers.go @@ -273,11 +273,14 @@ func FormatStepWithCommandAndEnv(stepLines []string, command string, env map[str // appendEnvVarLine appends a YAML env var entry to lines. // If the value contains embedded newlines (e.g. from a multi-line YAML block scalar // like >- with extra-indented continuation lines), it is emitted as a YAML literal -// block scalar (|) with proper indentation. A trailing newline produced by block -// scalars is trimmed before processing. +// block scalar (|) with proper indentation. At most one trailing newline (produced +// by block scalars) is trimmed before processing; multiple intentional trailing +// newlines are preserved. func appendEnvVarLine(lines []string, key, value string) []string { - // Trim trailing newline added by YAML | or > block scalars - value = strings.TrimRight(value, "\n") + // Trim at most one trailing newline added by YAML | or > block scalars. + // Using TrimSuffix (not TrimRight) to avoid stripping multiple trailing + // newlines that may be intentional in the value. + value = strings.TrimSuffix(value, "\n") if !strings.Contains(value, "\n") { // Single-line: emit inline with YAML-safe quoting diff --git a/pkg/workflow/engine_helpers_test.go b/pkg/workflow/engine_helpers_test.go index 63e2929f43e..4eea81c8dcc 100644 --- a/pkg/workflow/engine_helpers_test.go +++ b/pkg/workflow/engine_helpers_test.go @@ -342,6 +342,8 @@ func TestFormatStepWithCommandAndEnvYAMLSafe(t *testing.T) { t.Run("multi-line env var emitted as literal block scalar", func(t *testing.T) { stepLines := []string{" - name: Test step"} + // Continuation lines have 4-space leading whitespace (as produced by goccy/go-yaml + // when parsing a >- block scalar with extra-indented continuation lines). multiLineValue := "${{ secrets.PAT_1 != '' && secrets.PAT_1 ||\n secrets.PAT_2 != '' && secrets.PAT_2 ||\n secrets.PAT_3 }}" env := map[string]string{ "COPILOT_GITHUB_TOKEN": multiLineValue, @@ -356,8 +358,9 @@ func TestFormatStepWithCommandAndEnvYAMLSafe(t *testing.T) { if !strings.Contains(output, " ${{ secrets.PAT_1 != '' && secrets.PAT_1 ||") { t.Errorf("Expected first line of multi-line value, got:\n%s", output) } - if !strings.Contains(output, " secrets.PAT_3 }}") { - t.Errorf("Expected last line of multi-line value, got:\n%s", output) + // Continuation lines have 4-space prefix preserved, so total indentation is 12+4=16 spaces. + if !strings.Contains(output, " secrets.PAT_3 }}") { + t.Errorf("Expected last line of multi-line value with preserved continuation indentation (16 spaces), got:\n%s", output) } }) @@ -413,13 +416,16 @@ func TestAppendEnvVarLine(t *testing.T) { }, }, { - name: "multi-line value emitted as literal block scalar", - key: "COPILOT_GITHUB_TOKEN", + name: "multi-line value emitted as literal block scalar", + key: "COPILOT_GITHUB_TOKEN", + // Continuation lines have 4-space leading whitespace (as produced by goccy/go-yaml + // when parsing a >- block scalar with extra-indented continuation lines). value: "${{ secrets.PAT_1 != '' && secrets.PAT_1 ||\n secrets.PAT_2 }}", expectedContent: []string{ " COPILOT_GITHUB_TOKEN: |", " ${{ secrets.PAT_1 != '' && secrets.PAT_1 ||", - " secrets.PAT_2 }}", + // Continuation line has 4-space prefix preserved: 12 base + 4 continuation = 16 spaces total. + " secrets.PAT_2 }}", }, notExpected: []string{ "COPILOT_GITHUB_TOKEN: ${{ secrets.PAT_1", @@ -436,6 +442,18 @@ func TestAppendEnvVarLine(t *testing.T) { "TRIMMED: |", }, }, + { + name: "only one trailing newline is trimmed (not multiple)", + key: "MULTI_NEWLINE", + value: "line one\nline two\n\n", + expectedContent: []string{ + " MULTI_NEWLINE: |", + " line one", + " line two", + // The second trailing newline becomes an empty line in the block scalar. + " ", + }, + }, { name: "multi-line value with trailing newline trimmed", key: "MULTI", diff --git a/pkg/workflow/secret_validation_test.go b/pkg/workflow/secret_validation_test.go index 0868f8fc1f0..41388c57633 100644 --- a/pkg/workflow/secret_validation_test.go +++ b/pkg/workflow/secret_validation_test.go @@ -252,6 +252,8 @@ func TestGenerateMultiSecretValidationStepWithEnvOverrides(t *testing.T) { }) t.Run("multi-line override emitted as literal block scalar", func(t *testing.T) { + // Continuation lines have 4-space leading whitespace (as produced by goccy/go-yaml + // when parsing a >- block scalar with extra-indented continuation lines). multiLineExpr := "${{ secrets.GH_AW_PAT_1 != '' && secrets.GH_AW_PAT_1 ||\n secrets.GH_AW_PAT_2 != '' && secrets.GH_AW_PAT_2 ||\n secrets.GH_AW_PAT_3 }}" overrides := map[string]string{ "COPILOT_GITHUB_TOKEN": multiLineExpr, @@ -271,8 +273,9 @@ func TestGenerateMultiSecretValidationStepWithEnvOverrides(t *testing.T) { if !strings.Contains(stepContent, " ${{ secrets.GH_AW_PAT_1 != '' && secrets.GH_AW_PAT_1 ||") { t.Errorf("Expected first line of multi-line expression, got:\n%s", stepContent) } - if !strings.Contains(stepContent, " secrets.GH_AW_PAT_3 }}") { - t.Errorf("Expected last line of multi-line expression, got:\n%s", stepContent) + // Continuation lines have 4-space prefix preserved: 12 base + 4 continuation = 16 spaces total. + if !strings.Contains(stepContent, " secrets.GH_AW_PAT_3 }}") { + t.Errorf("Expected last line of multi-line expression with preserved continuation indentation (16 spaces), got:\n%s", stepContent) } // Should not emit the raw multi-line value inline if strings.Contains(stepContent, "COPILOT_GITHUB_TOKEN: ${{ secrets.GH_AW_PAT_1") {