From dbcb13a23a5f7881102ccb2cea7e10741f0152c5 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 | 21 ++++++++++++++ README.md | 15 ++++++++++ action.yml | 3 ++ dist/index.js | 20 ++++++++++++- src/index.ts | 31 ++++++++++++++++++--- 5 files changed, 85 insertions(+), 5 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..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}`);