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
15 changes: 15 additions & 0 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -6949,6 +6949,21 @@
"runs-on": {
"type": "string",
"description": "Runner specification for all safe-outputs jobs (activation, create-issue, add-comment, etc.). Single runner label (e.g., 'ubuntu-slim', 'ubuntu-latest', 'windows-latest', 'self-hosted'). Defaults to 'ubuntu-slim'. See https://github.blog/changelog/2025-10-28-1-vcpu-linux-runner-now-available-in-github-actions-in-public-preview/"
},
"steps": {
"type": "array",
"description": "Custom steps to inject into all safe-output jobs. These steps run after checking out the repository and setting up the action, and before any safe-output code executes.",
"items": {
"$ref": "#/$defs/githubActionsStep"
},
"examples": [
[
{
"name": "Install custom dependencies",
"run": "npm install my-package"
}
]
]
}
},
"additionalProperties": false
Expand Down
22 changes: 22 additions & 0 deletions pkg/workflow/compiler_safe_outputs_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,28 @@ func (c *Compiler) buildConsolidatedSafeOutputsJob(data *WorkflowData, mainJobNa
steps = append(steps, checkoutSteps...)
}

// Add user-provided steps after checkout/setup, before safe-output code
if len(data.SafeOutputs.Steps) > 0 {
consolidatedSafeOutputsJobLog.Printf("Adding %d user-provided steps to safe-outputs job", len(data.SafeOutputs.Steps))
for i, step := range data.SafeOutputs.Steps {
stepMap, ok := step.(map[string]any)
if !ok {
consolidatedSafeOutputsJobLog.Printf("Warning: safe-outputs step at index %d is not a valid step object (must be a map with properties like name, run, uses). Skipping this step.", i)
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

The error message should mention that the step will be skipped in the generated workflow to make the impact clearer to users reviewing logs.

Suggested change
consolidatedSafeOutputsJobLog.Printf("Warning: safe-outputs step at index %d is not a valid step object (must be a map with properties like name, run, uses). Skipping this step.", i)
consolidatedSafeOutputsJobLog.Printf("Warning: safe-outputs step at index %d is not a valid step object (must be a map with properties like name, run, uses). Skipping this step in the generated workflow.", i)

Copilot uses AI. Check for mistakes.
continue
}
typedStep, err := MapToStep(stepMap)
if err != nil {
return nil, nil, fmt.Errorf("failed to convert safe-outputs step at index %d to typed step: %w", i, err)
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

Error message could be more actionable by suggesting the user check the step structure in their workflow configuration.

Suggested change
return nil, nil, fmt.Errorf("failed to convert safe-outputs step at index %d to typed step: %w", i, err)
return nil, nil, fmt.Errorf("failed to convert safe-outputs step at index %d to typed step: %w. Check that this entry in 'safe-outputs.steps' uses the correct GitHub Actions step structure (a map with keys like 'name', 'run', or 'uses').", i, err)

Copilot uses AI. Check for mistakes.
}
pinnedStep := ApplyActionPinToTypedStep(typedStep, data)
stepYAML, err := c.convertStepToYAML(pinnedStep.ToMap())
if err != nil {
return nil, nil, fmt.Errorf("failed to convert safe-outputs step at index %d to YAML: %w", i, err)
}
steps = append(steps, stepYAML)
}
}

// Note: Unlock step has been moved to dedicated unlock job
// The safe_outputs job now depends on the unlock job, so the issue
// will already be unlocked when this job runs
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 @@ -526,6 +526,7 @@ type SafeOutputsConfig struct {
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)
MaxBotMentions *string `yaml:"max-bot-mentions,omitempty"` // Maximum bot trigger references (e.g. 'fixes #123') allowed before filtering. Default: 10. Supports integer or GitHub Actions expression.
Steps []any `yaml:"steps,omitempty"` // User-provided steps injected after setup/checkout and before safe-output code
AutoInjectedCreateIssue bool `yaml:"-"` // Internal: true when create-issues was automatically injected by the compiler (not user-configured)
}

Expand Down
5 changes: 5 additions & 0 deletions pkg/workflow/imports.go
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,11 @@ func mergeSafeOutputConfig(result *SafeOutputsConfig, config map[string]any, c *
result.Mentions = importedConfig.Mentions
}

// Merge steps: concatenate imported steps after main workflow's steps
if len(importedConfig.Steps) > 0 {
result.Steps = append(result.Steps, importedConfig.Steps...)
}

// NOTE: Jobs are NOT merged here. They are handled separately in compiler_orchestrator.go
// via mergeSafeJobsFromIncludedConfigs and extractSafeJobsFromFrontmatter.
// The Jobs field is managed independently from other safe-output types to support
Expand Down
8 changes: 8 additions & 0 deletions pkg/workflow/safe_outputs_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,14 @@ func (c *Compiler) extractSafeOutputsConfig(frontmatter map[string]any) *SafeOut
}
}

// Handle steps (user-provided steps injected after checkout/setup, before safe-output code)
if steps, exists := outputMap["steps"]; exists {
if stepsList, ok := steps.([]any); ok {
config.Steps = stepsList
safeOutputsConfigLog.Printf("Configured %d user-provided steps for safe-outputs", len(stepsList))
}
}

// Handle jobs (safe-jobs must be under safe-outputs)
if jobs, exists := outputMap["jobs"]; exists {
if jobsMap, ok := jobs.(map[string]any); ok {
Expand Down
Loading