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
6 changes: 5 additions & 1 deletion .github/workflows/test-claude.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions .github/workflows/test-claude.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ on:
push:
branches:
- "*claude*"
pull_request:
branches:
- "*claude*"
workflow_dispatch:
engine:
id: claude
Expand Down
47 changes: 46 additions & 1 deletion docs/frontmatter.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,52 @@ concurrency:
cancel-in-progress: true
```

Defaults to single instance per workflow.
#### Enhanced Concurrency Policies

GitHub Agentic Workflows automatically generates enhanced concurrency policies based on workflow trigger types to provide better isolation and resource management. Different workflow types receive different concurrency groups and cancellation behavior:

| Trigger Type | Concurrency Group | Cancellation | Description |
|--------------|-------------------|--------------|-------------|
| `issues` | `gh-aw-${{ github.workflow }}-${{ github.event.issue.number }}` | ❌ | Issue workflows include issue number for isolation |
| `pull_request` | `gh-aw-${{ github.workflow }}-${{ github.event.pull_request.number \|\| github.ref }}` | ✅ | PR workflows include PR number with cancellation |
| `discussion` | `gh-aw-${{ github.workflow }}-${{ github.event.discussion.number }}` | ❌ | Discussion workflows include discussion number |
| Mixed issue/PR | `gh-aw-${{ github.workflow }}-${{ github.event.issue.number \|\| github.event.pull_request.number }}` | ✅ | Mixed workflows handle both contexts with cancellation |
| Alias workflows | `gh-aw-${{ github.workflow }}-${{ github.event.issue.number \|\| github.event.pull_request.number }}` | ❌ | Alias workflows handle both contexts without cancellation |
| Other triggers | `gh-aw-${{ github.workflow }}` | ❌ | Default behavior for schedule, push, etc. |

**Benefits:**
- **Better Isolation**: Workflows operating on different issues/PRs can run concurrently
- **Conflict Prevention**: No interference between unrelated workflow executions
- **Resource Management**: Pull request workflows can cancel previous runs when updated
- **Predictable Behavior**: Consistent concurrency rules based on trigger type

**Examples:**

```yaml
# Issue workflow - no cancellation, isolated by issue number
on:
issues:
types: [opened, edited]
# Generates: group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number }}"

# PR workflow - with cancellation, isolated by PR number
on:
pull_request:
types: [opened, synchronize]
# Generates: group: "gh-aw-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}"
# cancel-in-progress: true

# Mixed workflow - handles both issues and PRs with cancellation
on:
issues:
types: [opened, edited]
pull_request:
types: [opened, synchronize]
# Generates: group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number }}"
# cancel-in-progress: true
```

If you need custom concurrency behavior, you can override the automatic generation by specifying your own `concurrency` section in the frontmatter.

### Environment Variables

Expand Down
79 changes: 64 additions & 15 deletions pkg/workflow/concurrency.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package workflow

import (
"fmt"
"strings"
)

Expand All @@ -12,25 +13,73 @@ func GenerateConcurrencyConfig(workflowData *WorkflowData, isAliasTrigger bool)
return workflowData.Concurrency
}

// Generate concurrency configuration based on workflow type
// Note: Check alias trigger first since alias workflows also contain pull_request events
if isAliasTrigger {
// For alias workflows: use issue/PR number for concurrency but do NOT enable cancellation
return `concurrency:
group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number }}"`
} else if isPullRequestWorkflow(workflowData.On) {
// For PR workflows: include ref and enable cancellation
return `concurrency:
group: "gh-aw-${{ github.workflow }}-${{ github.ref }}"
cancel-in-progress: true`
} else {
// For other workflows: use static concurrency without cancellation
return `concurrency:
group: "gh-aw-${{ github.workflow }}"`
// Build concurrency group keys
keys := buildConcurrencyGroupKeys(workflowData, isAliasTrigger)
groupValue := strings.Join(keys, "-")

// Build the concurrency configuration
concurrencyConfig := fmt.Sprintf("concurrency:\n group: \"%s\"", groupValue)

// Add cancel-in-progress if appropriate
if shouldEnableCancelInProgress(workflowData, isAliasTrigger) {
concurrencyConfig += "\n cancel-in-progress: true"
}

return concurrencyConfig
}

// 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")
}

// buildConcurrencyGroupKeys builds an array of keys for the concurrency group
func buildConcurrencyGroupKeys(workflowData *WorkflowData, isAliasTrigger bool) []string {
keys := []string{"gh-aw", "${{ github.workflow }}"}

if isAliasTrigger {
// For alias workflows: use issue/PR number
keys = append(keys, "${{ github.event.issue.number || github.event.pull_request.number }}")
} else if isPullRequestWorkflow(workflowData.On) && isIssueWorkflow(workflowData.On) {
// Mixed workflows with both issue and PR triggers: use issue/PR number
keys = append(keys, "${{ github.event.issue.number || github.event.pull_request.number }}")
} else if isPullRequestWorkflow(workflowData.On) && isDiscussionWorkflow(workflowData.On) {
// Mixed workflows with PR and discussion triggers: use PR/discussion number
keys = append(keys, "${{ github.event.pull_request.number || github.event.discussion.number }}")
} else if isIssueWorkflow(workflowData.On) && isDiscussionWorkflow(workflowData.On) {
// Mixed workflows with issue and discussion triggers: use issue/discussion number
keys = append(keys, "${{ github.event.issue.number || github.event.discussion.number }}")
} else if isPullRequestWorkflow(workflowData.On) {
// Pure PR workflows: use PR number if available, otherwise fall back to ref for compatibility
keys = append(keys, "${{ github.event.pull_request.number || github.ref }}")
} else if isIssueWorkflow(workflowData.On) {
// Issue workflows: use issue number
keys = append(keys, "${{ github.event.issue.number }}")
} else if isDiscussionWorkflow(workflowData.On) {
// Discussion workflows: use discussion number
keys = append(keys, "${{ github.event.discussion.number }}")
}

return keys
}

// shouldEnableCancelInProgress determines if cancel-in-progress should be enabled
func shouldEnableCancelInProgress(workflowData *WorkflowData, isAliasTrigger bool) bool {
// Never enable cancellation for alias workflows
if isAliasTrigger {
return false
}

// Enable cancellation for pull request workflows (including mixed workflows)
return isPullRequestWorkflow(workflowData.On)
}
Loading