Skip to content

Add concurrency.job-discriminator to prevent fan-out cancellations in job-level concurrency groups#20190

Merged
pelikhan merged 4 commits intomainfrom
copilot/fix-concurrency-group-issue
Mar 9, 2026
Merged

Add concurrency.job-discriminator to prevent fan-out cancellations in job-level concurrency groups#20190
pelikhan merged 4 commits intomainfrom
copilot/fix-concurrency-group-issue

Conversation

Copy link
Contributor

Copilot AI commented Mar 9, 2026

When multiple workflow instances are dispatched concurrently with different inputs (fan-out pattern), compiler-generated job-level concurrency groups (gh-aw-{engine}-${{ github.workflow }}) are static across all runs — causing all but 2 of N dispatched runs to be cancelled as they compete for the same slot.

Changes

  • New job-discriminator field in the concurrency frontmatter block: when set, the expression is appended to compiler-generated job-level concurrency groups (agent, output jobs), making each run's group unique
  • Schema: added job-discriminator property to the concurrency object in main_workflow_schema.json; the group field is now optional so job-discriminator can be used standalone
  • Extraction: extractConcurrencySection() strips job-discriminator from the serialized lock file YAML (it's a gh-aw extension, not valid GitHub Actions); extractConcurrencyJobDiscriminator() reads it from raw frontmatter without modifying the original map
  • Validation: job-discriminator expression is validated alongside other concurrency expressions
  • Behavior: discriminator has no effect when no default job concurrency is generated (e.g. workflow_dispatch-only, push, pull_request) or when engine provides explicit concurrency
  • Applied to existing workflows: stale-repo-identifier.md and slide-deck-maintainer.md now use job-discriminator to allow concurrent fan-out runs with different inputs

Usage

job-discriminator can be used with or without an explicit group:

# With explicit group
concurrency:
  group: gh-aw-${{ github.workflow }}-${{ inputs.finding_id }}
  job-discriminator: ${{ inputs.finding_id }}

# Without explicit group (compiler generates default workflow-level concurrency)
concurrency:
  job-discriminator: ${{ inputs.finding_id }}

# Universal uniqueness using github.run_id (for schedule workflows with no distinguishing input)
concurrency:
  job-discriminator: ${{ github.run_id }}

# Fallback expression: use input when dispatched manually, run_id for scheduled runs
concurrency:
  job-discriminator: ${{ inputs.organization || github.run_id }}

All forms compile to job-level groups that are unique per run:

jobs:
  agent:
    concurrency:
      group: "gh-aw-copilot-${{ github.workflow }}-${{ inputs.finding_id }}"  # ✅ unique per run

💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

…-level concurrency groups

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix job-level concurrency group for workflow inputs Add concurrency.job-discriminator to prevent fan-out cancellations in job-level concurrency groups Mar 9, 2026
@pelikhan
Copy link
Contributor

pelikhan commented Mar 9, 2026

@copilot make the group field non-required so that the job-discriminator can be used without having to specify the group

…minator standalone

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 9, 2026

@copilot make the group field non-required so that the job-discriminator can be used without having to specify the group

Done in 4d82b6b. Removed "required": ["group"] from the concurrency object schema, so job-discriminator can now be used standalone:

concurrency:
  job-discriminator: ${{ inputs.finding_id }}

When group is omitted, the compiler generates the default workflow-level concurrency (same as if no concurrency block were specified) while still appending the discriminator to job-level groups.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • https://api.github.com/graphql
    • Triggering command: /usr/bin/gh /usr/bin/gh api graphql -f query=query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { hasDiscussionsEnabled } } -f owner=github -f name=gh-aw -ifaceassert -nilfunc git rev-�� --show-toplevel -tests /usr/bin/git (http block)
  • https://api.github.com/repos/actions/ai-inference/git/ref/tags/v1
    • Triggering command: /usr/bin/gh gh api /repos/actions/ai-inference/git/ref/tags/v1 --jq .object.sha se 9210994/b236/vet.cfg ache/go/1.25.0/x64/pkg/tool/linux_amd64/compile (http block)
  • https://api.github.com/repos/actions/checkout/git/ref/tags/v3
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v3 --jq .object.sha (http block)
  • https://api.github.com/repos/actions/checkout/git/ref/tags/v5
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v5 --jq .object.sha (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v5 --jq .object.sha -unreachable=false /tmp/go-build1389210994/b038/vet.cfg 9210994/b311/vet.cfg (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v5 --jq .object.sha --show-toplevel (http block)
  • https://api.github.com/repos/actions/checkout/git/ref/tags/v6
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v6 --jq .object.sha se 9210994/b062/vet.cfg .cfg pull.rebase (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v6 --jq .object.sha se 9210994/b048/vet.cfg ache/go/1.25.0/x64/pkg/tool/linux_amd64/vet pull.rebase (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v6 --jq .object.sha --show-toplevel (http block)
  • https://api.github.com/repos/actions/github-script/git/ref/tags/v8
    • Triggering command: /usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v8 --jq .object.sha get --local ache/go/1.25.0/x64/pkg/tool/linux_amd64/vet user.email (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v8 --jq .object.sha se 9210994/b068/vet.cfg .cfg pull.rebase (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v8 --jq .object.sha se 9210994/b055/vet.cfg .cfg pull.rebase (http block)
  • https://api.github.com/repos/actions/setup-go/git/ref/tags/v4
    • Triggering command: /usr/bin/gh gh api /repos/actions/setup-go/git/ref/tags/v4 --jq .object.sha get --local .cfg pull.rebase (http block)
  • https://api.github.com/repos/actions/setup-node/git/ref/tags/v4
    • Triggering command: /usr/bin/gh gh api /repos/actions/setup-node/git/ref/tags/v4 --jq .object.sha se 9210994/b042/vet.cfg .cfg pull.rebase (http block)
  • https://api.github.com/repos/actions/upload-artifact/git/ref/tags/v4
    • Triggering command: /usr/bin/gh gh api /repos/actions/upload-artifact/git/ref/tags/v4 --jq .object.sha (http block)
  • https://api.github.com/repos/github/gh-aw/git/ref/tags/v1.0.0
    • Triggering command: /usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/v1.0.0 --jq .object.sha (http block)
  • https://api.github.com/repos/nonexistent/action/git/ref/tags/v999.999.999
    • Triggering command: /usr/bin/gh gh api /repos/nonexistent/action/git/ref/tags/v999.999.999 --jq .object.sha (http block)

If you need me to access, download, or install something from one of these locations, you can either:

@pelikhan
Copy link
Contributor

pelikhan commented Mar 9, 2026

@copilot add more examples in test.
Find existing agentic workflows and apply new tag (not release.md)

…fier and slide-deck-maintainer workflows

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 9, 2026

@copilot add more examples in test.
Find existing agentic workflows and apply new tag (not release.md)

Done in 7de7af0.

More test examples (pkg/workflow/concurrency_test.go): Added 4 new cases to TestGenerateJobConcurrencyConfig:

  • github.run_id discriminator (universal uniqueness for schedule workflows)
  • claude engine with ${{ inputs.organization || github.run_id }} fallback expression
  • push trigger with discriminator → no effect (special trigger, no default group)
  • pull_request trigger with discriminator → no effect

Applied to existing workflows:

  • stale-repo-identifier.md: job-discriminator: ${{ inputs.organization || github.run_id }} — allows concurrent runs for different orgs; falls back to run_id for the monthly scheduled run
  • slide-deck-maintainer.md: job-discriminator: ${{ inputs.focus || github.run_id }} — allows feature-deep-dive and global-sweep runs to proceed in parallel

@pelikhan pelikhan marked this pull request as ready for review March 9, 2026 14:22
Copilot AI review requested due to automatic review settings March 9, 2026 14:22
@pelikhan pelikhan merged commit 9059513 into main Mar 9, 2026
110 of 111 checks passed
@pelikhan pelikhan deleted the copilot/fix-concurrency-group-issue branch March 9, 2026 14:26
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a concurrency.job-discriminator frontmatter field to prevent job-level concurrency group collisions in fan-out dispatch patterns (especially for schedule workflows), while keeping compiled lock files valid GitHub Actions YAML by stripping the extension field.

Changes:

  • Introduces concurrency.job-discriminator extraction/validation and appends it to compiler-generated job-level concurrency groups.
  • Updates the main workflow JSON schema to allow job-discriminator (and makes group optional).
  • Applies the new field to existing workflows and updates lock files; adds unit tests covering extraction and group generation behavior.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
pkg/workflow/concurrency.go Appends ConcurrencyJobDiscriminator to default job-level concurrency group generation.
pkg/workflow/concurrency_test.go Adds test cases for discriminator behavior across triggers and engines.
pkg/workflow/compiler_types.go Adds ConcurrencyJobDiscriminator to WorkflowData.
pkg/workflow/compiler_orchestrator_workflow.go Extracts job-discriminator from raw frontmatter and strips it from serialized workflow-level concurrency YAML.
pkg/workflow/compiler_orchestrator_workflow_test.go Adds tests for discriminator extraction and stripping behavior.
pkg/workflow/compiler.go Validates concurrency.job-discriminator expressions during compilation.
pkg/parser/schemas/main_workflow_schema.json Extends concurrency schema with job-discriminator and makes group optional.
.github/workflows/stale-repo-identifier.md Uses concurrency.job-discriminator to avoid fan-out cancellations.
.github/workflows/stale-repo-identifier.lock.yml Reflects updated compiled job concurrency group with discriminator appended.
.github/workflows/slide-deck-maintainer.md Uses concurrency.job-discriminator to avoid fan-out cancellations.
.github/workflows/slide-deck-maintainer.lock.yml Reflects updated compiled job concurrency group with discriminator appended.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +74 to 78
if workflowData.ConcurrencyJobDiscriminator != "" {
concurrencyLog.Printf("Appending job discriminator to job-level concurrency group: %s", workflowData.ConcurrencyJobDiscriminator)
groupValue = fmt.Sprintf("%s-%s", groupValue, workflowData.ConcurrencyJobDiscriminator)
}
concurrencyConfig := fmt.Sprintf("concurrency:\n group: \"%s\"", groupValue)
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

job-discriminator can contain valid GitHub expressions that include double quotes or escaped newlines (e.g. ${{ github.event_name == "push" }}), which will break the generated YAML because the group is always wrapped in unescaped double quotes. Prefer emitting the YAML scalar using proper escaping (e.g. %q / YAML marshalling) and/or add validation to reject \n/\r in the discriminator to guarantee the compiled workflow YAML remains valid.

Copilot uses AI. Check for mistakes.
Comment on lines +295 to +303
// 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")
}
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

After stripping job-discriminator, this function can still serialize a concurrency object that has cancel-in-progress (or other fields) but no group (e.g. frontmatter concurrency: { cancel-in-progress: true, job-discriminator: ... }). GitHub Actions requires group in the object form, so this would generate invalid lock file YAML. Consider explicitly detecting cleanMap["group"] missing and returning an error (preferred) or returning empty string / dropping the remaining fields so the compiler generates a valid default concurrency group.

Copilot uses AI. Check for mistakes.
Comment on lines +192 to +196
if workflowData.ConcurrencyJobDiscriminator != "" {
if err := validateConcurrencyGroupExpression(workflowData.ConcurrencyJobDiscriminator); err != nil {
return formatCompilerError(markdownPath, "error", "concurrency.job-discriminator validation failed: "+err.Error(), err)
}
}
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

With concurrency.group no longer required by the schema, workflowData.Concurrency can now be present but contain no group (e.g. only cancel-in-progress). extractConcurrencyGroupFromYAML will return empty string, so the current validation silently accepts an invalid GitHub Actions concurrency object. Add a guard here to fail validation when workflowData.Concurrency is non-empty but no group can be extracted, and/or explicitly validate that cancel-in-progress is never set without group.

Copilot uses AI. Check for mistakes.
Comment on lines 1964 to 1967
}
},
"required": ["group"],
"required": [],
"examples": [
Copy link

Copilot AI Mar 9, 2026

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 requires group when using the object form. Update this schema to require at least one of group or job-discriminator (e.g. via anyOf with separate required clauses), and also require group whenever cancel-in-progress is present.

Copilot uses AI. Check for mistakes.
github-actions bot added a commit that referenced this pull request Mar 9, 2026
Document the new `job-discriminator` field added in #20190, which
prevents fan-out cancellations when multiple workflow instances are
dispatched concurrently with different inputs.

- Added Fan-Out Concurrency section with examples to concurrency.md
- Added job-discriminator field to frontmatter-full.md reference

Merged after today's DDUw run (14:26 UTC vs 04:28 UTC).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Job-level concurrency group ignores workflow inputs

3 participants