Skip to content

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

Merged
pelikhan merged 4 commits into
mainfrom
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 into
mainfrom
copilot/fix-concurrency-group-issue

Conversation

CopilotAI commented Mar 9, 2026

Copy link
Copy Markdown
Contributor

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 groupconcurrency:
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 runsconcurrency:
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>
CopilotAI changed the title [WIP] Fix job-level concurrency group for workflow inputsAdd concurrency.job-discriminator to prevent fan-out cancellations in job-level concurrency groupsMar 9, 2026
@pelikhan

Copy link
Copy Markdown
Collaborator

@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>

CopilotAI commented Mar 9, 2026

Copy link
Copy Markdown
ContributorAuthor

@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
Copy Markdown
Collaborator

@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>

CopilotAI commented Mar 9, 2026

Copy link
Copy Markdown
ContributorAuthor

@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
CopilotAI review requested due to automatic review settings March 9, 2026 14:22
@pelikhan
pelikhan merged commit 9059513 into mainMar 9, 2026
110 of 111 checks passed
@pelikhan
pelikhan deleted the copilot/fix-concurrency-group-issue branch March 9, 2026 14:26

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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
FileDescription
pkg/workflow/concurrency.goAppends ConcurrencyJobDiscriminator to default job-level concurrency group generation.
pkg/workflow/concurrency_test.goAdds test cases for discriminator behavior across triggers and engines.
pkg/workflow/compiler_types.goAdds ConcurrencyJobDiscriminator to WorkflowData.
pkg/workflow/compiler_orchestrator_workflow.goExtracts job-discriminator from raw frontmatter and strips it from serialized workflow-level concurrency YAML.
pkg/workflow/compiler_orchestrator_workflow_test.goAdds tests for discriminator extraction and stripping behavior.
pkg/workflow/compiler.goValidates concurrency.job-discriminator expressions during compilation.
pkg/parser/schemas/main_workflow_schema.jsonExtends concurrency schema with job-discriminator and makes group optional.
.github/workflows/stale-repo-identifier.mdUses concurrency.job-discriminator to avoid fan-out cancellations.
.github/workflows/stale-repo-identifier.lock.ymlReflects updated compiled job concurrency group with discriminator appended.
.github/workflows/slide-deck-maintainer.mdUses concurrency.job-discriminator to avoid fan-out cancellations.
.github/workflows/slide-deck-maintainer.lock.ymlReflects 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)

CopilotAIMar 9, 2026

Copy link

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

CopilotAIMar 9, 2026

Copy link

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

CopilotAIMar 9, 2026

Copy link

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": [

CopilotAIMar 9, 2026

Copy link

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-actionsBot 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 freeto 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

@pelikhan