This repository demonstrates the difference between workflow-level and job-level concurrency in GitHub Actions when using matrix strategies with environment protection rules.
When using a matrix strategy to deploy to multiple environments (dev → test → prod) with environment protection rules requiring manual approval, workflow-level concurrency can cause unexpected blocking behavior:
- First workflow runs, deploys to
dev, waits fortestapproval - Second workflow run gets queued and shows as "pending" because the first workflow is still active
- The entire second workflow is blocked, even though
devdeployment could proceed independently
This repo uses three GitHub environments:
- env_dev - No protection rules (auto-deploys)
- env_test - Manual approval required
- env_prod - Manual approval required
-
workflow-concurrency.yml - Current problematic setup
- Uses workflow-level concurrency:
group: build-${{ github.head_ref || github.ref_name }} - All jobs share the same concurrency group
- Uses workflow-level concurrency:
-
job-concurrency.yml - Proposed solution
- Uses job-level concurrency:
group: deploy-${{ matrix.environment }}-${{ github.head_ref || github.ref_name }} - Each environment has its own concurrency group
- Uses job-level concurrency:
- Set up environment protection rules:
- Go to repo Settings → Environments
- For
env_testandenv_prod: Enable "Required reviewers" and add yourself
- Disable the job-concurrency workflow (add
#beforename:) - Make a change to
test-file.txtand push to main - Observe the workflow run:
devdeployment completestestdeployment waits for approval (yellow clock icon)
- While the first workflow is still pending approval, make another change to
test-file.txtand push - Expected Issue: The second workflow run will show as "pending" and won't start until the first completes
- Disable the workflow-concurrency workflow
- Enable the job-concurrency workflow
- Repeat the same test steps as above
- Expected Result:
- First workflow:
devcompletes,testwaits for approval - Second workflow: New
devdeployment starts immediately,testwaits in its own queue
- First workflow:
| Scenario | Workflow-Level Concurrency | Job-Level Concurrency |
|---|---|---|
| First push | ✅ dev deploys, test waits | ✅ dev deploys, test waits |
| Second push while test pending | ❌ Entire workflow queued | ✅ dev deploys immediately, test queued per environment |
| Third push | ❌ Still queued | ✅ dev replaces previous pending, test maintains queue |
- Pro: Simple configuration
- Con: Entire workflow shares concurrency group
- Issue: One pending job blocks all subsequent workflow runs
- Pro: Fine-grained control per environment
- Pro: Independent concurrency groups per deployment target
- Result: Environments can deploy independently while respecting their own approval workflows
.github/workflows/workflow-concurrency.yml- Demonstrates the problem.github/workflows/job-concurrency.yml- Demonstrates the solutiontest-file.txt- Simple file to modify for triggering workflowsREADME.md- This documentation
concurrency:
group: build-main
cancel-in-progress: falseAll jobs in workflow share this group.
concurrency:
group: deploy-dev-main # dev environment
group: deploy-test-main # test environment
group: deploy-prod-main # prod environmentEach environment deployment has independent concurrency control.
Job-level concurrency provides the granular control needed for multi-environment deployments with approval workflows, preventing blocking behavior while maintaining proper deployment sequencing per environment.