From 8886c19cb0fae6082e0a785e0da24c25926e78ec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 12:54:00 +0000 Subject: [PATCH 1/4] Initial plan From 5600ca4b2bbfdfb9c70840afc90d859c8ad2052c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 12:59:38 +0000 Subject: [PATCH 2/4] Initial plan for Option C: engine.concurrency: none support Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/smoke-copilot.lock.yml | 6 ++++++ 1 file changed, 6 insertions(+) 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: | From 96ec44cd5887a3c0b9fee80e0a3db54660c10d0b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 13:09:28 +0000 Subject: [PATCH 3/4] feat: support engine.concurrency: none to disable default job-level concurrency Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../src/content/docs/reference/concurrency.md | 22 +++++++++++++++++++ pkg/parser/schemas/main_workflow_schema.json | 4 ++-- pkg/workflow/concurrency.go | 5 +++++ pkg/workflow/concurrency_test.go | 9 ++++++++ pkg/workflow/engine.go | 9 ++++++-- 5 files changed, 45 insertions(+), 4 deletions(-) diff --git a/docs/src/content/docs/reference/concurrency.md b/docs/src/content/docs/reference/concurrency.md index 3f1ad90c743..b26559cd81c 100644 --- a/docs/src/content/docs/reference/concurrency.md +++ b/docs/src/content/docs/reference/concurrency.md @@ -53,6 +53,28 @@ tools: --- ``` +## Disabling Job-Level Concurrency + +Set `engine.concurrency: none` to opt out of the default per-engine job-level concurrency. This is useful when the workflow-level `concurrency` already provides the required isolation (for example, a `workflow_dispatch` workflow that processes individual issues and includes the issue number in its concurrency group): + +```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 + concurrency: none # Disable default job-level concurrency; rely on workflow-level group above +--- +``` + +Without `engine.concurrency: none`, the compiled agent job would use `gh-aw-copilot-${{ github.workflow }}` as its concurrency group, which serializes all runs of the workflow regardless of the issue number. Setting it to `none` allows different issues to be processed in parallel while still cancelling duplicate runs for the same issue. + ## 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..9c8907d8588 100644 --- a/pkg/workflow/concurrency.go +++ b/pkg/workflow/concurrency.go @@ -44,6 +44,11 @@ func GenerateJobConcurrencyConfig(workflowData *WorkflowData) string { // 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 default job-level concurrency + if workflowData.EngineConfig.Concurrency == "none" { + concurrencyLog.Print("Engine concurrency set to none, skipping default job concurrency") + return "" + } concurrencyLog.Print("Using engine-configured concurrency") return workflowData.EngineConfig.Concurrency } diff --git a/pkg/workflow/concurrency_test.go b/pkg/workflow/concurrency_test.go index cbdd136ec2f..a8b0b94db31 100644 --- a/pkg/workflow/concurrency_test.go +++ b/pkg/workflow/concurrency_test.go @@ -391,6 +391,15 @@ func TestGenerateJobConcurrencyConfig(t *testing.T) { group: "gh-aw-codex-${{ github.workflow }}"`, description: "Codex with schedule should get default 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", + }, } for _, tt := range tests { 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 From a87cf64022f0486c7fc0a6d34280c3e221ab36e5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 13:20:32 +0000 Subject: [PATCH 4/4] feat: make engine.concurrency: none the default (no job-level concurrency by default) Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../src/content/docs/reference/concurrency.md | 26 +++++--- pkg/workflow/concurrency.go | 66 +++---------------- pkg/workflow/concurrency_test.go | 21 +++--- 3 files changed, 35 insertions(+), 78 deletions(-) diff --git a/docs/src/content/docs/reference/concurrency.md b/docs/src/content/docs/reference/concurrency.md index b26559cd81c..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 @@ -55,7 +59,7 @@ tools: ## Disabling Job-Level Concurrency -Set `engine.concurrency: none` to opt out of the default per-engine job-level concurrency. This is useful when the workflow-level `concurrency` already provides the required isolation (for example, a `workflow_dispatch` workflow that processes individual issues and includes the issue number in its concurrency group): +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 --- @@ -69,11 +73,17 @@ concurrency: cancel-in-progress: true engine: id: copilot - concurrency: none # Disable default job-level concurrency; rely on workflow-level group above + # No engine.concurrency needed - none is the default --- ``` -Without `engine.concurrency: none`, the compiled agent job would use `gh-aw-copilot-${{ github.workflow }}` as its concurrency group, which serializes all runs of the workflow regardless of the issue number. Setting it to `none` allows different issues to be processed in parallel while still cancelling duplicate runs for the same issue. +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 diff --git a/pkg/workflow/concurrency.go b/pkg/workflow/concurrency.go index 9c8907d8588..bc98e299e60 100644 --- a/pkg/workflow/concurrency.go +++ b/pkg/workflow/concurrency.go @@ -38,76 +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 default job-level 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 default job concurrency") + 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 a8b0b94db31..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,13 @@ 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",