Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions pkg/workflow/compiler_orchestrator_workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,10 @@ func (c *Compiler) extractAdditionalConfigurations(
}
workflowData.SafeOutputs = mergedSafeOutputs

// Auto-inject create-issues if safe-outputs is configured but has no non-builtin outputs.
// This ensures every workflow with safe-outputs has at least one meaningful action handler.
applyDefaultCreateIssue(workflowData)

return nil
}

Expand Down
1 change: 1 addition & 0 deletions pkg/workflow/compiler_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,7 @@ type SafeOutputsConfig struct {
Mentions *MentionsConfig `yaml:"mentions,omitempty"` // Configuration for @mention filtering in safe outputs
Footer *bool `yaml:"footer,omitempty"` // Global footer control - when false, omits visible footer from all safe outputs (XML markers still included)
GroupReports bool `yaml:"group-reports,omitempty"` // If true, create parent "Failed runs" issue for agent failures (default: false)
AutoInjectedCreateIssue bool `yaml:"-"` // Internal: true when create-issues was automatically injected by the compiler (not user-configured)
}

// SafeOutputMessagesConfig holds custom message templates for safe-output footer and notification messages
Expand Down
67 changes: 67 additions & 0 deletions pkg/workflow/safe_outputs_config_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,51 @@ func (c *Compiler) formatSafeOutputsRunsOn(safeOutputs *SafeOutputsConfig) strin
return fmt.Sprintf("runs-on: %s", safeOutputs.RunsOn)
}

// builtinSafeOutputFields contains the struct field names for the built-in safe output types
// that are excluded from the "non-builtin" check. These are: noop, missing-data, missing-tool.
var builtinSafeOutputFields = map[string]bool{
"NoOp": true,
"MissingData": true,
"MissingTool": true,
}

// nonBuiltinSafeOutputFieldNames is a pre-computed list of field names from safeOutputFieldMapping
// that are not builtins, used by hasNonBuiltinSafeOutputsEnabled to avoid repeated map iterations.
var nonBuiltinSafeOutputFieldNames = func() []string {
var fields []string
for fieldName := range safeOutputFieldMapping {
if !builtinSafeOutputFields[fieldName] {
fields = append(fields, fieldName)
}
}
return fields
}()

// hasNonBuiltinSafeOutputsEnabled checks if any non-builtin safe outputs are configured.
// The builtin types (noop, missing-data, missing-tool) are excluded from this check
// because they are always auto-enabled and do not represent a meaningful output action.
func hasNonBuiltinSafeOutputsEnabled(safeOutputs *SafeOutputsConfig) bool {
if safeOutputs == nil {
return false
}

// Custom safe-jobs are always non-builtin
if len(safeOutputs.Jobs) > 0 {
return true
}

// Check non-builtin pointer fields using the pre-computed list
val := reflect.ValueOf(safeOutputs).Elem()
for _, fieldName := range nonBuiltinSafeOutputFieldNames {
field := val.FieldByName(fieldName)
if field.IsValid() && !field.IsNil() {
return true
}
}
Comment on lines +132 to +164
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hasNonBuiltinSafeOutputsEnabled() relies on nonBuiltinSafeOutputFieldNames derived from safeOutputFieldMapping, but safeOutputFieldMapping does not include several non-builtin SafeOutputsConfig fields (e.g., MissingData, UnassignFromUser, AutofixCodeScanningAlert). As a result, workflows that configure only one of those outputs will be misdetected as having “no non-builtin safe outputs” and will get create-issues auto-injected unexpectedly.

Fix by either (a) making safeOutputFieldMapping exhaustive for all safe-output action fields, or (b) rewriting hasNonBuiltinSafeOutputsEnabled() to reflect over SafeOutputsConfig fields directly and exclude only the builtins + non-action config fields (RunsOn, Env, App, ThreatDetection, etc.), rather than depending on safeOutputFieldMapping.

Copilot uses AI. Check for mistakes.

return false
}

// HasSafeOutputsEnabled checks if any safe-outputs are enabled
func HasSafeOutputsEnabled(safeOutputs *SafeOutputsConfig) bool {
enabled := hasAnySafeOutputEnabled(safeOutputs)
Expand All @@ -132,6 +177,28 @@ func HasSafeOutputsEnabled(safeOutputs *SafeOutputsConfig) bool {
return enabled
}

// applyDefaultCreateIssue injects a default create-issues safe output when safe-outputs is configured
// but has no non-builtin output types. The injected config uses the workflow ID as the label
// and [workflowID] as the title prefix. The AutoInjectedCreateIssue flag is set so the prompt
// generator can add a specific instruction for the agent.
func applyDefaultCreateIssue(workflowData *WorkflowData) {
if workflowData.SafeOutputs == nil {
return
}
if hasNonBuiltinSafeOutputsEnabled(workflowData.SafeOutputs) {
return
}

workflowID := workflowData.WorkflowID
safeOutputsConfigLog.Printf("Auto-injecting create-issues for workflow %q (no non-builtin safe outputs configured)", workflowID)
workflowData.SafeOutputs.CreateIssues = &CreateIssuesConfig{
BaseSafeOutputConfig: BaseSafeOutputConfig{Max: 1},
Labels: []string{workflowID},
TitlePrefix: fmt.Sprintf("[%s]", workflowID),
}
workflowData.SafeOutputs.AutoInjectedCreateIssue = true
}

// GetEnabledSafeOutputToolNames returns a list of enabled safe output tool names.
// NOTE: Tool names should NOT be included in agent prompts. The agent should query
// the MCP server to discover available tools. This function is used for generating
Expand Down
Loading