Skip to content

Commit 4a07b4a

Browse files
committed
- Changing the concurrency limits to be defined by workflow file
- Adding tests
1 parent 62f26aa commit 4a07b4a

File tree

11 files changed

+6312
-481
lines changed

11 files changed

+6312
-481
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
node_modules/
33
jspm_packages/
44

5+
# Build artifacts
6+
lib/
7+
58
# Optional npm cache directory
69
.npm
710

README.md

Lines changed: 171 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
# Workflow Concurrency Validator
2-
32
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.
43

54
## What it checks
6-
75
- Top-level concurrency settings in workflows
86
- Job-level concurrency settings
97
- Implicit concurrency from jobs that run in parallel (no dependencies)
10-
- Respects `cancel-in-progress: true` which doesn't count toward concurrency limits
8+
- Matrix job combinations
9+
- Jobs within the same workflow run that can execute in parallel
10+
11+
## Important Notes
12+
- Jobs within the same workflow run can execute in parallel even with `cancel-in-progress: true`
13+
- `cancel-in-progress` only affects concurrent workflow runs, not jobs within the same workflow
14+
- Matrix jobs are counted by their total number of combinations
15+
- Dependencies between jobs (`needs:`) are properly analyzed to identify truly parallel execution paths
1116

1217
## Installation
1318

@@ -189,28 +194,87 @@ jobs:
189194
| `details` | JSON object with detailed information about concurrency usage |
190195

