From 297b397ab7b23fa6c17652ae67bc4bac6b7d7578 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:51:32 +0000 Subject: [PATCH 1/4] Initial plan From 554aac2502c57a1e44340c54de70cb0a8ad9ac07 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Aug 2026 17:05:00 +0000 Subject: [PATCH 2/4] Preserve safe-output expression operators Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/compiler_safe_outputs_builder.go | 8 +++++--- .../compiler_safe_outputs_config_targets_test.go | 12 ++++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/pkg/workflow/compiler_safe_outputs_builder.go b/pkg/workflow/compiler_safe_outputs_builder.go index 4d72e9e28a0..a65d6dfb298 100644 --- a/pkg/workflow/compiler_safe_outputs_builder.go +++ b/pkg/workflow/compiler_safe_outputs_builder.go @@ -9,6 +9,7 @@ import ( "strings" "sync/atomic" + "github.com/github/gh-aw/pkg/jsonutil" "github.com/github/gh-aw/pkg/logger" ) @@ -150,18 +151,19 @@ func (b *handlerConfigBuilder) AddTemplatableJSONSlice(key string, value []strin } func marshalSafeOutputsConfig(config map[string]any) ([]byte, error) { - configJSON, err := json.Marshal(config) + configJSON, err := jsonutil.MarshalCompactNoHTMLEscape(config) if err != nil { return nil, err } + result := []byte(configJSON) for _, expression := range templatableJSONExpressions(config) { placeholder, err := json.Marshal(expression.placeholder()) if err != nil { return nil, err } - configJSON = bytes.ReplaceAll(configJSON, placeholder, []byte(expression.expr)) + result = bytes.ReplaceAll(result, placeholder, []byte(expression.expr)) } - return configJSON, nil + return result, nil } var ( diff --git a/pkg/workflow/compiler_safe_outputs_config_targets_test.go b/pkg/workflow/compiler_safe_outputs_config_targets_test.go index d09bb6190f6..baf0da007ef 100644 --- a/pkg/workflow/compiler_safe_outputs_config_targets_test.go +++ b/pkg/workflow/compiler_safe_outputs_config_targets_test.go @@ -120,6 +120,18 @@ func TestHandlerConfigClosePullRequestTargetRepo(t *testing.T) { } } +func TestMarshalSafeOutputsConfigPreservesExpressionOperators(t *testing.T) { + configJSON, err := marshalSafeOutputsConfig(map[string]any{ + "create_issue": map[string]any{ + "target-repo": "${{ condition && value || fallback }}", + }, + }) + + require.NoError(t, err) + assert.Contains(t, string(configJSON), "${{ condition && value || fallback }}") + assert.NotContains(t, string(configJSON), `\u0026`) +} + func TestHandlerConfigCreateCheckRunTarget(t *testing.T) { compiler := NewCompiler() From 33fc483d173dcf4b34ce122a155c580586bbb531 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Aug 2026 18:19:27 +0000 Subject: [PATCH 3/4] Triage PR for merge readiness Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/mcp-inspector.lock.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mcp-inspector.lock.yml b/.github/workflows/mcp-inspector.lock.yml index cdeeddf93e9..a651357db6d 100644 --- a/.github/workflows/mcp-inspector.lock.yml +++ b/.github/workflows/mcp-inspector.lock.yml @@ -674,7 +674,7 @@ jobs: env: GH_AW_FILE_ROOT: "${{ runner.temp }}/gh-aw" GH_AW_FILE_CONFIG: "{\"files\":[{\"path\":\"safeoutputs/config.json\",\"content_env\":\"GH_AW_SAFE_OUTPUTS_CONFIG\"}]}" - GH_AW_SAFE_OUTPUTS_CONFIG: "{\"create_discussion\":{\"category\":\"audits\",\"close_older_discussions\":true,\"expires\":24,\"fallback_to_issue\":true,\"max\":1,\"title_prefix\":\"[mcp-inspector] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"post-to-slack-channel\":{\"description\":\"Post a message to a Slack channel. Message must be 200 characters or less. Supports basic Slack markdown: *bold*, _italic_, ~strike~, `code`, ```code block```, \\u003equote, and links \\u003curl|text\\u003e. Requires GH_AW_SLACK_CHANNEL_ID environment variable to be set.\",\"inputs\":{\"message\":{\"default\":null,\"description\":\"The message to post (max 200 characters, supports Slack markdown)\",\"required\":true,\"type\":\"string\"}},\"output\":\"Message posted to Slack successfully!\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_CONFIG: "{\"create_discussion\":{\"category\":\"audits\",\"close_older_discussions\":true,\"expires\":24,\"fallback_to_issue\":true,\"max\":1,\"title_prefix\":\"[mcp-inspector] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"post-to-slack-channel\":{\"description\":\"Post a message to a Slack channel. Message must be 200 characters or less. Supports basic Slack markdown: *bold*, _italic_, ~strike~, `code`, ```code block```, >quote, and links . Requires GH_AW_SLACK_CHANNEL_ID environment variable to be set.\",\"inputs\":{\"message\":{\"default\":null,\"description\":\"The message to post (max 200 characters, supports Slack markdown)\",\"required\":true,\"type\":\"string\"}},\"output\":\"Message posted to Slack successfully!\"},\"report_incomplete\":{}}" with: script: | const path = require('path'); From a1209df8a2dfb181f8c4419af6a899f178cd553d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Aug 2026 18:24:23 +0000 Subject: [PATCH 4/4] Preserve templatable expression operators Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/compiler_safe_outputs_builder.go | 4 ++-- .../compiler_safe_outputs_config_targets_test.go | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/pkg/workflow/compiler_safe_outputs_builder.go b/pkg/workflow/compiler_safe_outputs_builder.go index a65d6dfb298..9f1ce1df701 100644 --- a/pkg/workflow/compiler_safe_outputs_builder.go +++ b/pkg/workflow/compiler_safe_outputs_builder.go @@ -157,11 +157,11 @@ func marshalSafeOutputsConfig(config map[string]any) ([]byte, error) { } result := []byte(configJSON) for _, expression := range templatableJSONExpressions(config) { - placeholder, err := json.Marshal(expression.placeholder()) + placeholderJSON, err := jsonutil.MarshalCompactNoHTMLEscape(expression.placeholder()) if err != nil { return nil, err } - result = bytes.ReplaceAll(result, placeholder, []byte(expression.expr)) + result = bytes.ReplaceAll(result, []byte(placeholderJSON), []byte(expression.expr)) } return result, nil } diff --git a/pkg/workflow/compiler_safe_outputs_config_targets_test.go b/pkg/workflow/compiler_safe_outputs_config_targets_test.go index baf0da007ef..4db1345a9a6 100644 --- a/pkg/workflow/compiler_safe_outputs_config_targets_test.go +++ b/pkg/workflow/compiler_safe_outputs_config_targets_test.go @@ -132,6 +132,17 @@ func TestMarshalSafeOutputsConfigPreservesExpressionOperators(t *testing.T) { assert.NotContains(t, string(configJSON), `\u0026`) } +func TestMarshalSafeOutputsConfigPreservesTemplatableExpressionOperators(t *testing.T) { + builder := newHandlerConfigBuilder() + builder.AddTemplatableJSONSlice("items", []string{"${{ condition && value || fallback }}"}) + + configJSON, err := marshalSafeOutputsConfig(builder.config) + + require.NoError(t, err) + assert.Contains(t, string(configJSON), "${{ toJSON(condition && value || fallback) }}") + assert.NotContains(t, string(configJSON), `\u0026`) +} + func TestHandlerConfigCreateCheckRunTarget(t *testing.T) { compiler := NewCompiler()