Skip to content

Commit

Permalink
Add fail-fast option (#433)
Browse files Browse the repository at this point in the history
  • Loading branch information
int128 authored Oct 5, 2024
1 parent d0d436c commit 8acd1aa
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 56 deletions.
37 changes: 27 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,22 @@ jobs:
backend / *
```

### Fail-fast

By default, this action exits immediately if any workflow run is failing.
You can wait for completion of all workflow runs by disabling fail-fast.

```yaml
jobs:
wait-for-workflows:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: int128/wait-for-workflows-action@v1
with:
fail-fast: false
```

## Caveat

### Cost of GitHub-hosted runners :moneybag:
Expand All @@ -100,16 +116,17 @@ See [rate limiting](https://docs.github.com/en/rest/overview/resources-in-the-re

### Inputs

| Name | Default | Description |
| --------------------------- | ---------------------------------------------------- | ---------------------------------- |
| `filter-workflow-names` | - | Filter workflows by name patterns |
| `exclude-workflow-names` | - | Exclude workflows by name patterns |
| `filter-workflow-events` | `github.event_name` | Filter workflows by events |
| `initial-delay-seconds` | 10 | Initial delay before polling |
| `period-seconds` | 15 | Polling period |
| `page-size-of-check-suites` | 100 | Page size of CheckSuites query |
| `sha` | `github.event.pull_request.head.sha` or `github.sha` | Commit SHA to wait for |
| `token` | `github.token` | GitHub token |
| Name | Default | Description |
| --------------------------- | ---------------------------------------------------- | ------------------------------------------- |
| `filter-workflow-names` | - | Filter workflows by name patterns |
| `exclude-workflow-names` | - | Exclude workflows by name patterns |
| `filter-workflow-events` | `github.event_name` | Filter workflows by events |
| `fail-fast` | true | Exit immediately if any workflow is failing |
| `initial-delay-seconds` | 10 | Initial delay before polling |
| `period-seconds` | 15 | Polling period |
| `page-size-of-check-suites` | 100 | Page size of CheckSuites query |
| `sha` | `github.event.pull_request.head.sha` or `github.sha` | Commit SHA to wait for |
| `token` | `github.token` | GitHub token |

### Outputs

Expand Down
4 changes: 4 additions & 0 deletions action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ inputs:
description: Filter workflows by events (multiline)
required: false
default: ${{ github.event_name }}
fail-fast:
description: Exit immediately if any workflow is failing
required: true
default: 'true'
initial-delay-seconds:
description: Initial delay before polling in seconds
required: true
Expand Down
16 changes: 11 additions & 5 deletions src/checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type WorkflowRun = {
}

type RollupOptions = {
failFast: boolean
selfWorkflowName: string
filterWorkflowEvents: string[]
excludeWorkflowNames: string[]
Expand Down Expand Up @@ -73,7 +74,7 @@ export const rollupChecks = (checks: ListChecksQuery, options: RollupOptions): R
})

return {
conclusion: rollupWorkflowRuns(workflowRuns),
conclusion: rollupWorkflowRuns(workflowRuns, options),
workflowRuns,
}
}
Expand All @@ -84,13 +85,18 @@ const isFailedConclusion = (conclusion: CheckConclusionState | null): boolean =>
conclusion === CheckConclusionState.StartupFailure ||
conclusion === CheckConclusionState.TimedOut

export const rollupWorkflowRuns = (workflowRuns: WorkflowRun[]): RollupConclusion => {
if (workflowRuns.some((run) => isFailedConclusion(run.conclusion))) {
return CheckConclusionState.Failure
}
export const rollupWorkflowRuns = (workflowRuns: WorkflowRun[], options: { failFast: boolean }): RollupConclusion => {
if (workflowRuns.every((run) => run.status === CheckStatusState.Completed)) {
if (workflowRuns.some((run) => isFailedConclusion(run.conclusion))) {
return CheckConclusionState.Failure
}
return CheckConclusionState.Success
}
if (options.failFast) {
if (workflowRuns.some((run) => isFailedConclusion(run.conclusion))) {
return CheckConclusionState.Failure
}
}
return null
}

Expand Down
1 change: 1 addition & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const main = async (): Promise<void> => {
filterWorkflowNames: core.getMultilineInput('filter-workflow-names'),
excludeWorkflowNames: core.getMultilineInput('exclude-workflow-names'),
filterWorkflowEvents: core.getMultilineInput('filter-workflow-events'),
failFast: core.getBooleanInput('fail-fast', { required: true }),
initialDelaySeconds: Number.parseInt(core.getInput('initial-delay-seconds', { required: true })),
periodSeconds: Number.parseInt(core.getInput('period-seconds', { required: true })),
pageSizeOfCheckSuites: parseInt(core.getInput('page-size-of-check-suites', { required: true })),
Expand Down
7 changes: 4 additions & 3 deletions src/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ import { getOctokit } from './github.js'
const GITHUB_ACTIONS_APP_ID = 15368

type Inputs = {
initialDelaySeconds: number
periodSeconds: number
pageSizeOfCheckSuites: number
filterWorkflowEvents: string[]
excludeWorkflowNames: string[]
filterWorkflowNames: string[]
failFast: boolean
initialDelaySeconds: number
periodSeconds: number
pageSizeOfCheckSuites: number
sha: string
owner: string
repo: string
Expand Down
106 changes: 68 additions & 38 deletions tests/checks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ describe('rollupChecks', () => {
filterWorkflowEvents: [],
excludeWorkflowNames: [],
filterWorkflowNames: [],
failFast: true,
})
expect(rollup).toStrictEqual<Rollup>({
conclusion: CheckConclusionState.Success,
Expand Down Expand Up @@ -88,6 +89,7 @@ describe('rollupChecks', () => {
filterWorkflowEvents: ['pull_request_target'],
excludeWorkflowNames: [],
filterWorkflowNames: [],
failFast: true,
})
expect(rollup).toStrictEqual<Rollup>({
conclusion: CheckConclusionState.Success,
Expand All @@ -108,6 +110,7 @@ describe('rollupChecks', () => {
filterWorkflowEvents: [],
excludeWorkflowNames: ['*-1'],
filterWorkflowNames: [],
failFast: true,
})
expect(rollup).toStrictEqual<Rollup>({
conclusion: CheckConclusionState.Success,
Expand All @@ -128,6 +131,7 @@ describe('rollupChecks', () => {
filterWorkflowEvents: [],
excludeWorkflowNames: [],
filterWorkflowNames: ['*-2'],
failFast: true,
})
expect(rollup).toStrictEqual<Rollup>({
conclusion: CheckConclusionState.Success,
Expand All @@ -148,6 +152,7 @@ describe('rollupChecks', () => {
filterWorkflowEvents: [],
excludeWorkflowNames: ['*'],
filterWorkflowNames: [],
failFast: true,
})
expect(rollup).toStrictEqual<Rollup>({
conclusion: CheckConclusionState.Success,
Expand All @@ -157,11 +162,6 @@ describe('rollupChecks', () => {
})

describe('rollupWorkflowRuns', () => {
it(`should return ${CheckConclusionState.Success} if no workflow run is given`, () => {
const state = rollupWorkflowRuns([])
expect(state).toBe(CheckConclusionState.Success)
})

const runSuccess = {
status: CheckStatusState.Completed,
conclusion: CheckConclusionState.Success,
Expand All @@ -184,42 +184,72 @@ describe('rollupWorkflowRuns', () => {
workflowName: 'test-in-progress',
}

it.each([
{ workflowRuns: [runSuccess] },
{ workflowRuns: [runSuccess, runSuccess] },
{ workflowRuns: [runSuccess, runSuccess, runSuccess] },
])(
`should return ${CheckConclusionState.Success} if all workflow runs are ${CheckConclusionState.Success}`,
({ workflowRuns }) => {
const state = rollupWorkflowRuns(workflowRuns)
describe.each([false, true])('fail-fast is %p', (failFast) => {
it(`should return ${CheckConclusionState.Success} if no workflow run is given`, () => {
const state = rollupWorkflowRuns([], { failFast })
expect(state).toBe(CheckConclusionState.Success)
},
)
})

it.each([
{ workflowRuns: [runSuccess] },
{ workflowRuns: [runSuccess, runSuccess] },
{ workflowRuns: [runSuccess, runSuccess, runSuccess] },
])(`should return ${CheckConclusionState.Success} if all workflow runs are succeeded`, ({ workflowRuns }) => {
const state = rollupWorkflowRuns(workflowRuns, { failFast })
expect(state).toBe(CheckConclusionState.Success)
})
})

describe('fail-fast', () => {
const failFast = true

it.each([
{ workflowRuns: [runFailure] },
{ workflowRuns: [runSuccess, runFailure] },
{ workflowRuns: [runFailure, runFailure] },
{ workflowRuns: [runInProgress, runFailure] },
{ workflowRuns: [runSuccess, runSuccess, runFailure] },
{ workflowRuns: [runInProgress, runSuccess, runFailure] },
])(
`should return ${CheckConclusionState.Failure} if any workflow run is ${CheckConclusionState.Failure}`,
({ workflowRuns }) => {
const state = rollupWorkflowRuns(workflowRuns)
it.each([
{ workflowRuns: [runFailure] },
{ workflowRuns: [runFailure, runSuccess] },
{ workflowRuns: [runFailure, runFailure] },
{ workflowRuns: [runFailure, runInProgress] },
{ workflowRuns: [runFailure, runInProgress, runSuccess] },
])(`should return ${CheckConclusionState.Failure} if any workflow run is failed`, ({ workflowRuns }) => {
const state = rollupWorkflowRuns(workflowRuns, { failFast })
expect(state).toBe(CheckConclusionState.Failure)
},
)
})

it.each([
{ workflowRuns: [runInProgress] },
{ workflowRuns: [runInProgress, runSuccess] },
{ workflowRuns: [runInProgress, runInProgress] },
{ workflowRuns: [runInProgress, runSuccess, runSuccess] },
])(`should return null if any workflow run is not completed`, ({ workflowRuns }) => {
const state = rollupWorkflowRuns(workflowRuns, { failFast })
expect(state).toBe(null)
})
})

it.each([
{ workflowRuns: [runInProgress] },
{ workflowRuns: [runSuccess, runInProgress] },
{ workflowRuns: [runInProgress, runInProgress] },
{ workflowRuns: [runSuccess, runSuccess, runInProgress] },
{ workflowRuns: [runInProgress, runSuccess, runInProgress] },
{ workflowRuns: [runInProgress, runInProgress, runInProgress] },
])(`should return ${null} if any workflow run is not ${CheckStatusState.Completed}`, ({ workflowRuns }) => {
const state = rollupWorkflowRuns(workflowRuns)
expect(state).toBe(null)
describe('non fail-fast', () => {
const failFast = false

it.each([
{ workflowRuns: [runFailure] },
{ workflowRuns: [runFailure, runSuccess] },
{ workflowRuns: [runFailure, runFailure] },
{ workflowRuns: [runFailure, runSuccess, runSuccess] },
])(
`should return ${CheckConclusionState.Failure} if all workflow runs are completed and any workflow run is failed`,
({ workflowRuns }) => {
const state = rollupWorkflowRuns(workflowRuns, { failFast })
expect(state).toBe(CheckConclusionState.Failure)
},
)

it.each([
{ workflowRuns: [runInProgress] },
{ workflowRuns: [runInProgress, runSuccess] },
{ workflowRuns: [runInProgress, runFailure] },
{ workflowRuns: [runInProgress, runInProgress] },
{ workflowRuns: [runInProgress, runSuccess, runFailure] },
])(`should return null if any workflow run is not completed`, ({ workflowRuns }) => {
const state = rollupWorkflowRuns(workflowRuns, { failFast })
expect(state).toBe(null)
})
})
})

0 comments on commit 8acd1aa

Please sign in to comment.