Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ jobs:
| `timeout` | The number of seconds before the job is declared a failure if check runs have not yet concluded. | No | `600` |
| `name` | The name of Sloth's own check run. This is used to ensure Sloth does not wait upon itself. | No | `"sloth"` |
| `ignored` | A multi-line list of check run names or glob patterns to ignore when determining an overall result. Supports `*` wildcard. | No | `""` |
| `allow-empty` | When `true`, Sloth succeeds if no check runs appear after the settle period. Useful when path-filtered workflows may not trigger. | No | `"false"` |
| `empty-settle-time` | Seconds to wait before accepting an empty result when `allow-empty` is true. Ensures workflows have time to be scheduled. | No | `30` |

## Ignoring Checks

Expand All @@ -77,3 +79,18 @@ ignored: |
```

This is particularly useful for dynamic matrix jobs where check run names are generated at runtime and cannot be enumerated upfront. For example, skipping optional deployment or smoke-test checks from a dynamic CI matrix while still gating on the required checks.

## Allow Empty

When Sloth runs alongside path-filtered workflows, some PRs may not trigger any other checks (e.g. documentation-only changes). By default, Sloth waits for at least one check to appear and will eventually time out if none do.

Set `allow-empty: "true"` to let Sloth succeed when no other checks are found. To avoid a race condition where Sloth passes before other workflows have had time to be scheduled, Sloth waits for `empty-settle-time` seconds (default 30) before accepting an empty result. If any check appears during the settle period, Sloth resumes normal behaviour and waits for it to conclude.

```yaml
- name: Sloth
uses: lendable/sloth@v0
with:
token: ${{ secrets.GITHUB_TOKEN }}
allow-empty: "true"
empty-settle-time: "30"
```
8 changes: 8 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ inputs:
description: "A multi-line list of check run names or glob patterns to ignore when determining an overall result. Use * as a wildcard to match any sequence of characters (e.g. 'deploy-*-staging')."
required: false
default: ""
allow-empty:
description: "When true, Sloth succeeds if no other check runs are found after waiting for the settle period (see empty-settle-time). Useful when path-filtered workflows may not trigger on every PR. When false (default), Sloth waits until at least one check run appears or times out."
required: false
default: "false"
empty-settle-time:
description: "The number of seconds to wait before accepting an empty result when allow-empty is true. This grace period ensures workflows have time to be scheduled before Sloth concludes no checks will run."
required: false
default: "30"
runs:
using: node24
main: dist/index.js
106 changes: 77 additions & 29 deletions 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.

7 changes: 7 additions & 0 deletions src/display.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ export const Display = {
console.info(`🚀 ${colors.green}Success!${colors.reset}`);
},

emptySuccess: () => {
console.info("");
console.info(
`🚀 ${colors.green}No check runs found after settle period — allow-empty is enabled, passing.${colors.reset}`,
);
},

startingIteration: () => {
console.info("");
},
Expand Down
72 changes: 32 additions & 40 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,58 +3,50 @@ import { delay } from "./delay";
import { fetchCheckRuns } from "./fetch-check-runs";
import { inputs } from "./inputs";
import { Display } from "./display";
import { waitForCheckRuns } from "./wait-for-check-runs";

const startTime = new Date();

const shouldTimeOut = (): boolean => {
const executionTime = Math.round(
(new Date().getTime() - startTime.getTime()) / 1000,
);
return executionTime > inputs.timeout;
};

Display.ignoredCheckPatterns(inputs.ignored.patterns);

const waitForCheckRuns = async (): Promise<void> => {
try {
while (!shouldTimeOut()) {
Display.startingIteration();

const checkRuns = await fetchCheckRuns();

if (checkRuns.total() === 0) {
Display.delaying(inputs.interval);
await delay(inputs.interval);
continue;
}

Display.relevantCheckRuns(checkRuns);
const elapsedSeconds = (): number =>
Math.round((new Date().getTime() - startTime.getTime()) / 1000);

if (checkRuns.isOverallFailure()) {
Display.overallFailure();
core.setFailed("A check run failed.");
return;
}

if (checkRuns.isOverallSuccess()) {
Display.overallSuccess();
return;
}

Display.delaying(inputs.interval);
await delay(inputs.interval);
}

Display.timedOut();
core.setFailed("Timed out waiting on check runs to all be successful.");
const run = async (): Promise<void> => {
try {
await waitForCheckRuns(
{
fetchCheckRuns,
delay,
elapsedSeconds,
onSuccess: Display.overallSuccess,
onEmptySuccess: Display.emptySuccess,
onFailure: (message) => {
Display.overallFailure();
core.setFailed(message);
},
onTimeout: (message) => {
Display.timedOut();
core.setFailed(message);
},
onDelaying: Display.delaying,
onIterationStart: Display.startingIteration,
onDisplayCheckRuns: Display.relevantCheckRuns,
},
{
interval: inputs.interval,
timeout: inputs.timeout,
allowEmpty: inputs.allowEmpty,
emptySettleTime: inputs.emptySettleTime,
},
);
} catch (error) {
if (error instanceof Error) {
core.setFailed(error);
return;
} else {
throw error;
}
}
};

waitForCheckRuns();
run();
12 changes: 12 additions & 0 deletions src/inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,23 @@ if (timeout < 1) {
throw new Error("Timeout must be greater than 0");
}

const emptySettleTime = Number(core.getInput("empty-settle-time"));

if (!Number.isInteger(emptySettleTime)) {
throw new Error("Invalid empty-settle-time");
}

if (emptySettleTime < 0) {
throw new Error("empty-settle-time must be 0 or greater");
}

export const inputs = {
token: core.getInput("token", { required: true }),
name: core.getInput("name"),
interval,
timeout,
ref: core.getInput("ref"),
ignored: new IgnoreMatcher(core.getMultilineInput("ignored")),
allowEmpty: core.getBooleanInput("allow-empty"),
emptySettleTime,
} as const;
Loading
Loading