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
48 changes: 43 additions & 5 deletions pkg/workflow/concurrency.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,14 @@ func GenerateJobConcurrencyConfig(workflowData *WorkflowData) string {
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
// Check if this workflow has special trigger handling (issues, PRs, discussions, push, command,
// or workflow_dispatch-only). 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
// For remaining generic triggers like schedule, apply default concurrency
// Pattern: gh-aw-{engine-id}-${{ github.workflow }}
engineID := ""
if workflowData.EngineConfig != nil && workflowData.EngineConfig.ID != "" {
Expand All @@ -75,7 +75,8 @@ func GenerateJobConcurrencyConfig(workflowData *WorkflowData) string {
}

// hasSpecialTriggers checks if the workflow has special trigger types that require
// workflow-level concurrency handling (issues, PRs, discussions, push, command)
// workflow-level concurrency handling (issues, PRs, discussions, push, command,
// or workflow_dispatch-only)
func hasSpecialTriggers(workflowData *WorkflowData) bool {
// Check for specific trigger types that have special concurrency handling
on := workflowData.On
Expand All @@ -100,8 +101,14 @@ func hasSpecialTriggers(workflowData *WorkflowData) bool {
return true
}

// workflow_dispatch-only workflows represent explicit user intent, so the
// top-level workflow concurrency group is sufficient – no engine-level group needed
if isWorkflowDispatchOnly(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
// This means other generic triggers (e.g. schedule) will get default concurrency
return false
}

Expand All @@ -120,6 +127,37 @@ func isDiscussionWorkflow(on string) bool {
return strings.Contains(on, "discussion")
}

// isWorkflowDispatchOnly returns true when workflow_dispatch is the only trigger in the
// "on" section, indicating the workflow is always started by explicit user intent.
func isWorkflowDispatchOnly(on string) bool {
if !strings.Contains(on, "workflow_dispatch") {
return false
}
// If any other common trigger is present as a YAML key, this is not a
// workflow_dispatch-only workflow. We check for the trigger name followed by
// ':' (YAML key in object form) or as the sole inline value to avoid false
// matches from input parameter names (e.g., "push_branch" ≠ "push" trigger).
otherTriggers := []string{
"push", "pull_request", "pull_request_review", "pull_request_review_comment",
"pull_request_target", "issues", "issue_comment", "discussion",
"discussion_comment", "schedule", "repository_dispatch", "workflow_run",
"create", "delete", "release", "deployment", "fork", "gollum",
"label", "milestone", "page_build", "public", "registry_package",
"status", "watch", "merge_group", "check_run", "check_suite",
}
for _, trigger := range otherTriggers {
// Trigger in object format: "push:" / " push:"
if strings.Contains(on, trigger+":") {
return false
}
// Trigger in inline format: "on: push" (no colon, trigger is the last token)
if strings.HasSuffix(strings.TrimSpace(on), " "+trigger) {
return false
}
Comment on lines +132 to +156
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

isWorkflowDispatchOnly() relies on string matching for trigger+":" and a suffix check, which does not detect additional triggers when the on section uses array/flow style (e.g. "on": [workflow_dispatch, schedule]). In that case this function would incorrectly return true and skip engine-level concurrency. It also omits valid triggers like workflow_call, so workflow_dispatch + workflow_call would be misclassified as dispatch-only. Consider unmarshaling workflowData.On (or using workflowData.RawFrontmatter["on"]) and treating it as dispatch-only only when the parsed trigger set is exactly {workflow_dispatch}; add unit tests for the array form and for workflow_call mixed with workflow_dispatch.

Copilot uses AI. Check for mistakes.
}
return true
}

// isPushWorkflow checks if a workflow's "on" section contains push triggers
func isPushWorkflow(on string) bool {
return strings.Contains(on, "push")
Expand Down
24 changes: 16 additions & 8 deletions pkg/workflow/concurrency_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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-only 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-only should NOT get default concurrency (user intent, top-level group is sufficient)",
},
{
name: "Default concurrency for workflow_dispatch with claude engine",
name: "No default concurrency for workflow_dispatch-only 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-only should NOT get default concurrency (user intent, top-level group is sufficient)",
},
{
name: "No default concurrency for push workflows",
Expand Down Expand Up @@ -391,6 +389,16 @@ func TestGenerateJobConcurrencyConfig(t *testing.T) {
group: "gh-aw-codex-${{ github.workflow }}"`,
description: "Codex with schedule should get default concurrency",
},
{
name: "Default concurrency for workflow_dispatch combined with schedule",
workflowData: &WorkflowData{
On: "on:\n workflow_dispatch:\n schedule:\n - cron: '0 0 * * *'",
EngineConfig: &EngineConfig{ID: "copilot"},
},
expected: `concurrency:
group: "gh-aw-copilot-${{ github.workflow }}"`,
description: "workflow_dispatch combined with schedule should still get default concurrency (not workflow_dispatch-only)",
},
}

for _, tt := range tests {
Expand Down
14 changes: 6 additions & 8 deletions pkg/workflow/engine_concurrency_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Test content`,
description: "Copilot with push trigger should NOT have default concurrency (special case)",
},
{
name: "Copilot with workflow_dispatch HAS default concurrency",
name: "Copilot with workflow_dispatch does NOT have default concurrency",
markdown: `---
on: workflow_dispatch
engine:
Expand All @@ -48,9 +48,8 @@ tools:

# Test workflow
Test content`,
expectedInJob: `concurrency:
group: "gh-aw-copilot-${{ github.workflow }}"`,
description: "Copilot with workflow_dispatch should have default concurrency",
notExpectedInJob: `concurrency:`,
description: "Copilot with workflow_dispatch-only should NOT have engine-level concurrency (user intent, top-level group is sufficient)",
},
{
name: "Claude with issues does NOT have default concurrency",
Expand All @@ -71,7 +70,7 @@ Test content`,
description: "Claude with issues trigger should NOT have default concurrency (special case)",
},
{
name: "Claude with workflow_dispatch HAS default concurrency",
name: "Claude with workflow_dispatch does NOT have default concurrency",
markdown: `---
on: workflow_dispatch
engine:
Expand All @@ -83,9 +82,8 @@ tools:

# Test workflow
Test content`,
expectedInJob: `concurrency:
group: "gh-aw-claude-${{ github.workflow }}"`,
description: "Claude with workflow_dispatch should have default concurrency",
notExpectedInJob: `concurrency:`,
description: "Claude with workflow_dispatch-only should NOT have engine-level concurrency (user intent, top-level group is sufficient)",
},
{
name: "Custom concurrency with string format",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,6 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: read
concurrency:
group: "gh-aw-copilot-${{ github.workflow }}"
env:
GH_AW_WORKFLOW_ID_SANITIZED: basiccopilot
outputs:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,6 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: read
concurrency:
group: "gh-aw-copilot-${{ github.workflow }}"
env:
GH_AW_WORKFLOW_ID_SANITIZED: withimports
outputs:
Expand Down
Loading