diff --git a/.github/workflows/cancel-all-but-latest.yml b/.github/workflows/cancel-all-but-latest.yml new file mode 100644 index 00000000..745c6b9f --- /dev/null +++ b/.github/workflows/cancel-all-but-latest.yml @@ -0,0 +1,21 @@ +name: Cancel All But Latest + +on: + push: + workflow_dispatch: + +jobs: + task: + # Use macOS because it has a 5 concurrent job limit: easier to test by manual enqueue + # https://docs.github.com/en/actions/reference/usage-limits-billing-and-administration#usage-limits + runs-on: macos-latest + name: Task + steps: + - name: Checkout + uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + - name: Test Step + uses: ./ # Uses an action in the root directory + with: + - all_but_latest: true + - run: echo 'Sleeping...'; sleep 240; echo 'Done.'; diff --git a/README.md b/README.md index dc1de619..97077cfe 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,21 @@ jobs: workflow_id: 479426 ``` +### Advanced: Cancel All But Latest + +In some cases, you may wish to cancel all workflow runs except the newest one. This can help if you have very deep workflow queues and you find that the newer runs are not even executing thus they cannot clear out of date runs. In this mode, the out-of-date workflow runs will cancel all but the latest run and themselves, freeing up the workflow queue. + +```yml +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.8.0 + with: + all_but_latest: true +``` + ## Contributing - Clone this repo diff --git a/action.yml b/action.yml index faf5a33b..24d4077a 100644 --- a/action.yml +++ b/action.yml @@ -12,6 +12,9 @@ inputs: description: 'Optional - Allow canceling other workflows with the same SHA. Useful for the `pull_request.closed` event.' required: false default: 'false' + all_but_latest: + description: "Optional - Cancel all but the most recent action, can help with very deep queues" + required: false access_token: description: 'Your GitHub Access Token, defaults to: {{ github.token }}' default: '${{ github.token }}' diff --git a/dist/index.js b/dist/index.js index 9d194284..6ddfed0f 100644 --- a/dist/index.js +++ b/dist/index.js @@ -5864,6 +5864,7 @@ async function main() { const token = core.getInput('access_token', { required: true }); const workflow_id_input = core.getInput('workflow_id', { required: false }); const ignore_sha = core.getInput('ignore_sha', { required: false }) === 'true'; + const all_but_latest = core.getInput('all_but_latest', { required: false }) === 'true'; console.log(`Found token: ${token ? 'yes' : 'no'}`); const workflow_ids = []; const octokit = github.getOctokit(token); @@ -5891,14 +5892,31 @@ async function main() { workflow_id, branch }); + let cancel_before; + if (all_but_latest) { + cancel_before = new Date(data.workflow_runs.reduce((a, b) => parseInt(a.created_at, 10) > parseInt(b.created_at, 10) ? a : b).created_at); + } + else { + cancel_before = new Date(current_run.created_at); + } const branchWorkflows = data.workflow_runs.filter(run => run.head_branch === branch); console.log(`Found ${branchWorkflows.length} runs for workflow ${workflow_id} on branch ${branch}`); console.log(branchWorkflows.map(run => `- ${run.html_url}`).join('\n')); let runningWorkflows = branchWorkflows.filter(run => run.status !== 'completed'); runningWorkflows = runningWorkflows.filter(run => ignore_sha || run.head_sha !== headSha); - runningWorkflows = runningWorkflows.filter(run => new Date(run.created_at) < new Date(current_run.created_at)); + runningWorkflows = runningWorkflows.filter(run => { + if (all_but_latest && run !== current_run) { + return new Date(run.created_at) < cancel_before; + } + else { + return new Date(run.created_at) < new Date(current_run.created_at); + } + }); console.log(`with ${runningWorkflows.length} runs to cancel.`); await cancelWorkflowRuns(runningWorkflows, owner, repo, token); + if (all_but_latest && new Date(current_run.created_at) < cancel_before) { + await cancelWorkflowRuns([current_run], owner, repo, token); + } } catch (e) { const msg = e.message || e; diff --git a/src/index.ts b/src/index.ts index 9d611ad5..e992c904 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,6 +33,7 @@ async function main(): Promise { const token = core.getInput('access_token', { required: true }); const workflow_id_input = core.getInput('workflow_id', { required: false }); const ignore_sha = core.getInput('ignore_sha', { required: false }) === 'true'; + const all_but_latest = core.getInput('all_but_latest', { required: false }) === 'true'; console.log(`Found token: ${token ? 'yes' : 'no'}`); const workflow_ids: number[] = []; const octokit = github.getOctokit(token); @@ -67,6 +68,17 @@ async function main(): Promise { branch }); + let cancel_before: Date; + if (all_but_latest) { + cancel_before = new Date( + data.workflow_runs.reduce((a, b) => + parseInt(a.created_at, 10) > parseInt(b.created_at, 10) ? a : b + ).created_at + ); + } else { + cancel_before = new Date(current_run.created_at); + } + const branchWorkflows = data.workflow_runs.filter(run => run.head_branch === branch); console.log( `Found ${branchWorkflows.length} runs for workflow ${workflow_id} on branch ${branch}` @@ -79,13 +91,24 @@ async function main(): Promise { // Filter out for only our headSha unless ignoring it runningWorkflows = runningWorkflows.filter(run => ignore_sha || run.head_sha !== headSha); - // Filter all workflow runs newer than ours - runningWorkflows = runningWorkflows.filter( - run => new Date(run.created_at) < new Date(current_run.created_at) - ); + // Filter workflow runs over time - retain either all before us, or just the latest + runningWorkflows = runningWorkflows.filter(run => { + // In all_but_latest mode, we must not cancel ourselves until we have canceled the rest + if (all_but_latest && run !== current_run) { + return new Date(run.created_at) < cancel_before; + } else { + return new Date(run.created_at) < new Date(current_run.created_at); + } + }); console.log(`with ${runningWorkflows.length} runs to cancel.`); await cancelWorkflowRuns(runningWorkflows, owner, repo, token); + + // In all_but_latest_mode, we may need to cancel ourselves as well. + // We postponed canceling this run because otherwise we couldn't cancel the rest. + if (all_but_latest && new Date(current_run.created_at) < cancel_before) { + await cancelWorkflowRuns([current_run], owner, repo, token); + } } catch (e) { const msg = e.message || e; console.log(`Error while canceling workflow_id ${workflow_id}: ${msg}`);