-
Notifications
You must be signed in to change notification settings - Fork 272
Expand file tree
/
Copy pathconcurrency.go
More file actions
230 lines (195 loc) · 9.43 KB
/
concurrency.go
File metadata and controls
230 lines (195 loc) · 9.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
package workflow
import (
"fmt"
"strings"
"github.com/github/gh-aw/pkg/logger"
)
var concurrencyLog = logger.New("workflow:concurrency")
// GenerateConcurrencyConfig generates the concurrency configuration for a workflow
// based on its trigger types and characteristics.
func GenerateConcurrencyConfig(workflowData *WorkflowData, isCommandTrigger bool) string {
concurrencyLog.Printf("Generating concurrency config: isCommandTrigger=%v", isCommandTrigger)
// Don't override if already set
if workflowData.Concurrency != "" {
concurrencyLog.Print("Using existing concurrency configuration from workflow data")
return workflowData.Concurrency
}
// Build concurrency group keys using the original workflow-specific logic
keys := buildConcurrencyGroupKeys(workflowData, isCommandTrigger)
groupValue := strings.Join(keys, "-")
concurrencyLog.Printf("Built concurrency group: %s", groupValue)
// Build the concurrency configuration
concurrencyConfig := fmt.Sprintf("concurrency:\n group: \"%s\"", groupValue)
// Add cancel-in-progress if appropriate
if shouldEnableCancelInProgress(workflowData, isCommandTrigger) {
concurrencyLog.Print("Enabling cancel-in-progress for concurrency group")
concurrencyConfig += "\n cancel-in-progress: true"
}
return concurrencyConfig
}
// GenerateJobConcurrencyConfig generates the agent concurrency configuration
// for the agent job based on engine.concurrency field
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 != "" {
concurrencyLog.Print("Using engine-configured concurrency")
return workflowData.EngineConfig.Concurrency
}
// 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 remaining generic triggers like schedule, 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,
// slash_command, or workflow_dispatch-only)
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
}
// Check for slash_command triggers (synthetic event that expands to issue_comment + workflow_dispatch)
if isSlashCommandWorkflow(on) {
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 other generic triggers (e.g. schedule) will get default concurrency
return false
}
// isPullRequestWorkflow checks if a workflow's "on" section contains pull_request triggers
func isPullRequestWorkflow(on string) bool {
return strings.Contains(on, "pull_request")
}
// isIssueWorkflow checks if a workflow's "on" section contains issue-related triggers
func isIssueWorkflow(on string) bool {
return strings.Contains(on, "issues") || strings.Contains(on, "issue_comment")
}
// isDiscussionWorkflow checks if a workflow's "on" section contains discussion-related triggers
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.
// It handles both rendered YAML (standard GitHub Actions events) and input YAML
// (which may contain synthetic events like slash_command before they are expanded).
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).
// slash_command is included here because it is a synthetic event that expands
// to issue_comment + workflow_dispatch at compile time; its presence means the
// workflow is not triggered solely by explicit user dispatch.
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",
"slash_command",
}
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
}
}
return true
}
// isPushWorkflow checks if a workflow's "on" section contains push triggers
func isPushWorkflow(on string) bool {
return strings.Contains(on, "push")
}
// isSlashCommandWorkflow checks if a workflow's "on" section contains the slash_command
// synthetic trigger. slash_command is an input-level event that expands to
// issue_comment + workflow_dispatch at compile time. Detecting it here allows
// the concurrency helpers to produce correct results even when they are called
// with the pre-rendered "on" YAML (before the event expansion has taken place).
func isSlashCommandWorkflow(on string) bool {
return strings.Contains(on, "slash_command")
}
// buildConcurrencyGroupKeys builds an array of keys for the concurrency group
func buildConcurrencyGroupKeys(workflowData *WorkflowData, isCommandTrigger bool) []string {
keys := []string{"gh-aw", "${{ github.workflow }}"}
if isCommandTrigger || isSlashCommandWorkflow(workflowData.On) {
// For command/slash_command workflows: use issue/PR number; fall back to run_id when
// neither is available (e.g. manual workflow_dispatch of the outer workflow).
keys = append(keys, "${{ github.event.issue.number || github.event.pull_request.number || github.run_id }}")
} else if isPullRequestWorkflow(workflowData.On) && isIssueWorkflow(workflowData.On) {
// Mixed workflows with both issue and PR triggers
keys = append(keys, "${{ github.event.issue.number || github.event.pull_request.number || github.run_id }}")
} else if isPullRequestWorkflow(workflowData.On) && isDiscussionWorkflow(workflowData.On) {
// Mixed workflows with PR and discussion triggers
keys = append(keys, "${{ github.event.pull_request.number || github.event.discussion.number || github.run_id }}")
} else if isIssueWorkflow(workflowData.On) && isDiscussionWorkflow(workflowData.On) {
// Mixed workflows with issue and discussion triggers
keys = append(keys, "${{ github.event.issue.number || github.event.discussion.number || github.run_id }}")
} else if isPullRequestWorkflow(workflowData.On) {
// PR workflows: use PR number, fall back to ref then run_id
keys = append(keys, "${{ github.event.pull_request.number || github.ref || github.run_id }}")
} else if isIssueWorkflow(workflowData.On) {
// Issue workflows: run_id is the fallback when no issue context is available
// (e.g. when a mixed-trigger workflow is started via workflow_dispatch).
keys = append(keys, "${{ github.event.issue.number || github.run_id }}")
} else if isDiscussionWorkflow(workflowData.On) {
// Discussion workflows: run_id is the fallback when no discussion context is available.
keys = append(keys, "${{ github.event.discussion.number || github.run_id }}")
} else if isPushWorkflow(workflowData.On) {
// Push workflows: use ref to differentiate between branches
keys = append(keys, "${{ github.ref || github.run_id }}")
}
return keys
}
// shouldEnableCancelInProgress determines if cancel-in-progress should be enabled
func shouldEnableCancelInProgress(workflowData *WorkflowData, isCommandTrigger bool) bool {
// Never enable cancellation for command workflows
if isCommandTrigger {
return false
}
// Enable cancellation for pull request workflows (including mixed workflows)
return isPullRequestWorkflow(workflowData.On)
}