buildConcurrencyGroupKeys builds an array of keys for the concurrency group
(workflowData *WorkflowData, isCommandTrigger bool)
| 228 | |
| 229 | // buildConcurrencyGroupKeys builds an array of keys for the concurrency group |
| 230 | func buildConcurrencyGroupKeys(workflowData *WorkflowData, isCommandTrigger bool) []string { |
| 231 | keys := []string{"gh-aw", "${{ github.workflow }}"} |
| 232 | |
| 233 | // Whether this workflow exposes inputs.item_number via workflow_dispatch (label trigger shorthand). |
| 234 | // When true, include it in the concurrency key so that manual dispatches for different items |
| 235 | // use distinct groups and don't cancel each other. |
| 236 | hasItemNumber := workflowData.HasDispatchItemNumber |
| 237 | |
| 238 | // Detect whether the workflow is at risk of bot-self-cancellation. |
| 239 | // When safe-outputs uses a GitHub App token AND the workflow has issue_comment triggers, |
| 240 | // App-authored comments can re-trigger the workflow and collide with the primary run's |
| 241 | // concurrency group. When this risk is present we use botIsolatedConcurrencyKey so that |
| 242 | // bot-triggered runs always resolve to a unique github.run_id key instead. |
| 243 | botRisk := hasBotSelfCancelRisk(workflowData) |
| 244 | if botRisk { |
| 245 | concurrencyLog.Print("Bot self-cancel risk detected: applying bot-actor isolation to concurrency group key") |
| 246 | } |
| 247 | |
| 248 | // entityKey selects the correct concurrency key builder based on bot risk. |
| 249 | // It captures both botRisk and hasItemNumber from the outer scope, so call |
| 250 | // sites only need to supply the entity-specific parts. |
| 251 | entityKey := func(primaryParts []string, tailParts []string) string { |
| 252 | if botRisk { |
| 253 | return botIsolatedConcurrencyKey(primaryParts, tailParts, hasItemNumber) |
| 254 | } |
| 255 | return entityConcurrencyKey(primaryParts, tailParts, hasItemNumber) |
| 256 | } |
| 257 | |
| 258 | if isCommandTrigger || isSlashCommandWorkflow(workflowData.On) { |
| 259 | // For command/slash_command workflows: use issue/PR number; fall back to run_id when |
| 260 | // neither is available (e.g. manual workflow_dispatch of the outer workflow). |
| 261 | // When bot risk is detected, prepend the bot-actor isolation check. |
| 262 | concurrencyLog.Print("Building concurrency key for command/slash_command workflow") |
| 263 | if botRisk { |
| 264 | keys = append(keys, "${{ contains(github.actor, '[bot]') && github.run_id || github.event.issue.number || github.event.pull_request.number || github.run_id }}") |
| 265 | } else { |
| 266 | keys = append(keys, "${{ github.event.issue.number || github.event.pull_request.number || github.run_id }}") |
| 267 | } |
| 268 | } else if isPullRequestWorkflow(workflowData.On) && isIssueWorkflow(workflowData.On) { |
| 269 | // Mixed workflows with both issue and PR triggers |
| 270 | concurrencyLog.Print("Building concurrency key for mixed PR+issue workflow") |
| 271 | keys = append(keys, entityKey( |
| 272 | []string{"github.event.issue.number", "github.event.pull_request.number"}, |
| 273 | []string{"github.run_id"}, |
| 274 | )) |
| 275 | } else if isPullRequestWorkflow(workflowData.On) && isDiscussionWorkflow(workflowData.On) { |
| 276 | // Mixed workflows with PR and discussion triggers |
| 277 | keys = append(keys, entityConcurrencyKey( |
| 278 | []string{"github.event.pull_request.number", "github.event.discussion.number"}, |
| 279 | []string{"github.run_id"}, |
| 280 | hasItemNumber, |
| 281 | )) |
| 282 | } else if isIssueWorkflow(workflowData.On) && isDiscussionWorkflow(workflowData.On) { |
| 283 | // Mixed workflows with issue and discussion triggers |
| 284 | keys = append(keys, entityKey( |
| 285 | []string{"github.event.issue.number", "github.event.discussion.number"}, |
| 286 | []string{"github.run_id"}, |
| 287 | )) |