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
5 changes: 5 additions & 0 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -7044,6 +7044,11 @@
"description": "Override the id-token permission for the safe-outputs job. Use 'write' to force-enable the id-token: write permission (required for OIDC authentication with cloud providers). Use 'none' to suppress automatic detection and prevent adding id-token: write even when vault/OIDC actions are detected in steps. By default, the compiler auto-detects known OIDC/vault actions (aws-actions/configure-aws-credentials, azure/login, google-github-actions/auth, hashicorp/vault-action, cyberark/conjur-action) and adds id-token: write automatically.",
"examples": ["write", "none"]
},
"concurrency-group": {
"type": "string",
"description": "Concurrency group for the safe-outputs job. When set, the safe-outputs job will use this concurrency group with cancel-in-progress: false. Supports GitHub Actions expressions.",
"examples": ["my-workflow-safe-outputs", "safe-outputs-${{ github.repository }}"]
},
"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/"
Expand Down
7 changes: 7 additions & 0 deletions pkg/workflow/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,13 @@ func (c *Compiler) validateWorkflowData(workflowData *WorkflowData, markdownPath
}
}

// Validate safe-outputs concurrency group expression
if workflowData.SafeOutputs != nil && workflowData.SafeOutputs.ConcurrencyGroup != "" {
if err := validateConcurrencyGroupExpression(workflowData.SafeOutputs.ConcurrencyGroup); err != nil {
return formatCompilerError(markdownPath, "error", "safe-outputs.concurrency-group validation failed: "+err.Error(), err)
}
}

// Emit warning for sandbox.agent: false (disables agent sandbox firewall)
if isAgentSandboxDisabled(workflowData) {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage("⚠️ WARNING: Agent sandbox disabled (sandbox.agent: false). This removes firewall protection. The AI agent will have direct network access without firewall filtering. The MCP gateway remains enabled. Only use this for testing or in controlled environments where you trust the AI agent completely."))
Expand Down
8 changes: 8 additions & 0 deletions pkg/workflow/compiler_safe_outputs_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -334,12 +334,20 @@ func (c *Compiler) buildConsolidatedSafeOutputsJob(data *WorkflowData, mainJobNa
// Build job-level environment variables that are common to all safe output steps
jobEnv := c.buildJobLevelSafeOutputEnvVars(data, workflowID)

// Build concurrency config for the safe-outputs job if a concurrency-group is configured
var concurrency string
if data.SafeOutputs.ConcurrencyGroup != "" {
concurrency = c.indentYAMLLines(fmt.Sprintf("concurrency:\n group: %q\n cancel-in-progress: false", data.SafeOutputs.ConcurrencyGroup), " ")
consolidatedSafeOutputsJobLog.Printf("Configuring safe_outputs job concurrency group: %s", data.SafeOutputs.ConcurrencyGroup)
}

job := &Job{
Name: "safe_outputs",
If: jobCondition.Render(),
RunsOn: c.formatSafeOutputsRunsOn(data.SafeOutputs),
Permissions: permissions.RenderToYAML(),
TimeoutMinutes: 15, // Slightly longer timeout for consolidated job with multiple steps
Concurrency: concurrency,
Env: jobEnv,
Steps: steps,
Outputs: outputs,
Expand Down
54 changes: 53 additions & 1 deletion pkg/workflow/compiler_safe_outputs_job_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,59 @@ func TestBuildConsolidatedSafeOutputsJob(t *testing.T) {
}
}

// TestBuildJobLevelSafeOutputEnvVars tests job-level environment variable generation
// TestBuildConsolidatedSafeOutputsJobConcurrencyGroup tests that the concurrency-group field
// is correctly applied to the safe_outputs job
func TestBuildConsolidatedSafeOutputsJobConcurrencyGroup(t *testing.T) {
tests := []struct {
name string
concurrencyGroup string
expectConcurrency bool
}{
{
name: "no concurrency group",
concurrencyGroup: "",
expectConcurrency: false,
},
{
name: "simple concurrency group",
concurrencyGroup: "my-safe-outputs",
expectConcurrency: true,
},
{
name: "concurrency group with expression",
concurrencyGroup: "safe-outputs-${{ github.repository }}",
expectConcurrency: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
compiler := NewCompiler()
compiler.jobManager = NewJobManager()

workflowData := &WorkflowData{
Name: "Test Workflow",
SafeOutputs: &SafeOutputsConfig{
CreateIssues: &CreateIssuesConfig{TitlePrefix: "[Test] "},
ConcurrencyGroup: tt.concurrencyGroup,
},
}

job, _, err := compiler.buildConsolidatedSafeOutputsJob(workflowData, string(constants.AgentJobName), "test-workflow.md")
require.NoError(t, err, "Should build job without error")
require.NotNil(t, job, "Job should not be nil")

if tt.expectConcurrency {
assert.NotEmpty(t, job.Concurrency, "Job should have concurrency set")
assert.Contains(t, job.Concurrency, tt.concurrencyGroup, "Concurrency should contain the group value")
assert.Contains(t, job.Concurrency, "cancel-in-progress: false", "Concurrency should have cancel-in-progress: false")
} else {
assert.Empty(t, job.Concurrency, "Job should have no concurrency set")
}
})
}
}

func TestBuildJobLevelSafeOutputEnvVars(t *testing.T) {
tests := []struct {
name string
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 @@ -493,6 +493,7 @@ type SafeOutputsConfig struct {
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
IDToken *string `yaml:"id-token,omitempty"` // Override id-token permission: "write" to force-add, "none" to disable auto-detection
ConcurrencyGroup string `yaml:"concurrency-group,omitempty"` // Concurrency group for the safe-outputs job (cancel-in-progress is always false)
AutoInjectedCreateIssue bool `yaml:"-"` // Internal: true when create-issues was automatically injected by the compiler (not user-configured)
}

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 @@ -481,6 +481,14 @@ func (c *Compiler) extractSafeOutputsConfig(frontmatter map[string]any) *SafeOut
}
}

// Handle concurrency-group configuration
if concurrencyGroup, exists := outputMap["concurrency-group"]; exists {
if concurrencyGroupStr, ok := concurrencyGroup.(string); ok && concurrencyGroupStr != "" {
config.ConcurrencyGroup = concurrencyGroupStr
safeOutputsConfigLog.Printf("Configured concurrency-group for safe-outputs job: %s", concurrencyGroupStr)
}
}

// 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