-
Notifications
You must be signed in to change notification settings - Fork 295
Add concurrency.job-discriminator to prevent fan-out cancellations in job-level concurrency groups
#20190
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add concurrency.job-discriminator to prevent fan-out cancellations in job-level concurrency groups
#20190
Changes from all commits
be14d22
bbaaf14
4d82b6b
7de7af0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -188,6 +188,13 @@ func (c *Compiler) validateWorkflowData(workflowData *WorkflowData, markdownPath | |
| } | ||
| } | ||
|
|
||
| // Validate concurrency.job-discriminator expression | ||
| if workflowData.ConcurrencyJobDiscriminator != "" { | ||
| if err := validateConcurrencyGroupExpression(workflowData.ConcurrencyJobDiscriminator); err != nil { | ||
| return formatCompilerError(markdownPath, "error", "concurrency.job-discriminator validation failed: "+err.Error(), err) | ||
| } | ||
| } | ||
|
Comment on lines
+192
to
+196
|
||
|
|
||
| // Validate engine-level concurrency group expression | ||
| log.Printf("Validating engine-level concurrency configuration") | ||
| if workflowData.EngineConfig != nil && workflowData.EngineConfig.Concurrency != "" { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -224,7 +224,8 @@ func (c *Compiler) extractYAMLSections(frontmatter map[string]any, workflowData | |
| workflowData.HasDispatchItemNumber = extractDispatchItemNumber(frontmatter) | ||
| workflowData.Permissions = c.extractPermissions(frontmatter) | ||
| workflowData.Network = c.extractTopLevelYAMLSection(frontmatter, "network") | ||
| workflowData.Concurrency = c.extractTopLevelYAMLSection(frontmatter, "concurrency") | ||
| workflowData.ConcurrencyJobDiscriminator = extractConcurrencyJobDiscriminator(frontmatter) | ||
| workflowData.Concurrency = c.extractConcurrencySection(frontmatter) | ||
| workflowData.RunName = c.extractTopLevelYAMLSection(frontmatter, "run-name") | ||
| workflowData.Env = c.extractTopLevelYAMLSection(frontmatter, "env") | ||
| workflowData.Features = c.extractFeatures(frontmatter) | ||
|
|
@@ -239,6 +240,68 @@ func (c *Compiler) extractYAMLSections(frontmatter map[string]any, workflowData | |
| workflowData.Cache = c.extractTopLevelYAMLSection(frontmatter, "cache") | ||
| } | ||
|
|
||
| // extractConcurrencyJobDiscriminator reads the job-discriminator value from the | ||
| // frontmatter concurrency block without modifying the original map. | ||
| // Returns the discriminator expression string or empty string if not present. | ||
| func extractConcurrencyJobDiscriminator(frontmatter map[string]any) string { | ||
| concurrencyRaw, ok := frontmatter["concurrency"] | ||
| if !ok { | ||
| return "" | ||
| } | ||
| concurrencyMap, ok := concurrencyRaw.(map[string]any) | ||
| if !ok { | ||
| return "" | ||
| } | ||
| discriminator, ok := concurrencyMap["job-discriminator"] | ||
| if !ok { | ||
| return "" | ||
| } | ||
| discriminatorStr, ok := discriminator.(string) | ||
| if !ok { | ||
| return "" | ||
| } | ||
| return discriminatorStr | ||
| } | ||
|
|
||
| // extractConcurrencySection extracts the workflow-level concurrency YAML section, | ||
| // stripping the gh-aw-specific job-discriminator field so it does not appear in | ||
| // the compiled lock file (which must be valid GitHub Actions YAML). | ||
| func (c *Compiler) extractConcurrencySection(frontmatter map[string]any) string { | ||
| concurrencyRaw, ok := frontmatter["concurrency"] | ||
| if !ok { | ||
| return "" | ||
| } | ||
| concurrencyMap, ok := concurrencyRaw.(map[string]any) | ||
| if !ok || len(concurrencyMap) == 0 { | ||
| // String or empty format: serialize as-is (no job-discriminator possible) | ||
| return c.extractTopLevelYAMLSection(frontmatter, "concurrency") | ||
| } | ||
|
|
||
| _, hasDiscriminator := concurrencyMap["job-discriminator"] | ||
| if !hasDiscriminator { | ||
| return c.extractTopLevelYAMLSection(frontmatter, "concurrency") | ||
| } | ||
|
|
||
| // Build a copy of the concurrency map without job-discriminator for serialization. | ||
| // Use len(concurrencyMap) for capacity: at most one entry (job-discriminator) will be | ||
| // omitted, so this is a slight over-allocation that avoids a subtle negative-capacity | ||
| // edge case if job-discriminator were the only key. | ||
| cleanMap := make(map[string]any, len(concurrencyMap)) | ||
| for k, v := range concurrencyMap { | ||
| if k != "job-discriminator" { | ||
| cleanMap[k] = v | ||
| } | ||
| } | ||
| // When job-discriminator is the only field, there is no user-specified workflow-level | ||
| // group to emit; return empty so the compiler can generate the default concurrency. | ||
| if len(cleanMap) == 0 { | ||
| return "" | ||
| } | ||
| // Use a minimal temporary frontmatter containing only the concurrency key to avoid | ||
| // copying the entire (potentially large) frontmatter map. | ||
| return c.extractTopLevelYAMLSection(map[string]any{"concurrency": cleanMap}, "concurrency") | ||
| } | ||
|
Comment on lines
+295
to
+303
|
||
|
|
||
| // extractDispatchItemNumber reports whether the frontmatter's on.workflow_dispatch | ||
| // trigger exposes an item_number input. This is the signature produced by the label | ||
| // trigger shorthand (e.g. "on: pull_request labeled my-label"). Reading the | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Setting
required: []allows invalid concurrency objects like{}or{ "cancel-in-progress": true }to pass schema validation, but GitHub Actions requiresgroupwhen using the object form. Update this schema to require at least one ofgrouporjob-discriminator(e.g. viaanyOfwith separaterequiredclauses), and also requiregroupwhenevercancel-in-progressis present.