From 8acd1aa70d1b7595938d31e9ebeecd956f437fd6 Mon Sep 17 00:00:00 2001 From: Hidetake Iwata Date: Sat, 5 Oct 2024 20:16:32 +0900 Subject: [PATCH] Add fail-fast option (#433) --- README.md | 37 +++++++++++---- action.yaml | 4 ++ src/checks.ts | 16 +++++-- src/main.ts | 1 + src/run.ts | 7 +-- tests/checks.test.ts | 106 +++++++++++++++++++++++++++---------------- 6 files changed, 115 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index edd8321..e883855 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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 diff --git a/action.yaml b/action.yaml index 83ffd8e..10267a4 100644 --- a/action.yaml +++ b/action.yaml @@ -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 diff --git a/src/checks.ts b/src/checks.ts index e84ad6f..febf821 100644 --- a/src/checks.ts +++ b/src/checks.ts @@ -19,6 +19,7 @@ type WorkflowRun = { } type RollupOptions = { + failFast: boolean selfWorkflowName: string filterWorkflowEvents: string[] excludeWorkflowNames: string[] @@ -73,7 +74,7 @@ export const rollupChecks = (checks: ListChecksQuery, options: RollupOptions): R }) return { - conclusion: rollupWorkflowRuns(workflowRuns), + conclusion: rollupWorkflowRuns(workflowRuns, options), workflowRuns, } } @@ -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 } diff --git a/src/main.ts b/src/main.ts index 931d08b..60a8387 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,6 +7,7 @@ const main = async (): Promise => { 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 })), diff --git a/src/run.ts b/src/run.ts index 7c39b48..4f96417 100644 --- a/src/run.ts +++ b/src/run.ts @@ -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 diff --git a/tests/checks.test.ts b/tests/checks.test.ts index b84ceb9..d5b3ca5 100644 --- a/tests/checks.test.ts +++ b/tests/checks.test.ts @@ -61,6 +61,7 @@ describe('rollupChecks', () => { filterWorkflowEvents: [], excludeWorkflowNames: [], filterWorkflowNames: [], + failFast: true, }) expect(rollup).toStrictEqual({ conclusion: CheckConclusionState.Success, @@ -88,6 +89,7 @@ describe('rollupChecks', () => { filterWorkflowEvents: ['pull_request_target'], excludeWorkflowNames: [], filterWorkflowNames: [], + failFast: true, }) expect(rollup).toStrictEqual({ conclusion: CheckConclusionState.Success, @@ -108,6 +110,7 @@ describe('rollupChecks', () => { filterWorkflowEvents: [], excludeWorkflowNames: ['*-1'], filterWorkflowNames: [], + failFast: true, }) expect(rollup).toStrictEqual({ conclusion: CheckConclusionState.Success, @@ -128,6 +131,7 @@ describe('rollupChecks', () => { filterWorkflowEvents: [], excludeWorkflowNames: [], filterWorkflowNames: ['*-2'], + failFast: true, }) expect(rollup).toStrictEqual({ conclusion: CheckConclusionState.Success, @@ -148,6 +152,7 @@ describe('rollupChecks', () => { filterWorkflowEvents: [], excludeWorkflowNames: ['*'], filterWorkflowNames: [], + failFast: true, }) expect(rollup).toStrictEqual({ conclusion: CheckConclusionState.Success, @@ -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, @@ -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) + }) }) })