diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index c5af982b005..1e2ad1db81e 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: | diff --git a/docs/src/content/docs/reference/concurrency.md b/docs/src/content/docs/reference/concurrency.md index 3f1ad90c743..94653e8417a 100644 --- a/docs/src/content/docs/reference/concurrency.md +++ b/docs/src/content/docs/reference/concurrency.md @@ -24,13 +24,17 @@ This ensures workflows on different issues, PRs, or branches run concurrently wi ## Per-Engine Concurrency -The default per-engine pattern `gh-aw-{engine-id}` ensures only one agent job runs per engine across all workflows, preventing AI resource exhaustion. The group includes only the engine ID and `gh-aw-` prefix - workflow name, issue/PR numbers, and branches are excluded. +By default, no job-level concurrency is applied to agent jobs (`engine.concurrency: none` is the default). This allows multiple agent jobs — even across different workflows — to execute in parallel without any engine-level serialization. + +To opt in to job-level concurrency and limit how many agent jobs run simultaneously, set `engine.concurrency` explicitly: ```yaml wrap -jobs: - agent: - concurrency: - group: "gh-aw-{engine-id}" +--- +engine: + id: copilot + concurrency: # Limit to one agent job per engine across all workflows + group: "gh-aw-copilot-${{ github.workflow }}" +--- ``` ## Custom Concurrency @@ -53,6 +57,34 @@ tools: --- ``` +## Disabling Job-Level Concurrency + +Since `engine.concurrency: none` is the default, no action is required to run workflow dispatches in parallel. The workflow-level `concurrency` block is sufficient for per-issue isolation: + +```yaml wrap +--- +on: + workflow_dispatch: + inputs: + issue_number: + required: true +concurrency: + group: issue-triage-${{ github.event.inputs.issue_number }} + cancel-in-progress: true +engine: + id: copilot + # No engine.concurrency needed - none is the default +--- +``` + +You can also be explicit by setting `engine.concurrency: none`: + +```yaml wrap +engine: + id: copilot + concurrency: none # Explicit opt-out of job-level concurrency +``` + ## Related Documentation - [AI Engines](/gh-aw/reference/engines/) - Engine configuration and capabilities diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index a0a2e5f0ecc..8cf974e99ca 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -7438,7 +7438,7 @@ "oneOf": [ { "type": "string", - "description": "Simple concurrency group name. Gets converted to GitHub Actions concurrency format with the specified group." + "description": "Simple concurrency group name, or 'none' to disable the default job-level concurrency. Use 'none' when the workflow-level concurrency already provides the desired isolation (e.g., per-issue dispatch workflows). Gets converted to GitHub Actions concurrency format with the specified group." }, { "type": "object", @@ -7457,7 +7457,7 @@ "additionalProperties": false } ], - "description": "Agent job concurrency configuration. Defaults to single job per engine across all workflows (group: 'gh-aw-{engine-id}'). Supports full GitHub Actions concurrency syntax." + "description": "Agent job concurrency configuration. Defaults to single job per engine across all workflows (group: 'gh-aw-{engine-id}'). Set to 'none' to disable the default job-level concurrency and rely solely on workflow-level concurrency. Supports full GitHub Actions concurrency syntax." }, "user-agent": { "type": "string", diff --git a/pkg/workflow/concurrency.go b/pkg/workflow/concurrency.go index c79452ceaa7..bc98e299e60 100644 --- a/pkg/workflow/concurrency.go +++ b/pkg/workflow/concurrency.go @@ -38,71 +38,26 @@ func GenerateConcurrencyConfig(workflowData *WorkflowData, isCommandTrigger bool } // GenerateJobConcurrencyConfig generates the agent concurrency configuration -// for the agent job based on engine.concurrency field +// for the agent job based on engine.concurrency field. +// By default, no job-level concurrency is applied (equivalent to engine.concurrency: none). +// Set engine.concurrency to a group name or object to opt in to job-level concurrency. func GenerateJobConcurrencyConfig(workflowData *WorkflowData) string { concurrencyLog.Print("Generating job-level concurrency config") // If concurrency is explicitly configured in engine, use it if workflowData.EngineConfig != nil && workflowData.EngineConfig.Concurrency != "" { + // "none" is a special value to opt out of job-level concurrency (also the default) + if workflowData.EngineConfig.Concurrency == "none" { + concurrencyLog.Print("Engine concurrency set to none, skipping job concurrency") + return "" + } concurrencyLog.Print("Using engine-configured concurrency") return workflowData.EngineConfig.Concurrency } - // Check if this workflow has special trigger handling (issues, PRs, discussions, push, command) - // For these cases, no default concurrency should be applied at agent level - if hasSpecialTriggers(workflowData) { - concurrencyLog.Print("Workflow has special triggers, skipping default job concurrency") - return "" - } - - // For generic triggers like workflow_dispatch, apply default concurrency - // Pattern: gh-aw-{engine-id}-${{ github.workflow }} - engineID := "" - if workflowData.EngineConfig != nil && workflowData.EngineConfig.ID != "" { - engineID = workflowData.EngineConfig.ID - } - - if engineID == "" { - // If no engine ID is available, skip default concurrency - return "" - } - - // Build the default concurrency configuration - groupValue := fmt.Sprintf("gh-aw-%s-${{ github.workflow }}", engineID) - concurrencyConfig := fmt.Sprintf("concurrency:\n group: \"%s\"", groupValue) - - return concurrencyConfig -} - -// hasSpecialTriggers checks if the workflow has special trigger types that require -// workflow-level concurrency handling (issues, PRs, discussions, push, command) -func hasSpecialTriggers(workflowData *WorkflowData) bool { - // Check for specific trigger types that have special concurrency handling - on := workflowData.On - - // Check for issue-related triggers - if isIssueWorkflow(on) { - return true - } - - // Check for pull request triggers - if isPullRequestWorkflow(on) { - return true - } - - // Check for discussion triggers - if isDiscussionWorkflow(on) { - return true - } - - // Check for push triggers - if isPushWorkflow(on) { - return true - } - - // If none of the special triggers are detected, return false - // This means workflow_dispatch and other generic triggers will get default concurrency - return false + // Default: no job-level concurrency + concurrencyLog.Print("No engine concurrency configured, skipping job concurrency (default)") + return "" } // isPullRequestWorkflow checks if a workflow's "on" section contains pull_request triggers diff --git a/pkg/workflow/concurrency_test.go b/pkg/workflow/concurrency_test.go index cbdd136ec2f..a8213c5930c 100644 --- a/pkg/workflow/concurrency_test.go +++ b/pkg/workflow/concurrency_test.go @@ -316,24 +316,22 @@ func TestGenerateJobConcurrencyConfig(t *testing.T) { description string }{ { - name: "Default concurrency for workflow_dispatch with copilot engine", + name: "No default concurrency for workflow_dispatch with copilot engine", workflowData: &WorkflowData{ On: "on:\n workflow_dispatch:", EngineConfig: &EngineConfig{ID: "copilot"}, }, - expected: `concurrency: - group: "gh-aw-copilot-${{ github.workflow }}"`, - description: "Copilot with workflow_dispatch should get default concurrency", + expected: "", + description: "Copilot with workflow_dispatch should NOT get default job-level concurrency", }, { - name: "Default concurrency for workflow_dispatch with claude engine", + name: "No default concurrency for workflow_dispatch with claude engine", workflowData: &WorkflowData{ On: "on:\n workflow_dispatch:", EngineConfig: &EngineConfig{ID: "claude"}, }, - expected: `concurrency: - group: "gh-aw-claude-${{ github.workflow }}"`, - description: "Claude with workflow_dispatch should get default concurrency", + expected: "", + description: "Claude with workflow_dispatch should NOT get default job-level concurrency", }, { name: "No default concurrency for push workflows", @@ -382,14 +380,22 @@ func TestGenerateJobConcurrencyConfig(t *testing.T) { description: "Should preserve cancel-in-progress when specified", }, { - name: "Default concurrency for schedule with codex engine", + name: "No default concurrency for schedule with codex engine", workflowData: &WorkflowData{ On: "on:\n schedule:\n - cron: '0 0 * * *'", EngineConfig: &EngineConfig{ID: "codex"}, }, - expected: `concurrency: - group: "gh-aw-codex-${{ github.workflow }}"`, - description: "Codex with schedule should get default concurrency", + expected: "", + description: "Codex with schedule should NOT get default job-level concurrency", + }, + { + name: "No concurrency when engine.concurrency is set to none", + workflowData: &WorkflowData{ + On: "on:\n workflow_dispatch:", + EngineConfig: &EngineConfig{ID: "copilot", Concurrency: "none"}, + }, + expected: "", + description: "engine.concurrency: none should disable default job-level concurrency", }, } diff --git a/pkg/workflow/engine.go b/pkg/workflow/engine.go index de933b93094..c004fb6a6c3 100644 --- a/pkg/workflow/engine.go +++ b/pkg/workflow/engine.go @@ -119,8 +119,13 @@ func (c *Compiler) ExtractEngineConfig(frontmatter map[string]any) (string, *Eng // Extract optional 'concurrency' field (string or object format) if concurrency, hasConcurrency := engineObj["concurrency"]; hasConcurrency { if concurrencyStr, ok := concurrency.(string); ok { - // Simple string format (group name) - config.Concurrency = fmt.Sprintf("concurrency:\n group: \"%s\"", concurrencyStr) + if concurrencyStr == "none" { + // Special value to opt out of default job-level concurrency + config.Concurrency = "none" + } else { + // Simple string format (group name) + config.Concurrency = fmt.Sprintf("concurrency:\n group: \"%s\"", concurrencyStr) + } } else if concurrencyObj, ok := concurrency.(map[string]any); ok { // Object format with group and optional cancel-in-progress var parts []string