Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optionally cancel all workflows but the latest #35

Merged
merged 4 commits into from Apr 11, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
24 changes: 23 additions & 1 deletion README.md
Expand Up @@ -6,7 +6,7 @@ This includes runs with a [status](https://docs.github.com/en/rest/reference/che

## How does it work?

When you `git push`, this GitHub Action will capture the current Branch and SHA. It will query GitHub's API to find previous workflow runs that match the Branch but do not match the SHA. These in-progress runs will be canceled leaving only the latest run.
When you `git push`, this GitHub Action will capture the current Branch and SHA. It will query GitHub's API to find previous workflow runs that match the Branch but do not match the SHA. These in-progress runs will be cancelled leaving only this run.
styfle marked this conversation as resolved.
Show resolved Hide resolved

Read more about the [Workflow Runs API](https://docs.github.com/en/rest/reference/actions#workflow-runs).

Expand Down Expand Up @@ -100,6 +100,28 @@ jobs:
workflow_id: 479426
```

## Advanced: Cancel more recent workflows

Because this action can only cancel workflows if it is actually being run, it only helps if the pipeline isn't saturated and there are still runners available to schedule the workflow.
By default, this action does not cancel any workflows older than itself. The optional flag ``all_but_latest`` switches to a mode where the action also cancels itself and all later-scheduled workflows but the last one.

```yml
name: Cancel
on: [push]
jobs:
cancel:
name: 'Cancel Previous Runs'
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- uses: styfle/cancel-workflow-action@0.8.0
with:
all_but_latest: true
access_token: ${{ github.token }}
```

At the time of writing `0.8.0` is the latest release but you can select any [release](https://github.com/styfle/cancel-workflow-action/releases).

## Contributing

- Clone this repo
Expand Down
6 changes: 5 additions & 1 deletion action.yml
Expand Up @@ -2,7 +2,7 @@ name: 'Cancel Workflow Action'
description: 'This Action will cancel any previous runs that are not `completed` for a given workflow.'
author: styfle
branding:
icon: 'stop-circle'
icon: 'stop-circle'
color: 'white'
inputs:
workflow_id:
Expand All @@ -15,6 +15,10 @@ inputs:
access_token:
description: 'Your GitHub Access Token, defaults to: {{ github.token }}'
default: '${{ github.token }}'
required: true
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should probably no longer have required:

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah it is not actually in 0.8.0? that requirement was removed. Also in the "TIL" department from #59 I learned that YAML won't consider it a string anyway, unless it's in quotes, and the Actions spec says those should be strings. Who knew?

all_but_latest:
description: "Cancel all actions but the last one"
required: false
runs:
using: 'node12'
main: 'dist/index.js'
38 changes: 27 additions & 11 deletions src/index.ts
Expand Up @@ -27,6 +27,7 @@ async function main() {
const token = core.getInput('access_token', { required: true });
const workflow_id = 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 });
console.log(`Found token: ${token ? 'yes' : 'no'}`);
const workflow_ids: string[] = [];
const octokit = github.getOctokit(token);
Expand Down Expand Up @@ -58,20 +59,35 @@ async function main() {
workflow_id,
branch,
});
console.log(`Found ${data.total_count} runs total.`);

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'));
Comment on lines -62 to -64
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the PR would be easier to review if this change was reverted and the filter below was not done in one big combination, but done separately as before. Then this whole part of the diff would disappear and the filter below would be simpler? That said, it doesn't look incorrect

let cancel_before;
if (all_but_latest) {
cancel_before = new Date(data.workflow_runs.reduce((a, b) => Math.max(a.created_at, b.created_at)));
} else {
cancel_before = new Date(current_run.created_at);
}

const runningWorkflows = branchWorkflows.filter(run =>
(ignore_sha || run.head_sha !== headSha) &&
run.status !== 'completed' &&
new Date(run.created_at) < new Date(current_run.created_at)
const runningWorkflows = data.workflow_runs.filter(
run => run.head_branch === branch && (ignore_sha || run.head_sha !== headSha) && run.status !== 'completed' &&
run != current_run &&
new Date(run.created_at) < cancel_before
);
console.log(`with ${runningWorkflows.length} runs to cancel.`);

for (const {id, head_sha, status, html_url} of runningWorkflows) {
console.log('Canceling run: ', {id, head_sha, status, html_url});
console.log(`Found ${runningWorkflows.length} runs to cancel.`);
for (const {id, head_sha, status} of runningWorkflows) {
console.log('Cancelling another run: ', {id, head_sha, status});
styfle marked this conversation as resolved.
Show resolved Hide resolved
const res = await octokit.actions.cancelWorkflowRun({
owner,
repo,
run_id: id
});
console.log(`Cancel run ${id} responded with status ${res.status}`);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR would be easier to review I think if there was one very focused commit separate from the others that extracted this cancel logic to separate method - then it would not be duplicated below when it is time to cancel the current workflow. Bonus points for ingesting the idea from #59 if the extracted method was not actually a single cancel but took an array of IDs / pushed the Promise result from each cancel on an array / did the Promise.all thing to alleviate some of the "github responds 202 but doesn't actually cancel right away..." issue https://github.com/styfle/cancel-workflow-action/pull/59/files#diff-3d2b59189eeedc2d428ddd632e97658fe310f587f7cb63b01f9b98ffc11c0197R5903-R5914

// Make sure we cancel this run itself if it's out-of-date.
// 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) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the purpose of this if statement?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't have a run cancel itself before it has had a chance to cancel others. However, I think https://github.com/styfle/cancel-workflow-action/pull/35/files#diff-a2a171449d862fe29692ce031981047d7ab755ae7f84c707aef80701b3ea0c80R69 is a bit sloppy and might break this action if you're not using all_but_latest...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, no, if you're not all_but_latest you will never cancel current_run.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would this run cancel itself? Shouldn't it always allow itself to run? Otherwise you would have nothing running, everything would cancel everything.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should cancel itself if it's not the most recent run

// FIXME actions/core doesn't support cancelling, so we need to do it through the API
thomwiggers marked this conversation as resolved.
Show resolved Hide resolved
const id = current_run.id;
const res = await octokit.actions.cancelWorkflowRun({
owner,
repo,
Expand Down