From de15bbebdc04506b6d2c5c8110013ed1b355443a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 12:52:33 +0000 Subject: [PATCH 1/3] Initial plan From ae12ad79a8e52f14d85f945fdb969c4f4f1b122d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 12:57:26 +0000 Subject: [PATCH 2/3] Initial plan: fix blocked constraints in safe-outputs configs Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/smoke-copilot.lock.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index c5af982b00..1e2ad1db81 100644 --- a/.github/workflows/smoke-copilot.lock.yml +++ b/.github/workflows/smoke-copilot.lock.yml @@ -1861,6 +1861,12 @@ jobs: echo "run_detection=false" >> "$GITHUB_OUTPUT" echo "Detection skipped: no agent outputs or patches to analyze" fi + - name: Clear MCP configuration for detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + rm -f /tmp/gh-aw/mcp-config/mcp-servers.json + rm -f /home/runner/.copilot/mcp-config.json + rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" - name: Prepare threat detection files if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | From dc555ccd907181237baba102395e3368dcd8ec6f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 13:06:32 +0000 Subject: [PATCH 3/3] Fix: compiler drops blocked constraints from safe-outputs configs inconsistently - Add blocked field to add_labels config.json generation (safe_outputs_config_generation.go) - Add blocked field to assign_to_user handler config (compiler_safe_outputs_config.go) - Add blocked field to unassign_from_user handler config (compiler_safe_outputs_config.go) - Add tests for all three fixes Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/compiler_safe_outputs_config.go | 2 + .../compiler_safe_outputs_config_test.go | 100 ++++++++++++++++++ .../safe_outputs_config_generation.go | 3 + .../safe_outputs_config_generation_test.go | 37 +++++++ 4 files changed, 142 insertions(+) diff --git a/pkg/workflow/compiler_safe_outputs_config.go b/pkg/workflow/compiler_safe_outputs_config.go index f4e16d73b2..c4861df8b6 100644 --- a/pkg/workflow/compiler_safe_outputs_config.go +++ b/pkg/workflow/compiler_safe_outputs_config.go @@ -618,6 +618,7 @@ var handlerRegistry = map[string]handlerBuilder{ return newHandlerConfigBuilder(). AddTemplatableInt("max", c.Max). AddStringSlice("allowed", c.Allowed). + AddStringSlice("blocked", c.Blocked). AddIfNotEmpty("target", c.Target). AddIfNotEmpty("target-repo", c.TargetRepoSlug). AddStringSlice("allowed_repos", c.AllowedRepos). @@ -632,6 +633,7 @@ var handlerRegistry = map[string]handlerBuilder{ return newHandlerConfigBuilder(). AddTemplatableInt("max", c.Max). AddStringSlice("allowed", c.Allowed). + AddStringSlice("blocked", c.Blocked). AddIfNotEmpty("target", c.Target). AddIfNotEmpty("target-repo", c.TargetRepoSlug). AddStringSlice("allowed_repos", c.AllowedRepos). diff --git a/pkg/workflow/compiler_safe_outputs_config_test.go b/pkg/workflow/compiler_safe_outputs_config_test.go index 54baa4d94a..3aadd60299 100644 --- a/pkg/workflow/compiler_safe_outputs_config_test.go +++ b/pkg/workflow/compiler_safe_outputs_config_test.go @@ -1084,3 +1084,103 @@ func TestHandlerConfigUnassignFromUser(t *testing.T) { } } } + +// TestHandlerConfigAssignToUserWithBlocked tests that blocked patterns are included in assign_to_user handler config +func TestHandlerConfigAssignToUserWithBlocked(t *testing.T) { + compiler := NewCompiler() + + workflowData := &WorkflowData{ + Name: "Test Workflow", + SafeOutputs: &SafeOutputsConfig{ + AssignToUser: &AssignToUserConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{ + Max: strPtr("1"), + }, + SafeOutputTargetConfig: SafeOutputTargetConfig{ + Target: "*", + TargetRepoSlug: "microsoft/vscode", + }, + Blocked: []string{"copilot", "*[bot]"}, + }, + }, + } + + var steps []string + compiler.addHandlerManagerConfigEnvVar(&steps, workflowData) + + for _, step := range steps { + if strings.Contains(step, "GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG") { + parts := strings.Split(step, "GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: ") + if len(parts) == 2 { + jsonStr := strings.TrimSpace(parts[1]) + jsonStr = strings.Trim(jsonStr, "\"") + jsonStr = strings.ReplaceAll(jsonStr, "\\\"", "\"") + + var config map[string]map[string]any + err := json.Unmarshal([]byte(jsonStr), &config) + require.NoError(t, err, "Handler config JSON should be valid") + + assignConfig, ok := config["assign_to_user"] + require.True(t, ok, "Should have assign_to_user handler") + + blocked, ok := assignConfig["blocked"] + require.True(t, ok, "Should have blocked field") + blockedSlice, ok := blocked.([]any) + require.True(t, ok, "Blocked should be an array") + assert.Len(t, blockedSlice, 2, "Should have 2 blocked patterns") + assert.Equal(t, "copilot", blockedSlice[0], "First blocked pattern should be copilot") + assert.Equal(t, "*[bot]", blockedSlice[1], "Second blocked pattern should be *[bot]") + } + } + } +} + +// TestHandlerConfigUnassignFromUserWithBlocked tests that blocked patterns are included in unassign_from_user handler config +func TestHandlerConfigUnassignFromUserWithBlocked(t *testing.T) { + compiler := NewCompiler() + + workflowData := &WorkflowData{ + Name: "Test Workflow", + SafeOutputs: &SafeOutputsConfig{ + UnassignFromUser: &UnassignFromUserConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{ + Max: strPtr("2"), + }, + SafeOutputTargetConfig: SafeOutputTargetConfig{ + Target: "*", + TargetRepoSlug: "microsoft/vscode", + }, + Blocked: []string{"copilot", "*[bot]"}, + }, + }, + } + + var steps []string + compiler.addHandlerManagerConfigEnvVar(&steps, workflowData) + + for _, step := range steps { + if strings.Contains(step, "GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG") { + parts := strings.Split(step, "GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: ") + if len(parts) == 2 { + jsonStr := strings.TrimSpace(parts[1]) + jsonStr = strings.Trim(jsonStr, "\"") + jsonStr = strings.ReplaceAll(jsonStr, "\\\"", "\"") + + var config map[string]map[string]any + err := json.Unmarshal([]byte(jsonStr), &config) + require.NoError(t, err, "Handler config JSON should be valid") + + unassignConfig, ok := config["unassign_from_user"] + require.True(t, ok, "Should have unassign_from_user handler") + + blocked, ok := unassignConfig["blocked"] + require.True(t, ok, "Should have blocked field") + blockedSlice, ok := blocked.([]any) + require.True(t, ok, "Blocked should be an array") + assert.Len(t, blockedSlice, 2, "Should have 2 blocked patterns") + assert.Equal(t, "copilot", blockedSlice[0], "First blocked pattern should be copilot") + assert.Equal(t, "*[bot]", blockedSlice[1], "Second blocked pattern should be *[bot]") + } + } + } +} diff --git a/pkg/workflow/safe_outputs_config_generation.go b/pkg/workflow/safe_outputs_config_generation.go index 0a10f77cfd..1c59cb642a 100644 --- a/pkg/workflow/safe_outputs_config_generation.go +++ b/pkg/workflow/safe_outputs_config_generation.go @@ -274,6 +274,9 @@ func generateSafeOutputsConfig(data *WorkflowData) string { if len(data.SafeOutputs.AddLabels.Allowed) > 0 { additionalFields["allowed"] = data.SafeOutputs.AddLabels.Allowed } + if len(data.SafeOutputs.AddLabels.Blocked) > 0 { + additionalFields["blocked"] = data.SafeOutputs.AddLabels.Blocked + } safeOutputsConfig["add_labels"] = generateTargetConfigWithRepos( data.SafeOutputs.AddLabels.SafeOutputTargetConfig, data.SafeOutputs.AddLabels.Max, diff --git a/pkg/workflow/safe_outputs_config_generation_test.go b/pkg/workflow/safe_outputs_config_generation_test.go index 0ce0fed7de..3d2328079c 100644 --- a/pkg/workflow/safe_outputs_config_generation_test.go +++ b/pkg/workflow/safe_outputs_config_generation_test.go @@ -328,3 +328,40 @@ func TestGenerateCustomJobToolDefinitionJSONSerializable(t *testing.T) { require.NoError(t, json.Unmarshal(data, &parsed), "JSON should be parseable back") assert.Equal(t, "deploy", parsed["name"], "name should round-trip through JSON") } + +// TestGenerateSafeOutputsConfigAddLabelsBlocked tests that the blocked field is included +// in config.json for add_labels. +func TestGenerateSafeOutputsConfigAddLabelsBlocked(t *testing.T) { + data := &WorkflowData{ + SafeOutputs: &SafeOutputsConfig{ + AddLabels: &AddLabelsConfig{ + BaseSafeOutputConfig: BaseSafeOutputConfig{Max: strPtr("5")}, + SafeOutputTargetConfig: SafeOutputTargetConfig{ + Target: "*", + TargetRepoSlug: "microsoft/vscode", + }, + Allowed: []string{"bug", "enhancement"}, + Blocked: []string{"[*]*", "~spam", "stale", "triage-needed"}, + }, + }, + } + + result := generateSafeOutputsConfig(data) + require.NotEmpty(t, result, "Expected non-empty config") + + var parsed map[string]any + require.NoError(t, json.Unmarshal([]byte(result), &parsed), "Result must be valid JSON") + + addLabelsConfig, ok := parsed["add_labels"].(map[string]any) + require.True(t, ok, "Expected add_labels key in config") + + blocked, ok := addLabelsConfig["blocked"] + require.True(t, ok, "Expected blocked field in add_labels config") + blockedSlice, ok := blocked.([]any) + require.True(t, ok, "Blocked should be an array") + assert.Len(t, blockedSlice, 4, "Should have 4 blocked patterns") + assert.Equal(t, "[*]*", blockedSlice[0], "First blocked pattern should match") + assert.Equal(t, "~spam", blockedSlice[1], "Second blocked pattern should match") + assert.Equal(t, "stale", blockedSlice[2], "Third blocked pattern should match") + assert.Equal(t, "triage-needed", blockedSlice[3], "Fourth blocked pattern should match") +}