Skip to content

Commit f1563c5

Browse files
feature: wait for queued runs and an option to refresh runs from GitHub API (#31)
* Adding initialWaitSeconds input parameter A positive value can be specified as value to this parameter to instruct this action to wait and poll the GitHub API if no in_progress runs are returned in the first attempt. * Wait and check for new in_progress runs Refetch runs from GitHub API, If no runs are found in the first attempt and initialWaitSeconds is specified * Get in_progress and queued runs currently queued runs are not being picked up, which is causing issues when multiple runs are queued concurrently. * Rebuild distribution 1. initial-wait-seconds input parameter 2. fetch both queued and in_progress runs * fmt and build * update test and fmt code Signed-off-by: Rui Chen <[email protected]> * fix build issue * fmt code Signed-off-by: Rui Chen <[email protected]> --------- Signed-off-by: Rui Chen <[email protected]> Co-authored-by: Rui Chen <[email protected]>
1 parent df7e268 commit f1563c5

File tree

8 files changed

+158
-6
lines changed

8 files changed

+158
-6
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ jobs:
154154
| `abort-after-seconds` | number | Maximum number of seconds to wait before aborting the job (unbound by default). Mutually exclusive with continue-after-seconds |
155155
| `poll-interval-seconds` | number | Number of seconds to wait in between checks for previous run completion (defaults to 60) |
156156
| `same-branch-only` | boolean | Only wait on other runs from the same branch (defaults to true) |
157+
| `initial-wait-seconds` | number | Total elapsed seconds within which period the action will refresh the list of current runs, if no runs were found in the first attempt |
157158

158159
#### outputs
159160

__tests__/input.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ describe("input", () => {
1414
"INPUT_CONTINUE-AFTER-SECONDS": "10",
1515
"INPUT_POLL-INTERVAL-SECONDS": "5",
1616
"INPUT_SAME-BRANCH-ONLY": "false",
17+
"INPUT_INITIAL-WAIT-SECONDS": "5",
1718
}),
1819
{
1920
githubToken: "s3cr3t",
@@ -26,6 +27,7 @@ describe("input", () => {
2627
abortAfterSeconds: undefined,
2728
pollIntervalSeconds: 5,
2829
sameBranchOnly: false,
30+
initialWaitSeconds: 5,
2931
},
3032
);
3133
});
@@ -41,6 +43,7 @@ describe("input", () => {
4143
"INPUT_ABORT-AFTER-SECONDS": "10",
4244
"INPUT_POLL-INTERVAL-SECONDS": "5",
4345
"INPUT_SAME-BRANCH-ONLY": "false",
46+
"INPUT_INITIAL-WAIT-SECONDS": "0",
4447
}),
4548
{
4649
githubToken: "s3cr3t",
@@ -53,6 +56,7 @@ describe("input", () => {
5356
abortAfterSeconds: 10,
5457
pollIntervalSeconds: 5,
5558
sameBranchOnly: false,
59+
initialWaitSeconds: 0,
5660
},
5761
);
5862
});
@@ -82,6 +86,7 @@ describe("input", () => {
8286
"INPUT_CONTINUE-AFTER-SECONDS": "",
8387
"INPUT_POLL-INTERVAL-SECONDS": "",
8488
"INPUT_SAME-BRANCH-ONLY": "",
89+
"INPUT_INITIAL-WAIT-SECONDS": "",
8590
}),
8691
{
8792
githubToken: "s3cr3t",
@@ -94,6 +99,7 @@ describe("input", () => {
9499
abortAfterSeconds: undefined,
95100
pollIntervalSeconds: 60,
96101
sameBranchOnly: true,
102+
initialWaitSeconds: 0,
97103
},
98104
);
99105
});
@@ -119,6 +125,7 @@ describe("input", () => {
119125
abortAfterSeconds: undefined,
120126
pollIntervalSeconds: 60,
121127
sameBranchOnly: true,
128+
initialWaitSeconds: 0,
122129
},
123130
);
124131
});

