Skip to content

Commit

Permalink
Mikehardy/all but latest (#1)
Browse files Browse the repository at this point in the history
* chore: add prettier config, format file, add lint workflow

The prettier config was adapted from the official GitHub Actions repo,
bent to fit the prevailing style (where possible) already in the project

The intent is not to be controversial or argue about whitespace, it is just
to have a consistent easy-to-verify style specifically to avoid all arguments
about whitespace. If anything in here is objectionable, just name the setting
to alter and I can edit / re-format / re-push

* chore(lint): add eslint config, check lint in workflow

Similar to the prettier config, this is adapted from the main GitHub Actions repo,
and the intent is not to be controversial it is simply to have any consistent / easy
to check standard. If anything is objectionable, just point out the setting

The config was adapted from the main repo to match what appeared to be prevailing opinion
on this project (semicolons preferred, bracket spacing preferred, etc)

The following commits will fix the small errors that seemed worth fixing

* lint: add explicit return type on main() method

* lint: use for..of vs array.forEach

* lint: workflow_id variable was shadowed, make it unambiguous

* lint: remote ts-ignore via typing workflow_id as number in all places

* chore: add husky and hook build/format/lint checks to pre-commit

This enforces the same checks locally that will execute in CI

With this, everyone should have a clean / consistent dev environment,
and it will be clear to contributors if they submit code that is not valid
typescript

Additionally, after doing the build it adds the dist/index.js output to the
commit list so contributors can't forget to commit it

* fix(ignore_sha): change default from false to 'false'

This was noted by @Gisleburt here styfle#59 (comment)

According to the spec the parameter should be a string, but if you use false without
quotes in YAML it is taken as a boolean so the types are not quite correct

* refactor: extraction of cancel workflow runs method

this is intended to be completely non-functional, nothing should be different
here in how this works from before, it is a pure method extraction

testing this change:
the cancel_self workflow action was updated so it may be run manually, and
the no-longer-required access_token was removed from it

* perf: cancel workflow runs in parallel

This was suggested by @Gisleburt in styfle#59

I quote:

"The next problem we has is that we have multiple jobs starting at once, downloading a list of other jobs and trying to cancel them one at a time. GitHub doesn't wait for the job to be cancelled before returning a 202 response so its possible for two jobs to cancel each other. In order to reduce the chance of this happening we decided to send all of the cancellations in one go, and wait for the 202s in one lump at the end. This change will improve the speed of the task for everyone with more than one workflow to cancel."

* fix: list maximum (100) workflows possible before paging

This was suggested by @Gislebert in styfle#59

the default was 30, this widens the number of workflows we scan to
cancel the most workflows possible, without adding complication by paging

* refactor: break workflow run filter into multiple parts

this will allow the cancel all but latest feature to be easier to review

* feat(all_but_latest): add ability to clear every run but latest

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 3d86a7c commit 0d42688
Show file tree
Hide file tree
Showing 14 changed files with 3,079 additions and 94 deletions.
2 changes: 2 additions & 0 deletions .eslintignore
@@ -0,0 +1,2 @@
dist/
node_modules/
59 changes: 59 additions & 0 deletions .eslintrc.json
@@ -0,0 +1,59 @@
{
"plugins": ["jest", "@typescript-eslint"],
"extends": ["plugin:github/es6"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 9,
"sourceType": "module",
"project": "./tsconfig.json"
},
"rules": {
"eslint-comments/no-use": "off",
"import/no-namespace": "off",
"no-unused-vars": "off",
"no-console": "off",
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}],
"@typescript-eslint/no-require-imports": "error",
"@typescript-eslint/array-type": "error",
"@typescript-eslint/await-thenable": "error",
"@typescript-eslint/ban-ts-ignore": "error",
"camelcase": "off",
"@typescript-eslint/class-name-casing": "error",
"@typescript-eslint/explicit-function-return-type": ["error", {"allowExpressions": true}],
"@typescript-eslint/func-call-spacing": ["error", "never"],
"@typescript-eslint/generic-type-naming": ["error", "^[A-Z][A-Za-z]*$"],
"@typescript-eslint/no-array-constructor": "error",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-extraneous-class": "error",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-for-in-array": "error",
"@typescript-eslint/no-inferrable-types": "error",
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-non-null-assertion": "warn",
"@typescript-eslint/no-object-literal-type-assertion": "error",
"@typescript-eslint/no-unnecessary-qualifier": "error",
"@typescript-eslint/no-unnecessary-type-assertion": "error",
"@typescript-eslint/no-useless-constructor": "error",
"@typescript-eslint/no-var-requires": "error",
"@typescript-eslint/prefer-for-of": "warn",
"@typescript-eslint/prefer-function-type": "warn",
"@typescript-eslint/prefer-includes": "error",
"@typescript-eslint/prefer-interface": "error",
"@typescript-eslint/prefer-string-starts-ends-with": "error",
"@typescript-eslint/promise-function-async": "error",
"@typescript-eslint/require-array-sort-compare": "error",
"@typescript-eslint/restrict-plus-operands": "error",
"semi": "off",
"@typescript-eslint/semi": ["error", "always"],
"@typescript-eslint/type-annotation-spacing": "error",
"@typescript-eslint/unbound-method": "error"
},
"env": {
"node": true,
"es6": true,
"jest/globals": true
}
}
21 changes: 21 additions & 0 deletions .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.';
8 changes: 4 additions & 4 deletions .github/workflows/cancel-self.yml
@@ -1,6 +1,8 @@
name: Cancel Self

