Skip to content

Commit caf95c6

Browse files
marknet15claude
andauthored
feat: add allow-empty option for path-filtered workflow support (#131)
## Summary - Adds `allow-empty` input (default `false`) that lets Sloth succeed when no other check runs are found - Adds `empty-settle-time` input (default `30s`) — a grace period before accepting an empty result, preventing a race where Sloth passes before other workflows have been scheduled - Extracts wait loop into a testable module with full test coverage - No change to default behaviour — existing users are unaffected ## Context When Sloth runs in a shared workflow (triggered on all PRs) alongside path-filtered workflows, PRs that don't match the path filters (e.g. docs-only changes) will never trigger other checks. Sloth currently polls for 600s then fails with a timeout, blocking the PR. With `allow-empty: "true"`, Sloth waits for the settle period, then succeeds if no checks have appeared. If any check appears during the settle period, Sloth resumes normal behaviour. ## Test plan - [x] All 41 tests pass (30 existing + 11 new) - [x] Lint passes - [ ] Verify on a docs-only PR that Sloth passes after settle period - [ ] Verify on a PR with path-filtered workflows that Sloth still waits for checks 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent daca9f3 commit caf95c6

9 files changed

Lines changed: 485 additions & 70 deletions

File tree

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ jobs:
5656
| `timeout` | The number of seconds before the job is declared a failure if check runs have not yet concluded. | No | `600` |
5757
| `name` | The name of Sloth's own check run. This is used to ensure Sloth does not wait upon itself. | No | `"sloth"` |
5858
| `ignored` | A multi-line list of check run names or glob patterns to ignore when determining an overall result. Supports `*` wildcard. | No | `""` |
59+
| `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"` |
60+
| `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` |
5961

6062
## Ignoring Checks
6163

@@ -77,3 +79,18 @@ ignored: |
7779
```
7880

7981
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.
82+
83+
## Allow Empty
84+
85+
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.
86+
87+
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.
88+
89+
```yaml
90+
- name: Sloth
91+
uses: lendable/sloth@v0
92+
with:
93+
token: ${{ secrets.GITHUB_TOKEN }}
94+
allow-empty: "true"
95+
empty-settle-time: "30"
96+
```

action.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ inputs:
2828
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')."
2929
required: false
3030
default: ""
31+
allow-empty:
32+
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."
33+
required: false
34+
default: "false"
35+
empty-settle-time:
36+
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."
37+
required: false
38+
default: "30"
3139
runs:
3240
using: node24
3341
main: dist/index.js

dist/index.js

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

dist/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/display.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,13 @@ export const Display = {
5050
console.info(`🚀 ${colors.green}Success!${colors.reset}`);
5151
},
5252

53+
emptySuccess: () => {
54+
console.info("");
55+
console.info(
56+
`🚀 ${colors.green}No check runs found after settle period — allow-empty is enabled, passing.${colors.reset}`,
57+
);
58+
},
59+
5360
startingIteration: () => {
5461
console.info("");
5562
},

src/index.ts

Lines changed: 32 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,58 +3,50 @@ import { delay } from "./delay";
33
import { fetchCheckRuns } from "./fetch-check-runs";
44
import { inputs } from "./inputs";
55
import { Display } from "./display";
6+
import { waitForCheckRuns } from "./wait-for-check-runs";
67

78
const startTime = new Date();
89

9-
const shouldTimeOut = (): boolean => {
10-
const executionTime = Math.round(
11-
(new Date().getTime() - startTime.getTime()) / 1000,
12-
);
13-
return executionTime > inputs.timeout;
14-
};
15-
1610
Display.ignoredCheckPatterns(inputs.ignored.patterns);
1711

18-
const waitForCheckRuns = async (): Promise<void> => {
19-
try {
20-
while (!shouldTimeOut()) {
21-
Display.startingIteration();
22-
23-
const checkRuns = await fetchCheckRuns();
24-
25-
if (checkRuns.total() === 0) {
26-
Display.delaying(inputs.interval);
27-
await delay(inputs.interval);
28-
continue;
29-
}
30-
31-
Display.relevantCheckRuns(checkRuns);
12+
const elapsedSeconds = (): number =>
13+
Math.round((new Date().getTime() - startTime.getTime()) / 1000);
3214

33-
if (checkRuns.isOverallFailure()) {
34-
Display.overallFailure();
35-
core.setFailed("A check run failed.");
36-
return;
37-
}
38-
39-
if (checkRuns.isOverallSuccess()) {
40-
Display.overallSuccess();
41-
return;
42-
}
43-
44-
Display.delaying(inputs.interval);
45-
await delay(inputs.interval);
46-
}
47-
48-
Display.timedOut();
49-
core.setFailed("Timed out waiting on check runs to all be successful.");
15+
const run = async (): Promise<void> => {
16+
try {
17+
await waitForCheckRuns(
18+
{
19+
fetchCheckRuns,
20+
delay,
21+
elapsedSeconds,
22+
onSuccess: Display.overallSuccess,
23+
onEmptySuccess: Display.emptySuccess,
24+
onFailure: (message) => {
25+
Display.overallFailure();
26+
core.setFailed(message);
27+
},
28+
onTimeout: (message) => {
29+
Display.timedOut();
30+
core.setFailed(message);
31+
},
32+
onDelaying: Display.delaying,
33+
onIterationStart: Display.startingIteration,
34+
onDisplayCheckRuns: Display.relevantCheckRuns,
35+
},
36+
{
37+
interval: inputs.interval,
38+
timeout: inputs.timeout,
39+
allowEmpty: inputs.allowEmpty,
40+
emptySettleTime: inputs.emptySettleTime,
41+
},
42+
);
5043
} catch (error) {
5144
if (error instanceof Error) {
5245
core.setFailed(error);
53-
return;
5446
} else {
5547
throw error;
5648
}
5749
}
5850
};
5951

60-
waitForCheckRuns();
52+
run();

src/inputs.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,23 @@ if (timeout < 1) {
2121
throw new Error("Timeout must be greater than 0");
2222
}
2323

24+
const emptySettleTime = Number(core.getInput("empty-settle-time"));
25+
26+
if (!Number.isInteger(emptySettleTime)) {
27+
throw new Error("Invalid empty-settle-time");
28+
}
29+
30+
if (emptySettleTime < 0) {
31+
throw new Error("empty-settle-time must be 0 or greater");
32+
}
33+
2434
export const inputs = {
2535
token: core.getInput("token", { required: true }),
2636
name: core.getInput("name"),
2737
interval,
2838
timeout,
2939
ref: core.getInput("ref"),
3040
ignored: new IgnoreMatcher(core.getMultilineInput("ignored")),
41+
allowEmpty: core.getBooleanInput("allow-empty"),
42+
emptySettleTime,
3143
} as const;

0 commit comments

Comments
 (0)