191196
## Development and Building
197+
This action is written in TypeScript and uses [@vercel/ncc](https://github.com/vercel/ncc) to bundle all dependencies into a single file.
192198

193-
This action uses [@vercel/ncc](https://github.com/vercel/ncc) to bundle all dependencies into a single file. If you make changes to the action, follow these steps to rebuild it:
194-
199+
### Setting Up Development Environment
195200
1. Install dependencies:
196201
```bash
197202
npm install
198203
```
199-
200204
2. Install development dependencies:
201205
```bash
202206
npm install --save-dev @vercel/ncc
203207
```
204208

205-
3. Make your changes to `validate-concurrency.js`
206-
209+
### Development Workflow
210+
1. Make changes to files in the `src/` directory
211+
2. Run type checking:
212+
```bash
213+
npm run type-check
214+
```
215+
3. Run tests:
216+
```bash
217+
npm test
218+
```
207219
4. Build the bundled version:
208220
```bash
209221
npm run build
210222
```
211-
212223
5. Commit your changes, including the updated `dist/index.js` file
213224

225+
### TypeScript Implementation Details
226+
The action's core functionality is implemented with the following TypeScript interfaces:
227+
- `WorkflowConcurrency`: Defines the structure of concurrency settings
228+
- `WorkflowJob`: Represents a job in a workflow file
229+
- `WorkflowFile`: Describes the overall workflow file structure
230+
- `ConcurrencyDetail`: Contains detailed information about concurrency usage
231+
232+
### Output Format Examples
233+
The action provides detailed output in JSON format. Here are examples of the output structure:
234+
235+
#### Details Output
236+
```json
237+
[
238+
{
239+
"file": ".github/workflows/build.yml",
240+
"level": "workflow",
241+
"type": "standard",
242+
"counted": true
243+
},
244+
{
245+
"file": ".github/workflows/test.yml",
246+
"level": "workflow",
247+
"type": "cancel-in-progress",
248+
"counted": false,
249+
"note": "Only affects concurrent workflow runs"
250+
},
251+
{
252+
"file": ".github/workflows/matrix.yml",
253+
"level": "implicit",
254+
"jobs": ["test"],
255+
"count": 6,
256+
"counted": true,
257+
"note": "Matrix job with 6 combinations"
258+
},
259+
{
260+
"file": ".github/workflows/deploy.yml",
261+
"level": "implicit",
262+
"jobs": ["deploy-staging", "deploy-prod"],
263+
"count": 2,
264+
"counted": true,
265+
"note": "Jobs running in parallel"
266+
}
267+
]
268+
```
269+
270+
### Issues Output
271+
```json
272+
[
273+
"Error processing .github/workflows/invalid.yml: Unexpected token in YAML",
274+
"Total concurrency (12) exceeds maximum allowed (10)"
275+
]
276+
```
277+
214278
## Testing
215279

216280
You can test the action with various workflow patterns. The repository includes several example workflow files that demonstrate different concurrency patterns:
@@ -261,6 +325,104 @@ This action uses a bundled approach to avoid dependency issues. If you encounter
261325

262326
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.
263327

328+
### Debugging TypeScript Issues
329+
1. Check the TypeScript configuration in `tsconfig.json`
330+
2. Run `npm run type-check` to verify types
331+
3. Ensure all imported modules have proper type definitions
332+
4. Check the `lib` directory for compiled JavaScript files
333+
334+
### Understanding Concurrency Detection
335+
The action detects concurrency in several ways:
336+
337+
1. **Explicit workflow-level concurrency**: Set via `concurrency:` at the workflow root
338+
- Simple string format: `concurrency: group1`
339+
- Object format with cancel-in-progress:
340+
```yaml
341+
concurrency:
342+
group: build-group
343+
cancel-in-progress: true
344+
```
345+
Note: cancel-in-progress only affects concurrent workflow runs, not jobs within the same workflow
346+
347+
2. **Explicit job-level concurrency**: Set via `concurrency:` in individual jobs
348+
```yaml
349+
jobs:
350+
job1:
351+
concurrency: group1
352+
job2:
353+
concurrency: group2
354+
```
355+
356+
3. **Implicit parallel jobs**: Jobs that can run in parallel based on:
357+
- No `needs:` dependencies between them
358+
- Being in the same dependency level
359+
Example:
360+
```yaml
361+
jobs:
362+
build:
363+
runs-on: ubuntu-latest
364+
steps: [...]
365+
test:
366+
runs-on: ubuntu-latest
367+
steps: [...]
368+
deploy:
369+
needs: [build, test]
370+
runs-on: ubuntu-latest
371+
steps: [...]
372+
```
373+
Here, `build` and `test` can run in parallel (count: 2), while `deploy` runs after them (different level)
374+
375+
4. **Matrix jobs**: Count based on total combinations
376+
```yaml
377+
jobs:
378+
test:
379+
strategy:
380+
matrix:
381+
os: [ubuntu, windows, macos]
382+
node: [14, 16]
383+
runs-on: ${{ matrix.os }}
384+
steps: [...]
385+
```
386+
This counts as 6 concurrent jobs (3 OS × 2 Node.js versions)
387+
388+
#### Example Concurrency Calculations
389+
390+
1. Simple workflow with two parallel jobs:
391+
```yaml
392+
jobs:
393+
job1:
394+
runs-on: ubuntu-latest
395+
job2:
396+
runs-on: ubuntu-latest
397+
```
398+
Total concurrency: 2 (both jobs can run in parallel)
399+
400+
2. Workflow with cancel-in-progress and parallel jobs:
401+
```yaml
402+
concurrency:
403+
group: build
404+
cancel-in-progress: true
405+
jobs:
406+
job1:
407+
runs-on: ubuntu-latest
408+
job2:
409+
runs-on: ubuntu-latest
410+
```
411+
Total concurrency: 2 (jobs can still run in parallel within the same workflow)
412+
413+
3. Matrix job with dependencies:
414+
```yaml
415+
jobs:
416+
test:
417+
strategy:
418+
matrix:
419+
os: [ubuntu, windows]
420+
node: [14, 16]
421+
deploy:
422+
needs: [test]
423+
```
424+
Total concurrency: 4 (2 OS × 2 Node.js versions in parallel, deploy runs after)
425+
264426
## How to Contribute
265427

266428
Contributions are welcome! Please feel free to submit a Pull Request.

action.yml

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,20 @@ inputs:
2424
required: false
2525
default: ${{ github.token }}
2626

27+
permissions:
28+
pull-requests: write
29+
contents: read
30+
2731
outputs:
2832
total-concurrency:
2933
description: 'Total concurrency detected across workflows'
3034
value: ${{ steps.validate.outputs.total_concurrency }}
3135
validation-passed:
3236
description: 'Whether validation passed'
3337
value: ${{ steps.validate.outputs.validation_passed }}
38+
validation-result:
39+
description: 'Detailed validation result including issues'
40+
value: ${{ steps.validate.outputs.validation_result }}
3441
issues:
3542
description: 'Issues found during validation'
3643
value: ${{ steps.validate.outputs.issues }}
@@ -51,31 +58,59 @@ runs:
5158
INPUT_MAX_CONCURRENCY: ${{ inputs.max-concurrency }}
5259
INPUT_WORKFLOW_PATH: ${{ inputs.workflow-path }}
5360
INPUT_FAIL_ON_ERROR: ${{ inputs.fail-on-error }}
61+
INPUT_COMMENT_ON_PR: ${{ inputs.comment-on-pr }}
5462
GITHUB_WORKSPACE: ${{ github.workspace }}
63+
GITHUB_TOKEN: ${{ inputs.token }}
64+
GITHUB_EVENT_NAME: ${{ github.event_name }}
65+
66+
- name: Debug PR comment conditions
67+
if: always()
68+
shell: bash
69+
run: |
70+
echo "Debug PR comment conditions:"
71+
echo "comment-on-pr input: ${{ inputs.comment-on-pr }}"
72+
echo "INPUT_COMMENT_ON_PR env: $INPUT_COMMENT_ON_PR"
73+
echo "validation_passed: ${{ steps.validate.outputs.validation_passed }}"
74+
echo "github.event_name: ${{ github.event_name }}"
75+
if [ -f "$GITHUB_OUTPUT" ]; then
76+
echo "Contents of GITHUB_OUTPUT:"
77+
cat "$GITHUB_OUTPUT"
78+
else
79+
echo "No GITHUB_OUTPUT file found"
80+
fi
5581
5682
- name: Comment on PR if validation fails
57-
if: inputs.comment-on-pr == 'true' && steps.validate.outputs.validation_passed == 'false' && github.event_name == 'pull_request'
83+
if: ${{ always() && inputs.comment-on-pr == 'true' && steps.validate.outputs.validation_passed == 'false' && github.event_name == 'pull_request' }}
5884
uses: actions/github-script@v6
5985
with:
6086
github-token: ${{ inputs.token }}
6187
script: |
62-
const issues = JSON.parse('${{ steps.validate.outputs.issues }}');
63-
const totalConcurrency = '${{ steps.validate.outputs.total_concurrency }}';
64-
const maxConcurrency = '${{ inputs.max-concurrency }}';
65-
66-
let body = '❌ Workflow Concurrency Validation Failed!\n\n';
67-
body += `Total concurrency (${totalConcurrency}) exceeds maximum allowed (${maxConcurrency}).\n\n`;
68-
69-
if (issues.length > 0) {
70-
body += 'Issues found:\n';
71-
issues.forEach(issue => {
72-
body += `- ${issue}\n`;
88+
try {
89+
console.log('Starting PR comment creation...');
90+
const validationResult = JSON.parse('${{ steps.validate.outputs.validation_result }}');
91+
console.log('Validation result:', validationResult);
92+
93+
let body = '❌ Workflow Concurrency Validation Failed!\n\n';
94+
body += `Found ${validationResult.issues.length} workflows with concurrency issues (maximum allowed: ${validationResult.max}).\n\n`;
95+
96+
if (validationResult.issues && validationResult.issues.length > 0) {
97+
body += 'Issues found:\n';
98+
validationResult.issues.forEach(issue => {
99+
body += `- ${issue}\n`;
100+
});
101+
}
102+
103+
console.log('Attempting to create comment with body:', body);
104+
105+
await github.rest.issues.createComment({
106+
issue_number: context.issue.number,
107+
owner: context.repo.owner,
108+
repo: context.repo.repo,
109+
body: body
73110
});
111+
112+
console.log('Successfully posted comment on PR');
113+
} catch (error) {
114+
console.log('Error details:', error);
115+
throw error;
74116
}
75-
76-
github.rest.issues.createComment({
77-
issue_number: context.issue.number,
78-
owner: context.repo.owner,
79-
repo: context.repo.repo,
80-
body: body
81-
});

0 commit comments

Comments
 (0)