Skip to content

Commit

Permalink
Allow to set ttl more than 15 minutes by setting no-max-ttl to true
Browse files Browse the repository at this point in the history
  • Loading branch information
yogeshlonkar committed Apr 8, 2024
1 parent 1c5bdd1 commit a3da376
Show file tree
Hide file tree
Showing 6 changed files with 61 additions and 38 deletions.
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,15 @@ permissions:
# required: false
interval: '2500'

# Maximum number of minutes to wait before giving up. Step will fail with message providing remaining job names, this can't be more than 15
# Maximum number of minutes to wait before giving up. Step will fail with message providing remaining job names.
# default: '5'
# required: false
ttl: '10'
ttl: '20'

# If set to true, the ttl will NOT be overridden to 15 minutes if it is set more than 15 minutes
# default: 'false'
# required: false
no-max-ttl: 'true'
```
### Output
Expand All @@ -89,7 +94,7 @@ This action has output named `outputs` which is JSON string generated by parsing
### ⚠️ Billing duration

The purpose of this action is to reduce Duration of workflow by prestarting dependee jobs. But waiting for jobs in a step/ action will increase Run time, Billable time.
If the ttl is set more than 15 it will be overriden to 15 minutes. If depdencies requires more than 15 minutes to finish perhaps the dependee job steps should be split in separate jobs not prestart together
If the ttl is set more than 15 it will be overridden to 15 minutes. If depdencies requires more than 15 minutes to finish perhaps the dependee job steps should be split in separate jobs not prestart together but if you still wish to wait more than that you can set `no-max-ttl: 'true'`
[jobs-for-a-workflow-run-attempt]: https://docs.github.com/en/rest/actions/workflow-jobs#list-jobs-for-a-workflow-run-attempt
Expand Down
4 changes: 4 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ inputs:
description: Maximum number of minutes to wait before giving up, step will fail with message providing remaining job names. Can't be more than 15
default: '5'
required: false
no-max-ttl:
description: If set to true, the ttl will NOT be overridden to 15 minutes if it is set more than 15 minutes
default: 'false'
required: false

outputs:
outputs:
Expand Down
4 changes: 3 additions & 1 deletion dist/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

73 changes: 41 additions & 32 deletions src/WaitForJobs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ beforeEach(() => {
]) {
mock.asMock().mockReset();
}
for (const input of ["some-gh-token", "build", "false", "false", "false", "2000", "1"]) {
for (const input of ["some-gh-token", "build", "false", "false", "false", "2000", "1", "false"]) {
getInput.asMock().mockReturnValueOnce(input);
}
const miscellaneous = jest.requireActual("../lib/miscellaneous");
Expand All @@ -50,32 +50,32 @@ describe("wait-for-jobs", () => {
test("ends successfully on all job completion", async () => {
getCurrentJobs.asMock().mockResolvedValueOnce(successfulJobs);
await expect(new WaitForJobs().start()).resolves.toBeUndefined();
expect(setFailed).not.toBeCalled();
expect(setFailed).not.toHaveBeenCalled();
});

test("ends successfully on all job completion with prefix", async () => {
getInput.asMock().mockReset();
for (const input of ["some-gh-token", "bui", "true", "false", "false", "2000", "1"]) {
for (const input of ["some-gh-token", "bui", "true", "false", "false", "2000", "1", "false"]) {
getInput.asMock().mockReturnValueOnce(input);
}
getCurrentJobs.asMock().mockResolvedValueOnce(successfulJobs);
await expect(new WaitForJobs().start()).resolves.toBeUndefined();
expect(setFailed).not.toBeCalled();
expect(setFailed).not.toHaveBeenCalled();
});

test("ends successfully on all job completion with suffix", async () => {
getInput.asMock().mockReset();
for (const input of ["some-gh-token", "ild", "false", "true", "false", "2000", "1"]) {
for (const input of ["some-gh-token", "ild", "false", "true", "false", "2000", "1", "false"]) {
getInput.asMock().mockReturnValueOnce(input);
}
getCurrentJobs.asMock().mockResolvedValueOnce(successfulJobs);
await expect(new WaitForJobs().start()).resolves.toBeUndefined();
expect(setFailed).not.toBeCalled();
expect(setFailed).not.toHaveBeenCalled();
});

test("wait for multiple jobs with same suffix", async () => {
getInput.asMock().mockReset();
for (const input of ["some-gh-token", "ild", "false", "true", "false", "2000", "1"]) {
for (const input of ["some-gh-token", "ild", "false", "true", "false", "2000", "1", "false"]) {
getInput.asMock().mockReturnValueOnce(input);
}
getCurrentJobs.asMock().mockResolvedValueOnce(waitingJobs2);
Expand All @@ -87,26 +87,26 @@ describe("wait-for-jobs", () => {
});
getCurrentJobs.asMock().mockResolvedValueOnce(nextResponse);
await expect(new WaitForJobs().start()).resolves.toBeUndefined();
expect(getCurrentJobs).toBeCalledTimes(2);
expect(info).toBeCalledWith(`dependency: "ild", lastJob to finish: "Next to next build"`);
expect(setFailed).not.toBeCalled();
expect(getCurrentJobs).toHaveBeenCalledTimes(2);
expect(info).toHaveBeenCalledWith(`dependency: "ild", lastJob to finish: "Next to next build"`);
expect(setFailed).not.toHaveBeenCalled();
});

test("ends successfully after 2nd try", async () => {
getCurrentJobs.asMock().mockResolvedValueOnce(waitingJobs);
getCurrentJobs.asMock().mockResolvedValueOnce(successfulJobs);
await expect(new WaitForJobs().start()).resolves.toBeUndefined();
expect(getCurrentJobs).toBeCalledTimes(2);
expect(sleep).toBeCalledTimes(2);
expect(setFailed).not.toBeCalled();
expect(getCurrentJobs).toHaveBeenCalledTimes(2);
expect(sleep).toHaveBeenCalledTimes(2);
expect(setFailed).not.toHaveBeenCalled();
});

test("setFailed on error", async () => {
getCurrentJobs.asMock().mockImplementationOnce(() => {
throw new Error("unexpected status from api.github.com: 404");
});
await expect(new WaitForJobs().start()).resolves.toBeUndefined();
expect(setFailed).toBeCalledWith("unexpected status from api.github.com: 404");
expect(setFailed).toHaveBeenCalledWith("unexpected status from api.github.com: 404");
});

test("setFailed on timeout", async () => {
Expand All @@ -122,8 +122,8 @@ describe("wait-for-jobs", () => {
return miscellaneous.sleep(10 * 60 * 1000, controller);
});
await expect(new WaitForJobs().start()).resolves.toBeUndefined();
expect(sleep).toBeCalledWith(expect.anything(), expect.anything(), "wait-for-jobs");
expect(setFailed).toBeCalledWith("error: jobs [build] did not complete in 1 minutes");
expect(sleep).toHaveBeenCalledWith(expect.anything(), expect.anything(), "wait-for-jobs");
expect(setFailed).toHaveBeenCalledWith("error: jobs [build] did not complete in 1 minutes");
});

test("warning if no job found for dependency", async () => {
Expand All @@ -132,52 +132,52 @@ describe("wait-for-jobs", () => {
jobs: []
});
await expect(new WaitForJobs().start()).resolves.toBeUndefined();
expect(warning).toBeCalledWith('⚠️ no job found for "build" in run');
expect(warning).toHaveBeenCalledWith('⚠️ no job found for "build" in run');
});

test("handle non error rejected promise", async () => {
getCurrentJobs.asMock().mockImplementationOnce(() => {
throw 404;
});
await expect(new WaitForJobs().start()).resolves.toBeUndefined();
expect(setFailed).toBeCalledWith("404");
expect(setFailed).toHaveBeenCalledWith("404");
});

test("handle unknown status", async () => {
getCurrentJobs.asMock().mockResolvedValueOnce({
jobs: [{ name: "build", status: "gollum" }]
});
await expect(new WaitForJobs().start()).resolves.toBeUndefined();
expect(setFailed).toBeCalledWith('error: unknown status "gollum" of job "build"');
expect(setFailed).toHaveBeenCalledWith('error: unknown status "gollum" of job "build"');
});

test("handle unknown conclusion", async () => {
getCurrentJobs.asMock().mockResolvedValueOnce({
jobs: [{ name: "build", status: "completed", conclusion: "gollum" }]
});
await expect(new WaitForJobs().start()).resolves.toBeUndefined();
expect(setFailed).toBeCalledWith('error: unknown conclusion "gollum" of job "build"');
expect(setFailed).toHaveBeenCalledWith('error: unknown conclusion "gollum" of job "build"');
});

test("handle failure conclusion", async () => {
getCurrentJobs.asMock().mockResolvedValueOnce({
jobs: [{ name: "build", status: "completed", conclusion: "failure" }]
});
await expect(new WaitForJobs().start()).resolves.toBeUndefined();
expect(setFailed).toBeCalledWith('error: job "build" failed');
expect(setFailed).toHaveBeenCalledWith('error: job "build" failed');
});

test("handle skipped conclusion", async () => {
getCurrentJobs.asMock().mockResolvedValueOnce({
jobs: [{ name: "build", status: "completed", conclusion: "skipped" }]
});
await expect(new WaitForJobs().start()).resolves.toBeUndefined();
expect(setFailed).toBeCalledWith('error: job dependency "build" skipped but ignore-skipped not set');
expect(setFailed).toHaveBeenCalledWith('error: job dependency "build" skipped but ignore-skipped not set');
});

test("handle skipped conclusion with ignore-skipped", async () => {
getInput.asMock().mockReset();
for (const input of ["some-gh-token", "build", "false", "false", "true", "2000", "1"]) {
for (const input of ["some-gh-token", "build", "false", "false", "true", "2000", "1", "false"]) {
getInput.asMock().mockReturnValueOnce(input);
}
getCurrentJobs.asMock().mockResolvedValueOnce({
Expand All @@ -188,23 +188,23 @@ describe("wait-for-jobs", () => {
jobs: [{ name: "build", status: "completed", conclusion: "success" }]
});
await expect(new WaitForJobs().start()).resolves.toBeUndefined();
expect(info).toBeCalledWith('ignoring skipped job "build" 😒');
expect(info).toHaveBeenCalledWith('ignoring skipped job "build" 😒');
});

test("handle cancelled conclusion", async () => {
getCurrentJobs.asMock().mockResolvedValueOnce({
jobs: [{ name: "build", status: "completed", conclusion: "cancelled" }]
});
await expect(new WaitForJobs().start()).resolves.toBeUndefined();
expect(setFailed).toBeCalledWith('error: job dependency "build" got cancelled');
expect(setFailed).toHaveBeenCalledWith('error: job dependency "build" got cancelled');
});

test("handle jobs with outputs", async () => {
getInput.asMock().mockReturnValueOnce("output1.json");
getOutput.asMock().mockResolvedValueOnce({ out1: "some-value", out2: { some: "some-value2" } });
getCurrentJobs.asMock().mockResolvedValueOnce(successfulJobs);
await expect(new WaitForJobs().start()).resolves.toBeUndefined();
expect(setOutput).toBeCalledWith("outputs", `{"out1":"some-value","out2":{"some":"some-value2"}}`);
expect(setOutput).toHaveBeenCalledWith("outputs", `{"out1":"some-value","out2":{"some":"some-value2"}}`);
});

test("warn on multiple outputs with same key", async () => {
Expand All @@ -213,8 +213,8 @@ describe("wait-for-jobs", () => {
getOutput.asMock().mockResolvedValueOnce({ out1: "other-value" });
getCurrentJobs.asMock().mockResolvedValueOnce(successfulJobs);
await expect(new WaitForJobs().start()).resolves.toBeUndefined();
expect(warning).toBeCalledWith("overwriting previously set outputs.out1 with output from output2.json");
expect(setOutput).toBeCalledWith("outputs", `{"out1":"other-value","out2":{"some":"some-value2"}}`);
expect(warning).toHaveBeenCalledWith("overwriting previously set outputs.out1 with output from output2.json");
expect(setOutput).toHaveBeenCalledWith("outputs", `{"out1":"other-value","out2":{"some":"some-value2"}}`);
});

test("handle error fetching job output", async () => {
Expand All @@ -224,18 +224,27 @@ describe("wait-for-jobs", () => {
});
getCurrentJobs.asMock().mockResolvedValueOnce(successfulJobs);
await expect(new WaitForJobs().start()).resolves.toBeUndefined();
expect(setFailed).toBeCalledWith("error fetching job output");
expect(setFailed).toHaveBeenCalledWith("error fetching job output");
});

test("handle ttl greater than 15 minutes", async () => {
getInput.asMock().mockReset();
for (const input of ["some-gh-token", "build", "false", "false", "true", "2000", "16"]) {
for (const input of ["some-gh-token", "build", "false", "false", "true", "2000", "16", "false"]) {
getInput.asMock().mockReturnValueOnce(input);
}
await expect(new WaitForJobs().start()).resolves.toBeUndefined();
expect(sleep).toBeCalledWith(15 * 60 * 1000, expect.anything(), "action-timeout");
expect(warning).toBeCalledWith(
expect(sleep).toHaveBeenCalledWith(15 * 60 * 1000, expect.anything(), "action-timeout");
expect(warning).toHaveBeenCalledWith(
"Overwriting ttl to 15 minutes. If depdencies requires more than 15 minutes to finish perhaps the dependee jobs should not prestart"
);
});

test("handle ttl greater than 15 minutes and no-max-ttl", async () => {
getInput.asMock().mockReset();
for (const input of ["some-gh-token", "build", "false", "false", "true", "2000", "16", "true"]) {
getInput.asMock().mockReturnValueOnce(input);
}
await expect(new WaitForJobs().start()).resolves.toBeUndefined();
expect(sleep).toHaveBeenCalledWith(16 * 60 * 1000, expect.anything(), "action-timeout");
});
});
5 changes: 4 additions & 1 deletion src/WaitForJobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ enum INPUTS {
PREFIX = "prefix",
SUFFIX = "suffix",
TTL = "ttl",
NO_MAX_TTL = "no-max-ttl",
OUTPUTS_FROM = "outputs-from"
}

Expand Down Expand Up @@ -42,6 +43,7 @@ export default class WaitForJobs {
private readonly timeoutCtrl = new AbortController();
private readonly token: string;
private readonly ttl: number;
private readonly noMaxTtl: boolean;

constructor() {
this.token = getInput(INPUTS.GH_TOKEN, { required: true });
Expand All @@ -51,7 +53,8 @@ export default class WaitForJobs {
this.ignoreSkipped = getInput(INPUTS.IGNORE_SKIPPED) === "true";
this.interval = parseInt(getInput(INPUTS.INTERVAL), 10);
this.ttl = parseInt(getInput(INPUTS.TTL), 10);
if (this.ttl > 15) {
this.noMaxTtl = getInput(INPUTS.NO_MAX_TTL) === "true";
if (this.ttl > 15 && !this.noMaxTtl) {
warning(
"Overwriting ttl to 15 minutes. If depdencies requires more than 15 minutes to finish perhaps the dependee jobs should not prestart"
);
Expand Down

0 comments on commit a3da376

Please sign in to comment.