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'); diff --git a/pkg/workflow/compiler_safe_outputs_builder.go b/pkg/workflow/compiler_safe_outputs_builder.go index 4d72e9e28a0..9f1ce1df701 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()) + placeholderJSON, err := jsonutil.MarshalCompactNoHTMLEscape(expression.placeholder()) if err != nil { return nil, err } - configJSON = bytes.ReplaceAll(configJSON, placeholder, []byte(expression.expr)) + result = bytes.ReplaceAll(result, []byte(placeholderJSON), []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..4db1345a9a6 100644 --- a/pkg/workflow/compiler_safe_outputs_config_targets_test.go +++ b/pkg/workflow/compiler_safe_outputs_config_targets_test.go @@ -120,6 +120,29 @@ 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 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()