__tests__/wait.test.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ describe("wait", () => {
2424
runId: 2,
2525
workflowName: workflow.name,
2626
sameBranchOnly: true,
27+
initialWaitSeconds: 0,
2728
};
2829
});
2930

@@ -199,6 +200,112 @@ describe("wait", () => {
199200
`✋Awaiting run ${input.runId - 1} ...`,
200201
);
201202
});
203+
204+
it("will wait for both in_progress and queued runs", async () => {
205+
const existingRuns = [
206+
{
207+
id: 1,
208+
status: "in_progress",
209+
html_url: "1",
210+
},
211+
{
212+
id: 2,
213+
status: "queued",
214+
html_url: "2",
215+
},
216+
];
217+
// Give the current run an id that makes it the last in the queue.
218+
input.runId = existingRuns.length + 1;
219+
// Add an in-progress run to simulate a run getting queued _after_ the one we
220+
// are interested in.
221+
existingRuns.push({
222+
id: input.runId + 1,
223+
status: "queued",
224+
html_url: input.runId + 1 + "",
225+
});
226+
227+
const mockedRunsFunc = jest.fn();
228+
mockedRunsFunc
229+
.mockReturnValueOnce(Promise.resolve(existingRuns.slice(0)))
230+
.mockReturnValueOnce(Promise.resolve(existingRuns.slice(0, 1)))
231+
.mockReturnValueOnce(Promise.resolve(existingRuns))
232+
// Finally return just the run that was queued _after_ the "input" run.
233+
.mockReturnValue(
234+
Promise.resolve(existingRuns.slice(existingRuns.length - 1)),
235+
);
236+
237+
const githubClient = {
238+
runs: mockedRunsFunc,
239+
run: jest.fn(),
240+
workflows: async (owner: string, repo: string) =>
241+
Promise.resolve([workflow]),
242+
};
243+
244+
const messages: Array<string> = [];
245+
const waiter = new Waiter(
246+
workflow.id,
247+
// @ts-ignore
248+
githubClient,
249+
input,
250+
(message: string) => {
251+
messages.push(message);
252+
},
253+
() => {},
254+
);
255+
await waiter.wait();
256+
// Verify that the last message printed is that the latest previous run
257+
// is complete and not the oldest one.
258+
const latestPreviousRun = existingRuns[existingRuns.length - 1];
259+
assert.deepEqual(
260+
messages[messages.length - 1],
261+
`✋Awaiting run ${input.runId - 1} ...`,
262+
);
263+
});
264+
265+
it("will retry to get previous runs, if not found during first try", async () => {
266+
jest.setTimeout(10 * 1000);
267+
input.initialWaitSeconds = 2;
268+
// give the current run a random id
269+
input.runId = 2;
270+
271+
const run = {
272+
id: 1,
273+
status: "in_progress",
274+
html_url: "1",
275+
};
276+
277+
const mockedRunsFunc = jest
278+
.fn()
279+
// don't return any runs in the first attempt
280+
.mockReturnValueOnce(Promise.resolve([]))
281+
// return the inprogress run
282+
.mockReturnValueOnce(Promise.resolve([run]))
283+
// then return the same run as completed
284+
.mockReturnValue(Promise.resolve([(run.status = "completed")]));
285+
286+
const githubClient = {
287+
runs: mockedRunsFunc,
288+
workflows: async (owner: string, repo: string) =>
289+
Promise.resolve([workflow]),
290+
};
291+
292+
const messages: Array<string> = [];
293+
const waiter = new Waiter(
294+
workflow.id,
295+
// @ts-ignore
296+
githubClient,
297+
input,
298+
(message: string) => {
299+
messages.push(message);
300+
},
301+
() => {},
302+
);
303+
await waiter.wait();
304+
assert.deepStrictEqual(messages, [
305+
`🔎 Waiting for ${input.initialWaitSeconds} seconds before checking for runs again...`,
306+
"✋Awaiting run 1 ...",
307+
]);
308+
});
202309
});
203310
});
204311
});

