Skip to content

Commit 35161c3

Browse files
authored
Merge pull request #4 from optimizely/concurrency-job-level
Concurrency-job-level
2 parents f1f035c + d9405bd commit 35161c3

File tree

6 files changed

+124
-22
lines changed

6 files changed

+124
-22
lines changed

Dockerfile

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ FROM alpine:3.19 AS base
44

55
# hadolint ignore=DL3018
66
RUN apk add --no-cache --update \
7-
nodejs=18.18.2-r0 \
8-
git=2.40.1-r0 \
9-
openssh=9.3_p2-r0 \
10-
ca-certificates=20230506-r0 \
11-
ruby-bundler=2.4.15-r0 \
12-
bash=5.2.15-r5
7+
nodejs \
8+
git \
9+
openssh \
10+
ca-certificates \
11+
ruby-bundler \
12+
bash
1313

1414
WORKDIR /action
1515

@@ -18,7 +18,7 @@ WORKDIR /action
1818
FROM base AS build
1919

2020
# hadolint ignore=DL3018
21-
RUN apk add --no-cache npm=9.6.6-r0
21+
RUN apk add --no-cache npm
2222

2323
# slience npm
2424
# hadolint ignore=DL3059

README.md

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ Workflows run on every commit asynchronously, this is fine for most cases, howev
1818

1919
## Usage
2020

21+
### Workflow-Level Concurrency (Default)
22+
2123
###### `.github/workflows/my-workflow.yml`
2224

2325
``` yaml
@@ -26,19 +28,65 @@ jobs:
2628
runs-on: ubuntu-latest
2729

2830
steps:
29-
- uses: actions/checkout@v2
31+
- uses: actions/checkout@v4
3032
- uses: ahmadnassri/action-workflow-queue@v1
3133

3234
# only runs additional steps if there is no other instance of `my-workflow.yml` currently running
3335
```
3436

37+
### Job-Level Concurrency
38+
39+
For more granular control, you can specify a job name to check concurrency only for that specific job within the workflow:
40+
41+
###### `.github/workflows/deployment-workflow.yml`
42+
43+
``` yaml
44+
jobs:
45+
deploy-staging:
46+
runs-on: ubuntu-latest
47+
steps:
48+
- uses: actions/checkout@v4
49+
- uses: ahmadnassri/action-workflow-queue@v1
50+
with:
51+
job-name: "deploy-staging"
52+
# only waits if another workflow run has the "deploy-staging" job currently running
53+
- name: Deploy to staging
54+
run: echo "Deploying to staging..."
55+
56+
deploy-production:
57+
runs-on: ubuntu-latest
58+
steps:
59+
- uses: actions/checkout@v4
60+
- uses: ahmadnassri/action-workflow-queue@v1
61+
with:
62+
job-name: "deploy-production"
63+
# only waits if another workflow run has the "deploy-production" job currently running
64+
- name: Deploy to production
65+
run: echo "Deploying to production..."
66+
67+
test:
68+
runs-on: ubuntu-latest
69+
steps:
70+
- uses: actions/checkout@v4
71+
# No queue action - tests can run concurrently
72+
- name: Run tests
73+
run: echo "Running tests..."
74+
```
75+
76+
In this example:
77+
- `deploy-staging` jobs from different workflow runs cannot run concurrently
78+
- `deploy-production` jobs from different workflow runs cannot run concurrently
79+
- `deploy-staging` and `deploy-production` jobs CAN run concurrently with each other
80+
- `test` jobs can always run concurrently
81+
3582
### Inputs
3683

3784
| input | required | default | description |
3885
|----------------|----------|----------------|-------------------------------------------------|
3986
| `github-token` | ❌ | `github.token` | The GitHub token used to call the GitHub API |
4087
| `timeout` | ❌ | `600000` | timeout before we stop trying (in milliseconds) |
4188
| `delay` | ❌ | `10000` | delay between status checks (in milliseconds) |
89+
| `job-name` | ❌ | `null` | Specific job name to check concurrency for (optional - defaults to workflow-level concurrency) |
4290

