From 9d0af3bc384f7ebcb4be6b58c8f1653acfbfba24 Mon Sep 17 00:00:00 2001 From: Mike Hardy Date: Thu, 1 Apr 2021 13:29:40 -0500 Subject: [PATCH] feat(all_but_latest): add ability to clear every run but latest This is an adaptation of #35 from @thomwiggers - the logic is entirely from that PR (thank you!) A new workflow adds a 240-second sleep job on macos (limit 5 concurrent) with manual dispatch available for testing --- .github/workflows/cancel-all-but-latest.yml | 22 +++++++++++ README.md | 15 +++++++ action.yml | 5 ++- dist/index.js | 32 +++++++++++++-- src/index.ts | 44 +++++++++++++++++---- 5 files changed, 106 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/cancel-all-but-latest.yml diff --git a/.github/workflows/cancel-all-but-latest.yml b/.github/workflows/cancel-all-but-latest.yml new file mode 100644 index 00000000..e33eec05 --- /dev/null +++ b/.github/workflows/cancel-all-but-latest.yml @@ -0,0 +1,22 @@ +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 + ignore_sha: 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..96254aaf 100644 --- a/action.yml +++ b/action.yml @@ -9,9 +9,12 @@ inputs: description: 'Optional - A single Workflow ID or a comma separated list of IDs' required: false ignore_sha: - description: 'Optional - Allow canceling other workflows with the same SHA. Useful for the `pull_request.closed` event.' + description: 'Optional - Allow canceling other workflows with the same SHA. Useful for `pull_request.closed` or `workflow_dispatch` 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..350d6dbd 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); @@ -5893,12 +5894,35 @@ async function main() { }); 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')); + console.log(branchWorkflows.map(run => `- ${run.html_url} @ ${run.created_at}`).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)); - console.log(`with ${runningWorkflows.length} runs to cancel.`); + console.log(`${runningWorkflows.length} of the workflows are not completed`); + runningWorkflows = runningWorkflows.filter(run => { + console.log(`SHA info: ignore? ${ignore_sha} / ${run.head_sha} !== ${headSha} ?`); + return ignore_sha || run.head_sha !== headSha; + }); + console.log(`${runningWorkflows.length} of those workflows pass the SHA filter`); + let cancel_before; + if (all_but_latest) { + cancel_before = new Date(data.workflow_runs.reduce((a, b) => new Date(a.created_at) > new Date(b.created_at) ? a : b).created_at); + } + else { + cancel_before = new Date(current_run.created_at); + } + console.log(`Canceling matching runs enqueued before ${cancel_before}`); + runningWorkflows = runningWorkflows.filter(run => { + if (all_but_latest && run.id === current_run.id) { + return false; + } + return new Date(run.created_at) < cancel_before; + }); + console.log(`${runningWorkflows.length} of the workflows are in a time range to cancel`); + console.log(`${runningWorkflows.length} matching runs to cancel.`); await cancelWorkflowRuns(runningWorkflows, owner, repo, token); + if (all_but_latest && new Date(current_run.created_at) < cancel_before) { + console.log('in all_but_latest mode and canceling ourselves now'); + 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..217bde68 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); @@ -71,21 +72,50 @@ async function main(): Promise { console.log( `Found ${branchWorkflows.length} runs for workflow ${workflow_id} on branch ${branch}` ); - console.log(branchWorkflows.map(run => `- ${run.html_url}`).join('\n')); + console.log(branchWorkflows.map(run => `- ${run.html_url} @ ${run.created_at}`).join('\n')); // Filter for only uncompleted workflow runs let runningWorkflows = branchWorkflows.filter(run => run.status !== 'completed'); + console.log(`${runningWorkflows.length} of the workflows are not completed`); // Filter out for only our headSha unless ignoring it runningWorkflows = runningWorkflows.filter(run => ignore_sha || run.head_sha !== headSha); + console.log(`${runningWorkflows.length} of those workflows pass the SHA filter`); + + // In all_but_latest mode, we calculate our cancel time as the very latest run available, + // including possibly runs queued after this one, but before this runs if the queue was deep + // Otherwise we just use the current run time to cancel those before this run + let cancel_before: Date; + if (all_but_latest) { + cancel_before = new Date( + data.workflow_runs.reduce((a, b) => + new Date(a.created_at) > new Date(b.created_at) ? a : b + ).created_at + ); + } else { + cancel_before = new Date(current_run.created_at); + } + console.log(`Canceling matching runs enqueued before ${cancel_before}`); + + // Filter workflow runs over time - retain only runs queued after our cancel time + 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.id === current_run.id) { + return false; + } + return new Date(run.created_at) < cancel_before; + }); + console.log(`${runningWorkflows.length} of the workflows are in a time range to cancel`); - // Filter all workflow runs newer than ours - runningWorkflows = runningWorkflows.filter( - run => new Date(run.created_at) < new Date(current_run.created_at) - ); - - console.log(`with ${runningWorkflows.length} runs to cancel.`); + console.log(`${runningWorkflows.length} matching 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) { + console.log('in all_but_latest mode and canceling ourselves now'); + await cancelWorkflowRuns([current_run], owner, repo, token); + } } catch (e) { const msg = e.message || e; console.log(`Error while canceling workflow_id ${workflow_id}: ${msg}`);