action.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ inputs:
1616
description: "Maximum number of seconds to wait before failing the step (unbound by default). Mutually exclusive with continue-after-seconds"
1717
same-branch-only:
1818
description: "Only wait on other runs from the same branch (defaults to true)"
19+
initial-wait-seconds:
20+
description: "Total elapsed seconds within which period the action will refresh the list of current runs, if no runs were found in the first poll (0 by default, ie doesn't retry)"
1921
outputs:
2022
force_continued:
2123
description: "True if continue-after-seconds is used and the step using turnstyle continued. False otherwise."

dist/index.js

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/github.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,34 @@ export class OctokitGitHub {
4646
owner,
4747
repo,
4848
workflow_id,
49-
status: "in_progress",
5049
per_page: 100,
5150
};
5251

5352
if (branch) {
5453
options.branch = branch;
5554
}
5655

57-
return this.octokit.paginate(
56+
const in_progress_options = {
57+
...options,
58+
status: "in_progress" as const,
59+
};
60+
const queued_options = {
61+
...options,
62+
status: "queued" as const,
63+
};
64+
65+
const in_progress_runs = this.octokit.paginate(
66+
this.octokit.actions.listWorkflowRuns,
67+
in_progress_options,
68+
);
69+
70+
const queued_runs = this.octokit.paginate(
5871
this.octokit.actions.listWorkflowRuns,
59-
options,
72+
queued_options,
73+
);
74+
75+
return Promise.all([in_progress_runs, queued_runs]).then((values) =>
76+
values.flat(),
6077
);
6178
};
6279
}

src/input.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export interface Input {
99
continueAfterSeconds: number | undefined;
1010
abortAfterSeconds: number | undefined;
1111
sameBranchOnly: boolean;
12+
initialWaitSeconds: number;
1213
}
1314

1415
export const parseInput = (env: Record<string, string | undefined>): Input => {
@@ -32,6 +33,10 @@ export const parseInput = (env: Record<string, string | undefined>): Input => {
3233
"Only one of continue-after-seconds and abort-after-seconds may be defined",
3334
);
3435
}
36+
const initialWaitSeconds = env["INPUT_INITIAL-WAIT-SECONDS"]
37+
? parseInt(env["INPUT_INITIAL-WAIT-SECONDS"], 10)
38+
: 0;
39+
3540
const sameBranchOnly =
3641
env["INPUT_SAME-BRANCH-ONLY"] === "true" || !env["INPUT_SAME-BRANCH-ONLY"]; // true if not specified
3742
return {
@@ -45,5 +50,6 @@ export const parseInput = (env: Record<string, string | undefined>): Input => {
4550
continueAfterSeconds,
4651
abortAfterSeconds,
4752
sameBranchOnly,
53+
initialWaitSeconds,
4854
};
4955
};

src/wait.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,18 @@ export class Waiter implements Wait {
7171
.sort((a, b) => b.id - a.id);
7272
if (!previousRuns || !previousRuns.length) {
7373
setOutput("force_continued", "");
74+
if (
75+
this.input.initialWaitSeconds > 0 &&
76+
(secondsSoFar || 0) < this.input.initialWaitSeconds
77+
) {
78+
this.info(
79+
`🔎 Waiting for ${this.input.initialWaitSeconds} seconds before checking for runs again...`,
80+
);
81+
await new Promise((resolve) =>
82+
setTimeout(resolve, this.input.initialWaitSeconds * 1000),
83+
);
84+
return this.wait((secondsSoFar || 0) + this.input.initialWaitSeconds);
85+
}
7486
return;
7587
} else {
7688
this.debug(`Found ${previousRuns.length} previous runs`);

0 commit comments

Comments
 (0)