4391
----
4492
> Author: [Ahmad Nassri](https://www.ahmadnassri.com/) •

action.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ inputs:
1818
description: delay between status checks (in milliseconds)
1919
default: "3000"
2020

21+
job-name:
22+
description: Specific job name to check concurrency for (optional - defaults to workflow-level concurrency)
23+
required: false
24+
2125
runs:
2226
using: docker
23-
image: docker://ghcr.io/ahmadnassri/action-workflow-queue:1.2.0
27+
image: Dockerfile

src/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import main from './lib/index.js'
1111
const inputs = {
1212
token: core.getInput('github-token', { required: true }),
1313
delay: Number(core.getInput('delay', { required: true })),
14-
timeout: Number(core.getInput('timeout', { required: true }))
14+
timeout: Number(core.getInput('timeout', { required: true })),
15+
jobName: core.getInput('job-name', { required: false }) || null
1516
}
1617

1718
// error handler

src/lib/index.js

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import runs from './runs.js'
1010
// sleep function
1111
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
1212

13-
export default async function ({ token, delay, timeout }) {
13+
export default async function ({ token, delay, timeout, jobName }) {
1414
let timer = 0
1515

1616
// init octokit
@@ -28,13 +28,21 @@ export default async function ({ token, delay, timeout }) {
2828
// date to check against
2929
const before = new Date(run_started_at)
3030

31-
core.info(`searching for workflow runs before ${before}`)
31+
if (jobName) {
32+
core.info(`searching for job "${jobName}" in workflow runs before ${before}`)
33+
} else {
34+
core.info(`searching for workflow runs before ${before}`)
35+
}
3236

3337
// get previous runs
34-
let waiting_for = await runs({ octokit, run_id, workflow_id, before })
38+
let waiting_for = await runs({ octokit, run_id, workflow_id, before, jobName })
3539

3640
if (waiting_for.length === 0) {
37-
core.info('no active run of this workflow found')
41+
if (jobName) {
42+
core.info(`no active run of job "${jobName}" found`)
43+
} else {
44+
core.info('no active run of this workflow found')
45+
}
3846
process.exit(0)
3947
}
4048

@@ -44,16 +52,24 @@ export default async function ({ token, delay, timeout }) {
4452

4553

4654
for (const run of waiting_for) {
47-
core.info(`waiting for run #${run.id}: current status: ${run.status}`)
55+
if (jobName) {
56+
core.info(`waiting for job "${jobName}" in run #${run.id}: current status: ${run.status}`)
57+
} else {
58+
core.info(`waiting for run #${run.id}: current status: ${run.status}`)
59+
}
4860
}
4961

5062
// zzz
5163
core.info(`waiting for #${delay/1000} seconds before polling the status again`)
5264
await sleep(delay)
5365

5466
// get the data again
55-
waiting_for = await runs({ octokit, run_id, workflow_id, before })
67+
waiting_for = await runs({ octokit, run_id, workflow_id, before, jobName })
5668
}
5769

58-
core.info('all runs in the queue completed!')
70+
if (jobName) {
71+
core.info(`all instances of job "${jobName}" in the queue completed!`)
72+
} else {
73+
core.info('all runs in the queue completed!')
74+
}
5975
}

src/lib/runs.js

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,57 @@ import { inspect } from 'util'
77
import core from '@actions/core'
88
import github from '@actions/github'
99

10-
export default async function ({ octokit, workflow_id, run_id, before }) {
10+
export default async function ({ octokit, workflow_id, run_id, before, jobName }) {
1111
// get current run of this workflow
1212
const { data: { workflow_runs } } = await octokit.request('GET /repos/{owner}/{repo}/actions/workflows/{workflow_id}/runs', {
1313
...github.context.repo,
1414
workflow_id
1515
})
1616

1717
// find any instances of the same workflow
18-
const waiting_for = workflow_runs
18+
const active_runs = workflow_runs
1919
// limit to currently running ones
2020
.filter(run => ['in_progress', 'queued', 'waiting', 'pending', 'action_required', 'requested'].includes(run.status))
2121
// exclude this one
2222
.filter(run => run.id !== run_id)
2323
// get older runs
2424
.filter(run => new Date(run.run_started_at) < before)
2525

26-
core.info(`found ${waiting_for.length} workflow runs`)
27-
core.debug(inspect(waiting_for.map(run => ({ id: run.id, name: run.name }))))
26+
core.info(`found ${active_runs.length} active workflow runs`)
2827

29-
return waiting_for
28+
// If no job name specified, return all active runs (existing behavior)
29+
if (!jobName) {
30+
core.debug(inspect(active_runs.map(run => ({ id: run.id, name: run.name }))))
31+
return active_runs
32+
}
33+
34+
// Job-level filtering: check each active run for the specific job
35+
const runs_with_target_job = []
36+
37+
for (const run of active_runs) {
38+
try {
39+
// Get jobs for this workflow run
40+
const { data: { jobs } } = await octokit.request('GET /repos/{owner}/{repo}/actions/runs/{run_id}/jobs', {
41+
...github.context.repo,
42+
run_id: run.id
43+
})
44+
45+
// Check if this run has the target job currently running
46+
const target_job = jobs.find(job =>
47+
job.name === jobName &&
48+
['in_progress', 'queued', 'waiting', 'pending', 'action_required', 'requested'].includes(job.status)
49+
)
50+
51+
if (target_job) {
52+
core.info(`found job "${jobName}" (status: ${target_job.status}) in run #${run.id}`)
53+
runs_with_target_job.push(run)
54+
}
55+
} catch (error) {
56+
// Log error but continue checking other runs
57+
core.warning(`failed to fetch jobs for run #${run.id}: ${error.message}`)
58+
}
59+
}
60+
61+
core.debug(inspect(runs_with_target_job.map(run => ({ id: run.id, name: run.name }))))
62+
return runs_with_target_job
3063
}

0 commit comments

Comments
 (0)