Skip to content

Commit

Permalink
feature: wait for queued runs and an option to refresh runs from GitH…
Browse files Browse the repository at this point in the history
…ub 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]>
  • Loading branch information
vigneshmsft and chenrui333 authored Jun 19, 2024
1 parent df7e268 commit f1563c5
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 6 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ jobs:
| `abort-after-seconds` | number | Maximum number of seconds to wait before aborting the job (unbound by default). Mutually exclusive with continue-after-seconds |
| `poll-interval-seconds` | number | Number of seconds to wait in between checks for previous run completion (defaults to 60) |
| `same-branch-only` | boolean | Only wait on other runs from the same branch (defaults to true) |
| `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 |

#### outputs

Expand Down
7 changes: 7 additions & 0 deletions __tests__/input.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ describe("input", () => {
"INPUT_CONTINUE-AFTER-SECONDS": "10",
"INPUT_POLL-INTERVAL-SECONDS": "5",
"INPUT_SAME-BRANCH-ONLY": "false",
"INPUT_INITIAL-WAIT-SECONDS": "5",
}),
{
githubToken: "s3cr3t",
Expand All @@ -26,6 +27,7 @@ describe("input", () => {
abortAfterSeconds: undefined,
pollIntervalSeconds: 5,
sameBranchOnly: false,
initialWaitSeconds: 5,
},
);
});
Expand All @@ -41,6 +43,7 @@ describe("input", () => {
"INPUT_ABORT-AFTER-SECONDS": "10",
"INPUT_POLL-INTERVAL-SECONDS": "5",
"INPUT_SAME-BRANCH-ONLY": "false",
"INPUT_INITIAL-WAIT-SECONDS": "0",
}),
{
githubToken: "s3cr3t",
Expand All @@ -53,6 +56,7 @@ describe("input", () => {
abortAfterSeconds: 10,
pollIntervalSeconds: 5,
sameBranchOnly: false,
initialWaitSeconds: 0,
},
);
});
Expand Down Expand Up @@ -82,6 +86,7 @@ describe("input", () => {
"INPUT_CONTINUE-AFTER-SECONDS": "",
"INPUT_POLL-INTERVAL-SECONDS": "",
"INPUT_SAME-BRANCH-ONLY": "",
"INPUT_INITIAL-WAIT-SECONDS": "",
}),
{
githubToken: "s3cr3t",
Expand All @@ -94,6 +99,7 @@ describe("input", () => {
abortAfterSeconds: undefined,
pollIntervalSeconds: 60,
sameBranchOnly: true,
initialWaitSeconds: 0,
},
);
});
Expand All @@ -119,6 +125,7 @@ describe("input", () => {
abortAfterSeconds: undefined,
pollIntervalSeconds: 60,
sameBranchOnly: true,
initialWaitSeconds: 0,
},
);
});
Expand Down
107 changes: 107 additions & 0 deletions __tests__/wait.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ describe("wait", () => {
runId: 2,
workflowName: workflow.name,
sameBranchOnly: true,
initialWaitSeconds: 0,
};
});

Expand Down Expand Up @@ -199,6 +200,112 @@ describe("wait", () => {
`✋Awaiting run ${input.runId - 1} ...`,
);
});

it("will wait for both in_progress and queued runs", async () => {
const existingRuns = [
{
id: 1,
status: "in_progress",
html_url: "1",
},
{
id: 2,
status: "queued",
html_url: "2",
},
];
// Give the current run an id that makes it the last in the queue.
input.runId = existingRuns.length + 1;
// Add an in-progress run to simulate a run getting queued _after_ the one we
// are interested in.
existingRuns.push({
id: input.runId + 1,
status: "queued",
html_url: input.runId + 1 + "",
});

const mockedRunsFunc = jest.fn();
mockedRunsFunc
.mockReturnValueOnce(Promise.resolve(existingRuns.slice(0)))
.mockReturnValueOnce(Promise.resolve(existingRuns.slice(0, 1)))
.mockReturnValueOnce(Promise.resolve(existingRuns))
// Finally return just the run that was queued _after_ the "input" run.
.mockReturnValue(
Promise.resolve(existingRuns.slice(existingRuns.length - 1)),
);

const githubClient = {
runs: mockedRunsFunc,
run: jest.fn(),
workflows: async (owner: string, repo: string) =>
Promise.resolve([workflow]),
};

const messages: Array<string> = [];
const waiter = new Waiter(
workflow.id,
// @ts-ignore
githubClient,
input,
(message: string) => {
messages.push(message);
},
() => {},
);
await waiter.wait();
// Verify that the last message printed is that the latest previous run
// is complete and not the oldest one.
const latestPreviousRun = existingRuns[existingRuns.length - 1];
assert.deepEqual(
messages[messages.length - 1],
`✋Awaiting run ${input.runId - 1} ...`,
);
});

it("will retry to get previous runs, if not found during first try", async () => {
jest.setTimeout(10 * 1000);
input.initialWaitSeconds = 2;
// give the current run a random id
input.runId = 2;

const run = {
id: 1,
status: "in_progress",
html_url: "1",
};

const mockedRunsFunc = jest
.fn()
// don't return any runs in the first attempt
.mockReturnValueOnce(Promise.resolve([]))
// return the inprogress run
.mockReturnValueOnce(Promise.resolve([run]))
// then return the same run as completed
.mockReturnValue(Promise.resolve([(run.status = "completed")]));

const githubClient = {
runs: mockedRunsFunc,
workflows: async (owner: string, repo: string) =>
Promise.resolve([workflow]),
};

const messages: Array<string> = [];
const waiter = new Waiter(
workflow.id,
// @ts-ignore
githubClient,
input,
(message: string) => {
messages.push(message);
},
() => {},
);
await waiter.wait();
assert.deepStrictEqual(messages, [
`🔎 Waiting for ${input.initialWaitSeconds} seconds before checking for runs again...`,
"✋Awaiting run 1 ...",
]);
});
});
});
});
2 changes: 2 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ inputs:
description: "Maximum number of seconds to wait before failing the step (unbound by default). Mutually exclusive with continue-after-seconds"
same-branch-only:
description: "Only wait on other runs from the same branch (defaults to true)"
initial-wait-seconds:
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)"
outputs:
force_continued:
description: "True if continue-after-seconds is used and the step using turnstyle continued. False otherwise."
Expand Down
6 changes: 3 additions & 3 deletions dist/index.js

Large diffs are not rendered by default.

23 changes: 20 additions & 3 deletions src/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,34 @@ export class OctokitGitHub {
owner,
repo,
workflow_id,
status: "in_progress",
per_page: 100,
};

if (branch) {
options.branch = branch;
}

return this.octokit.paginate(
const in_progress_options = {
...options,
status: "in_progress" as const,
};
const queued_options = {
...options,
status: "queued" as const,
};

const in_progress_runs = this.octokit.paginate(
this.octokit.actions.listWorkflowRuns,
in_progress_options,
);

const queued_runs = this.octokit.paginate(
this.octokit.actions.listWorkflowRuns,
options,
queued_options,
);

return Promise.all([in_progress_runs, queued_runs]).then((values) =>
values.flat(),
);
};
}
6 changes: 6 additions & 0 deletions src/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface Input {
continueAfterSeconds: number | undefined;
abortAfterSeconds: number | undefined;
sameBranchOnly: boolean;
initialWaitSeconds: number;
}

export const parseInput = (env: Record<string, string | undefined>): Input => {
Expand All @@ -32,6 +33,10 @@ export const parseInput = (env: Record<string, string | undefined>): Input => {
"Only one of continue-after-seconds and abort-after-seconds may be defined",
);
}
const initialWaitSeconds = env["INPUT_INITIAL-WAIT-SECONDS"]
? parseInt(env["INPUT_INITIAL-WAIT-SECONDS"], 10)
: 0;

const sameBranchOnly =
env["INPUT_SAME-BRANCH-ONLY"] === "true" || !env["INPUT_SAME-BRANCH-ONLY"]; // true if not specified
return {
Expand All @@ -45,5 +50,6 @@ export const parseInput = (env: Record<string, string | undefined>): Input => {
continueAfterSeconds,
abortAfterSeconds,
sameBranchOnly,
initialWaitSeconds,
};
};
12 changes: 12 additions & 0 deletions src/wait.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,18 @@ export class Waiter implements Wait {
.sort((a, b) => b.id - a.id);
if (!previousRuns || !previousRuns.length) {
setOutput("force_continued", "");
if (
this.input.initialWaitSeconds > 0 &&
(secondsSoFar || 0) < this.input.initialWaitSeconds
) {
this.info(
`🔎 Waiting for ${this.input.initialWaitSeconds} seconds before checking for runs again...`,
);
await new Promise((resolve) =>
setTimeout(resolve, this.input.initialWaitSeconds * 1000),
);
return this.wait((secondsSoFar || 0) + this.input.initialWaitSeconds);
}
return;
} else {
this.debug(`Found ${previousRuns.length} previous runs`);
Expand Down

0 comments on commit f1563c5

Please sign in to comment.