This GitHub Action validates that the concurrency usage across workflows in a repository does not exceed a specified limit. It helps prevent abuse of GitHub's concurrent job limits by analyzing workflow files and detecting potential parallel execution.
- Implicit concurrency from jobs that run in parallel (no dependencies)
- Matrix job combinations
- Jobs within the same workflow run that can execute in parallel
- Dependencies between jobs (
needs:
) are properly analyzed to identify truly parallel execution paths - Matrix jobs are counted by their total number of combinations
- All jobs at the same dependency level will be counted towards parallel execution
This action uses a bundled approach for dependencies, which makes it more reliable and faster.
You can use this tool in two ways:
- As a GitHub Action directly in your workflow
- As a reusable workflow that can be called from other workflows
Add the following to your workflow:
jobs:
validate-concurrency:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Validate Workflow Concurrency
uses: homeles/workflow-concurrency-validator@v1
with:
max-concurrency: '10' # Optional, defaults to 10
workflow-path: '.github/workflows' # Optional
fail-on-error: 'true' # Optional
comment-on-pr: 'true' # Optional
Create a file .github/workflows/validate-concurrency.yml
:
name: Validate Workflow Concurrency
on:
pull_request:
paths:
- '.github/workflows/**'
push:
branches:
- main
paths:
- '.github/workflows/**'
jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Validate Workflow Concurrency
uses: homeles/workflow-concurrency-validator@v1
with:
max-concurrency: '10'
comment-on-pr: 'true'
You can also set this up as a reusable workflow that can be called from other workflows.
First, create a file .github/workflows/concurrency-validator-reusable.yml
in your repository:
name: Reusable Concurrency Validator
on:
workflow_call:
inputs:
max-concurrency:
description: 'Maximum allowed concurrency across all workflows'
required: false
type: number
default: 10
workflow-path:
description: 'Path to the workflows directory'
required: false
type: string
default: '.github/workflows'
fail-on-error:
description: 'Whether to fail the workflow if validation fails'
required: false
type: boolean
default: true
comment-on-pr:
description: 'Whether to comment on PR if validation fails'
required: false
type: boolean
default: true
outputs:
total-concurrency:
description: 'Total concurrency detected across workflows'
value: ${{ jobs.validate.outputs.total-concurrency }}
validation-passed:
description: 'Whether validation passed'
value: ${{ jobs.validate.outputs.validation-passed }}
secrets:
token:
description: 'GitHub token for commenting on PRs'
required: false
jobs:
validate:
runs-on: ubuntu-latest
outputs:
total-concurrency: ${{ steps.validate.outputs.total_concurrency }}
validation-passed: ${{ steps.validate.outputs.validation_passed }}
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Validate Workflow Concurrency
id: validate
uses: homeles/workflow-concurrency-validator@v1
with:
max-concurrency: ${{ inputs.max-concurrency }}
workflow-path: ${{ inputs.workflow-path }}
fail-on-error: ${{ inputs.fail-on-error }}
comment-on-pr: ${{ inputs.comment-on-pr }}
token: ${{ secrets.token || github.token }}
Then, you can call this workflow from another workflow:
name: PR Checks
on:
pull_request:
paths:
- '.github/workflows/**'
jobs:
validate-concurrency:
uses: ./.github/workflows/concurrency-validator-reusable.yml
with:
max-concurrency: 10
comment-on-pr: true
secrets:
token: ${{ secrets.GITHUB_TOKEN }}
# Add other jobs that depend on the validation result
additional-checks:
needs: validate-concurrency
if: needs.validate-concurrency.outputs.validation-passed == 'true'
runs-on: ubuntu-latest
steps:
- name: Run additional checks
run: echo "Running additional checks because concurrency validation passed"
Input | Description | Required | Default |
---|---|---|---|
max-concurrency |
Maximum allowed concurrency across all workflows | No | 10 |
workflow-path |
Path to the workflows directory | No | .github/workflows |
fail-on-error |
Whether to fail the action if validation fails | No | true |
comment-on-pr |
Whether to comment on PR if validation fails | No | true |
token |
GitHub token for commenting on PRs | No | ${{ github.token }} |
Output | Description |
---|---|
total-concurrency |
Total concurrency detected across workflows |
validation-passed |
Whether validation passed (true or false ) |
issues |
JSON array of issues found during validation |
details |
JSON object with detailed information about concurrency usage |
This action is written in TypeScript and uses @vercel/ncc to bundle all dependencies into a single file.
- Install dependencies:
npm install
- Install development dependencies:
npm install --save-dev @vercel/ncc
- Make changes to files in the
src/
directory - Run type checking:
npm run type-check
- Run tests:
npm test
- Build the bundled version:
npm run build
- Commit your changes, including the updated
dist/index.js
file
The action's core functionality is implemented with the following TypeScript interfaces:
WorkflowConcurrency
: Defines the structure of concurrency settingsWorkflowJob
: Represents a job in a workflow fileWorkflowFile
: Describes the overall workflow file structureConcurrencyDetail
: Contains detailed information about concurrency usage
The action provides detailed output in JSON format. Here are examples of the output structure:
[
{
"file": ".github/workflows/build.yml",
"level": "workflow",
"type": "standard",
"counted": true
},
{
"file": ".github/workflows/test.yml",
"level": "workflow",
"type": "cancel-in-progress",
"counted": false,
"note": "Only affects concurrent workflow runs"
},
{
"file": ".github/workflows/matrix.yml",
"level": "implicit",
"jobs": ["test"],
"count": 6,
"counted": true,
"note": "Matrix job with 6 combinations"
},
{
"file": ".github/workflows/deploy.yml",
"level": "implicit",
"jobs": ["deploy-staging", "deploy-prod"],
"count": 2,
"counted": true,
"note": "Jobs running in parallel"
}
]
[
"Error processing .github/workflows/invalid.yml: Unexpected token in YAML",
"Total concurrency (12) exceeds maximum allowed (10)"
]
You can test the action with various workflow patterns. The repository includes several example workflow files that demonstrate different concurrency patterns:
- Simple parallel jobs
- Jobs with dependencies
- Workflow-level concurrency
- Job-level concurrency
- Matrix workflows
- Cancel-in-progress workflows
Place these test files in your .github/workflows/
directory to test the validation logic.
To prevent merging PRs that would violate the concurrency limit, set up a branch protection rule that requires the validation check to pass.
You can enhance the action by adding notifications to Slack or Teams when validation fails:
- name: Notify Slack
if: steps.validate.outputs.validation_passed == 'false'
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "⚠️ Workflow concurrency validation failed: ${{ steps.validate.outputs.total_concurrency }} exceeds limit of ${{ inputs.max-concurrency }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
This action uses a bundled approach to avoid dependency issues. If you encounter any problems:
- Make sure you're using the latest version of the action
- Check that the
dist/index.js
file is included in your repository - Verify the Node.js version in your workflow (Node.js 16+ is recommended)
The action uses the modern GitHub Actions output method with environment files. If you're seeing warnings about deprecated commands, make sure you're using the latest version of the action.
- Check the TypeScript configuration in
tsconfig.json
- Run
npm run type-check
to verify types - Ensure all imported modules have proper type definitions
- Check the
lib
directory for compiled JavaScript files
The action detects concurrency in several ways:
-
Implicit parallel jobs: Jobs that can run in parallel based on:
- No
needs:
dependencies between them - Being in the same dependency level Example:
jobs: build: runs-on: ubuntu-latest steps: [...] test: runs-on: ubuntu-latest steps: [...] deploy: needs: [build, test] runs-on: ubuntu-latest steps: [...]
Here,
build
andtest
can run in parallel (count: 2), whiledeploy
runs after them (different level) - No
-
Matrix jobs: Count based on total combinations
jobs: test: strategy: matrix: os: [ubuntu, windows, macos] node: [14, 16] runs-on: ${{ matrix.os }} steps: [...]
This counts as 6 concurrent jobs (3 OS × 2 Node.js versions)
-
Simple workflow with two parallel jobs:
jobs: job1: runs-on: ubuntu-latest job2: runs-on: ubuntu-latest
Total concurrency: 2 (both jobs can run in parallel)
-
Matrix job with dependencies:
jobs: test: strategy: matrix: os: [ubuntu, windows] node: [14, 16] deploy: needs: [test]
Total concurrency: 4 (2 OS × 2 Node.js versions in parallel, deploy runs after)
Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License - see the LICENSE file for details.