Skip to content

Commit

Permalink
feat(all_but_latest): add ability to clear every run but latest
Browse files Browse the repository at this point in the history
This is an adaptation of styfle#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
  • Loading branch information
mikehardy committed Apr 1, 2021
1 parent 12fef67 commit 9d0af3b
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 12 deletions.
22 changes: 22 additions & 0 deletions .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.';
15 changes: 15 additions & 0 deletions README.md
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion action.yml
Expand Up @@ -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 }}'
Expand Down
32 changes: 28 additions & 4 deletions dist/index.js
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
44 changes: 37 additions & 7 deletions src/index.ts
Expand Up @@ -33,6 +33,7 @@ async function main(): Promise<void> {
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);
Expand Down Expand Up @@ -71,21 +72,50 @@ async function main(): Promise<void> {
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}`);
Expand Down

0 comments on commit 9d0af3b

Please sign in to comment.