on: [push]
on:
push:
workflow_dispatch:

jobs:
task:
Expand All @@ -9,9 +11,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2
- uses: actions/setup-node@v2
- name: Test Step
uses: ./ # Uses an action in the root directory
with:
access_token: ${{ github.token }}
- uses: actions/setup-node@v1
- run: echo 'Sleeping...'; sleep 120; echo 'Done.';
19 changes: 19 additions & 0 deletions .github/workflows/lint.yml
@@ -0,0 +1,19 @@
name: Build and Analyze

on:
pull_request:
push:
workflow_dispatch:

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v2
with:
node-version: v14
- uses: actions/checkout@v2
- run: yarn
- run: yarn build
- run: yarn format-check
- run: yarn lint
4 changes: 4 additions & 0 deletions .husky/pre-commit
@@ -0,0 +1,4 @@
#!/bin/sh
yarn build && git add dist/index.js
yarn format-check
yarn lint
2 changes: 2 additions & 0 deletions .prettierignore
@@ -0,0 +1,2 @@
dist/
node_modules/
11 changes: 11 additions & 0 deletions .prettierrc.json
@@ -0,0 +1,11 @@
{
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"trailingComma": "none",
"bracketSpacing": true,
"arrowParens": "avoid",
"parser": "typescript"
}
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 @@ -11,7 +11,10 @@ inputs:
ignore_sha:
description: 'Optional - Allow canceling other workflows with the same SHA. Useful for the `pull_request.closed` event.'
required: false
default: 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
66 changes: 48 additions & 18 deletions dist/index.js
Expand Up @@ -5862,8 +5862,9 @@ async function main() {
}
console.log({ eventName, sha, headSha, branch, owner, repo, GITHUB_RUN_ID });
const token = core.getInput('access_token', { required: true });
const workflow_id = core.getInput('workflow_id', { required: false });
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 All @@ -5872,38 +5873,49 @@ async function main() {
repo,
run_id: Number(GITHUB_RUN_ID)
});
if (workflow_id) {
workflow_id.replace(/\s/g, '')
.split(',')
.forEach(n => workflow_ids.push(n));
if (workflow_id_input) {
const workflow_ids_input = workflow_id_input.replace(/\s/g, '').split(',');
for (const id of workflow_ids_input) {
workflow_ids.push(parseInt(id, 10));
}
}
else {
workflow_ids.push(String(current_run.workflow_id));
workflow_ids.push(current_run.workflow_id);
}
console.log(`Found workflow_id: ${JSON.stringify(workflow_ids)}`);
await Promise.all(workflow_ids.map(async (workflow_id) => {
try {
const { data } = await octokit.actions.listWorkflowRuns({
per_page: 100,
owner,
repo,
workflow_id,
branch,
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'));
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));
let runningWorkflows = branchWorkflows.filter(run => run.status !== 'completed');
runningWorkflows = runningWorkflows.filter(run => ignore_sha || run.head_sha !== headSha);
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.`);
for (const { id, head_sha, status, html_url } of runningWorkflows) {
console.log('Canceling run: ', { id, head_sha, status, html_url });
const res = await octokit.actions.cancelWorkflowRun({
owner,
repo,
run_id: id
});
console.log(`Cancel run ${id} responded with status ${res.status}`);
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) {
Expand All @@ -5913,6 +5925,24 @@ async function main() {
console.log('');
}));
}
async function cancelWorkflowRuns(runningWorkflows, owner, repo, token) {
const octokit = github.getOctokit(token);
const promises = [];
for (const { id, head_sha, status, html_url } of runningWorkflows) {
console.log('Canceling run: ', { id, head_sha, status, html_url });
const current_promise = octokit.actions
.cancelWorkflowRun({
owner,
repo,
run_id: id
})
.then(res => {
console.log(`Cancel run ${id} responded with status ${res.status}`);
});
promises.push(current_promise);
}
await Promise.all(promises);
}
main()
.then(() => core.info('Cancel Complete.'))
.catch(e => core.setFailed(e.message));
Expand Down
14 changes: 12 additions & 2 deletions package.json
Expand Up @@ -4,14 +4,24 @@
"main": "dist/index.js",
"license": "MIT",
"scripts": {
"build": "ncc build src/index.ts --license LICENSES.txt"
"build": "ncc build src/index.ts --license LICENSES.txt",
"format": "prettier --write '**/*.ts'",
"format-check": "prettier --check '**/*.ts'",
"lint": "eslint src/**/*.ts",
"prepare": "husky install"
},
"dependencies": {
"@actions/core": "1.2.6",
"@actions/github": "4.0.0"
},
"devDependencies": {
"@typescript-eslint/parser": "^2.8.0",
"@vercel/ncc": "0.27.0",
"typescript": "4.1.5"
"eslint": "^5.16.0",
"eslint-plugin-github": "^2.0.0",
"eslint-plugin-jest": "^22.21.0",
"prettier": "2.2.1",
"typescript": "4.1.5",
"husky": "^6.0.0"
}
}

0 comments on commit 0d42688

Please sign in to comment.