From dea45793f485c1df4cb3d98f53d26c62415a103e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Mar 2026 05:36:58 +0000 Subject: [PATCH 1/2] Initial plan From afc3a026a20a5b4ccad6afa1eb21fd4c13655c47 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Mar 2026 05:58:08 +0000 Subject: [PATCH 2/2] Add concurrency-group support to safe_outputs job Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/parser/schemas/main_workflow_schema.json | 5 ++ pkg/workflow/compiler.go | 7 +++ pkg/workflow/compiler_safe_outputs_job.go | 8 +++ .../compiler_safe_outputs_job_test.go | 54 ++++++++++++++++++- pkg/workflow/compiler_types.go | 1 + pkg/workflow/safe_outputs_config.go | 8 +++ 6 files changed, 82 insertions(+), 1 deletion(-) diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index dee5868af64..e49d686fe57 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -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/" diff --git a/pkg/workflow/compiler.go b/pkg/workflow/compiler.go index e4500cb1970..e64db99dd09 100644 --- a/pkg/workflow/compiler.go +++ b/pkg/workflow/compiler.go @@ -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.")) diff --git a/pkg/workflow/compiler_safe_outputs_job.go b/pkg/workflow/compiler_safe_outputs_job.go index e7f874af230..e02c2fdfff6 100644 --- a/pkg/workflow/compiler_safe_outputs_job.go +++ b/pkg/workflow/compiler_safe_outputs_job.go @@ -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, diff --git a/pkg/workflow/compiler_safe_outputs_job_test.go b/pkg/workflow/compiler_safe_outputs_job_test.go index 0a327833b84..3f6539d8481 100644 --- a/pkg/workflow/compiler_safe_outputs_job_test.go +++ b/pkg/workflow/compiler_safe_outputs_job_test.go @@ -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 diff --git a/pkg/workflow/compiler_types.go b/pkg/workflow/compiler_types.go index f96fae0b86d..0a326fad25e 100644 --- a/pkg/workflow/compiler_types.go +++ b/pkg/workflow/compiler_types.go @@ -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) } diff --git a/pkg/workflow/safe_outputs_config.go b/pkg/workflow/safe_outputs_config.go index 6f555b644e4..1d7dad82bb3 100644 --- a/pkg/workflow/safe_outputs_config.go +++ b/pkg/workflow/safe_outputs_config.go @@ -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 {