You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by po...@apache.org on 2020/12/27 13:52:42 UTC

[airflow-cancel-workflow-runs] 26/44: Adds cancelling of latest job when certain jobs failed

This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airflow-cancel-workflow-runs.git

commit f0bf36ae0bb733d9cdd659316aaf3d2003b09366
Author: Jarek Potiuk <ja...@potiuk.com>
AuthorDate: Sat Jul 25 20:57:09 2020 +0200

    Adds cancelling of latest job when certain jobs failed
---
 .gitignore    |   4 +-
 README.md     |  82 ++++++--
 action.yml    |  14 +-
 dist/index.js | 635 ++++++++++++++++++++++++++++++++++++----------------------
 src/main.ts   | 422 ++++++++++++++++++++++++--------------
 5 files changed, 745 insertions(+), 412 deletions(-)

diff --git a/.gitignore b/.gitignore
index 18e337d..0a04613 100644
--- a/.gitignore
+++ b/.gitignore
@@ -96,4 +96,6 @@ Thumbs.db
 
 # Ignore built ts files
 __tests__/runner/*
-lib/**/*
\ No newline at end of file
+lib/**/*
+
+.idea
diff --git a/README.md b/README.md
index 1448802..5c77714 100644
--- a/README.md
+++ b/README.md
@@ -1,24 +1,79 @@
-# cancel-previous-runs 
-This action cancels previous runs for one or more branches/prs associated with a workflow, effectively limiting the resource consumption of the workflow to one per branch.
+# cancel-workflow-runs
+This action cancels runs for one or more branches/prs associated with a workflow,
+effectively limiting the resource consumption of the workflow to one per branch.
 
-<p><a href="https://github.com/actions/typescript-action/actions"><img alt="typescript-action status" src="https://github.com/actions/typescript-action/workflows/build-test/badge.svg"></a>
+It also cancels workflows from the latest workflow run if specified jobs failed.
+That allows to further limit the resource usage of running workflows, without
+impacting the elapsed time of successful workflow runs. Typical behaviour of
+the Github Actions Workflow is that the success/failure propagation between the jobs
+happens through job dependency graph (needs: in the GA yaml). However, there are cases
+where you want to start some jobs without waiting for other jobs to succeed, yet if
+the other jobs fail, you want to cancel the whole workflow. It's similar to
+fail-fast behaviour of the matrix builds.
+
+Since cancelling workflow does not work from "fork" pull requests for security reasons,
+the capability of canceling the workflows should be built in the scheduled task.
+
+<p><a href="https://github.com/actions/typescript-action/actions">
+<img alt="typescript-action status"
+    src="https://github.com/actions/typescript-action/workflows/build-test/badge.svg"></a>
+
+I based the implementation of this action on the
+[n1hility action](https://github.com/n1hility/cancel-previous-runs) to cancel the previous runs only.
 
 ## Usage
 
-The easiest and most complete approach to utilize this action, is to create a separate schedule event triggered workflow, which is directed at the workflow you wish to clear duplicate runs. At each cron interval all branches and all PRs executing for either push or pull_request events will be processed and limited to one run per branch/pr.
+The easiest and most complete approach to utilize this action, is to create a separate schedule event
+triggered workflow, which is directed at the workflow you wish to clear duplicate runs.
+At each cron interval all branches and all PRs executing for either push or pull_request events
+will be processed and limited to one run per branch/pr.
 
-Additionally this action can be placed as an early step in your workflow (e.g. after checkout), so that it can abort the other previously running jobs immediately, in case most resources are tied up. Unfortunately this approach is a no-op when a pull request uses a fork for a source branch. This is because the GITHUB_TOKEN provided to runs with a fork source branch specifies reed-only permissions for security reasons. write permissions are required to be able to cancel a job. Therefore,  [...]
+Additionally, this action can be placed as an early step in your workflow (e.g. after the checkout), so
+that it can abort the other previously running jobs immediately, in case the workflows tie up most resources.
+Unfortunately this approach is a no-op when a pull request uses a fork for a source branch.
+This is because the GITHUB_TOKEN provided to runs with a fork source branch specifies reed-only
+permissions for security reasons. You need write permissions to be able to cancel a job.
+Therefore, it's a good idea to only rely on this approach as a fallback in-addition to the previously
+described scheduling model.
 
 ### Inputs
 
-token - The github token passed from `${{ secrets.GITHUB_TOKEN }}`. Since workflow files are visible in the repository, **DO NOT HARDCODE A TOKEN ONLY USE A REFERENCE**. 
-workflow - The filename of the workflow to limit runs on (only applies to schedule events) 
-
+* token - The github token passed from `${{ secrets.GITHUB_TOKEN }}`. Since workflow files are visible
+  in the repository, **DO NOT HARDCODE A TOKEN ONLY USE A REFERENCE**.
+* workflow - The filename of the workflow to limit runs on (only applies to schedule events)
+* failFastJobNames - optional array of job name regexps. If a job name that matches any of the regexp fails
+  in the most recent run, this causes a fail-fast of the run. This can be used if you want to run jobs
+  in parallel but kill them as soon as some of those jobs fail - effectively turning them into "fail-fast"
+  type of jobs. Note these are job names after interpolation of workflow variables - so you have to make sure that
+  you use the name as displayed in the status of the workflow or use regexp to
+  match the names.
 
 ### Schedule Example
 
 ```yaml
-name: Cleanup Duplicate Branches and PRs  
+name: Cleanup Duplicate Branches and PRs
+on:
+  schedule:
+    - cron:  '*/15 * * * *'
+cancel-runs:
+  # Prevent forks from running this to be nice
+  if: github.repository == 'foo-org/my-repo'
+  runs-on: ubuntu-latest
+    steps:
+      - uses: n1hility/cancel-previous-runs@v2
+        with:
+          token: ${{ secrets.GITHUB_TOKEN }}
+          workflow: my-heavy-workflow.yml
+```
+
+
+### Schedule Example with fail-fast
+
+This kills all previous runs of the workflow, and also latest run if one of the jobs
+matching `^Static checks$` and `^Build docs^` or `^Build prod image .*` regexp failed in it.
+
+```yaml
+name: Cleanup Duplicate Branches and fail-fast errors
 on:
   schedule:
     - cron:  '*/15 * * * *'
@@ -28,23 +83,24 @@ cancel-runs:
   runs-on: ubuntu-latest
     steps:
       - uses: n1hility/cancel-previous-runs@v2
-        with: 
+        with:
           token: ${{ secrets.GITHUB_TOKEN }}
           workflow: my-heavy-workflow.yml
+          failFastJobNames: '["^Static checks$", "^Build docs$", "^Build prod image.*"]'
 ```
 
 
 ### Alternate/Fallback Example
 
 ```yaml
-  test: 
+  test:
     runs-on: ubuntu-latest
     steps:
     - uses: actions/checkout@v1
     - uses: n1hility/cancel-previous-runs@v2
-      with: 
+      with:
         token: ${{ secrets.GITHUB_TOKEN }}
 ```
 
 ## License
-The scripts and documentation in this project are released under the [MIT License](LICENSE)
+[MIT License](LICENSE) covers the scripts and documentation in this project.
diff --git a/action.yml b/action.yml
index e0d9c6d..1b2515d 100644
--- a/action.yml
+++ b/action.yml
@@ -1,12 +1,16 @@
-name: 'Cancel Previous Workflow Runs'
-description: 'Cancels all previous runs of this workflow'
-author: 'n1hility'
+name: 'Cancel Workflow Runs'
+description: 'Cancels previous runs and optionally failed runs of a workflow'
+author: 'potiuk'
 inputs:
   token:
-    description: The GITHUB_TOKEN secret of this github workflow
+    description: The GITHUB_TOKEN secret of the repository
     required: true
   workflow:
-    description: The filename of the workflow to limit runs on (only applies to schedule events)
+    description: The filename of the workflow to limit runs on (only applies to scheduled events)
+    required: false
+  failFastJobNames:
+    description: |
+      Array of job names (JSON-encoded string). Failures of those jobs are fail-fast of whole workflow.
     required: false
 runs:
   using: 'node12'
diff --git a/dist/index.js b/dist/index.js
index 5505475..302be03 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -34,7 +34,7 @@ module.exports =
 /******/ 	// the startup function
 /******/ 	function startup() {
 /******/ 		// Load entry module and return exports
-/******/ 		return __webpack_require__(131);
+/******/ 		return __webpack_require__(198);
 /******/ 	};
 /******/
 /******/ 	// run startup
@@ -1434,208 +1434,6 @@ module.exports = require("child_process");
 
 /***/ }),
 
-/***/ 131:
-/***/ (function(__unusedmodule, exports, __webpack_require__) {
-
-"use strict";
-
-var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
-    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
-    return new (P || (P = Promise))(function (resolve, reject) {
-        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
-        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
-        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
-        step((generator = generator.apply(thisArg, _arguments || [])).next());
-    });
-};
-var __asyncValues = (this && this.__asyncValues) || function (o) {
-    if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
-    var m = o[Symbol.asyncIterator], i;
-    return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
-    function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
-    function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
-};
-var __importStar = (this && this.__importStar) || function (mod) {
-    if (mod && mod.__esModule) return mod;
-    var result = {};
-    if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
-    result["default"] = mod;
-    return result;
-};
-Object.defineProperty(exports, "__esModule", { value: true });
-const github = __importStar(__webpack_require__(469));
-const core = __importStar(__webpack_require__(393));
-const treemap = __importStar(__webpack_require__(706));
-function createRunsQuery(octokit, owner, repo, workflowId, status, branch, event) {
-    const request = branch === undefined
-        ? {
-            owner,
-            repo,
-            // eslint-disable-next-line @typescript-eslint/camelcase
-            workflow_id: workflowId,
-            status
-        }
-        : {
-            owner,
-            repo,
-            // eslint-disable-next-line @typescript-eslint/camelcase
-            workflow_id: workflowId,
-            status,
-            branch,
-            event
-        };
-    return octokit.actions.listWorkflowRuns.endpoint.merge(request);
-}
-function cancelDuplicates(token, selfRunId, owner, repo, workflowId, branch, event) {
-    var e_1, _a;
-    return __awaiter(this, void 0, void 0, function* () {
-        const octokit = new github.GitHub(token);
-        // Deteermind the workflow to reduce the result set, or reference anothre workflow
-        let resolvedId = '';
-        if (workflowId === undefined) {
-            const reply = yield octokit.actions.getWorkflowRun({
-                owner,
-                repo,
-                // eslint-disable-next-line @typescript-eslint/camelcase
-                run_id: Number.parseInt(selfRunId)
-            });
-            resolvedId = reply.data.workflow_url.split('/').pop() || '';
-            if (!(resolvedId.length > 0)) {
-                throw new Error('Could not resolve workflow');
-            }
-        }
-        else {
-            resolvedId = workflowId;
-        }
-        core.info(`Workflow ID is: ${resolvedId}`);
-        // eslint-disable-next-line @typescript-eslint/no-explicit-any
-        const sorted = new treemap.TreeMap();
-        for (const status of ['queued', 'in_progress']) {
-            const listRuns = createRunsQuery(octokit, owner, repo, resolvedId, status, branch, event);
-            try {
-                for (var _b = __asyncValues(octokit.paginate.iterator(listRuns)), _c; _c = yield _b.next(), !_c.done;) {
-                    const item = _c.value;
-                    // There is some sort of bug where the pagination URLs point to a
-                    // different endpoint URL which trips up the resulting representation
-                    // In that case, fallback to the actual REST 'workflow_runs' property
-                    const elements = item.data.length === undefined ? item.data.workflow_runs : item.data;
-                    for (const element of elements) {
-                        sorted.set(element.run_number, element);
-                    }
-                }
-            }
-            catch (e_1_1) { e_1 = { error: e_1_1 }; }
-            finally {
-                try {
-                    if (_c && !_c.done && (_a = _b.return)) yield _a.call(_b);
-                }
-                finally { if (e_1) throw e_1.error; }
-            }
-        }
-        // If a workflow was provided process everything
-        let matched = workflowId !== undefined;
-        const heads = new Set();
-        for (const entry of sorted.backward()) {
-            const element = entry[1];
-            core.info(`${element.id} : ${element.workflow_url} : ${element.status} : ${element.run_number}`);
-            if (!matched) {
-                if (element.id.toString() !== selfRunId) {
-                    // Skip everything up to this run
-                    continue;
-                }
-                matched = true;
-                core.info(`Matched ${selfRunId}`);
-            }
-            if ('completed' === element.status.toString() ||
-                !['push', 'pull_request'].includes(element.event.toString())) {
-                continue;
-            }
-            // This is a set of one in the non-schedule case, otherwise everything is a candidate
-            const head = `${element.head_repository.full_name}/${element.head_branch}`;
-            if (!heads.has(head)) {
-                core.info(`First: ${head}`);
-                heads.add(head);
-                continue;
-            }
-            core.info(`Cancelling: ${head}`);
-            yield cancelRun(octokit, owner, repo, element.id);
-        }
-    });
-}
-function run() {
-    return __awaiter(this, void 0, void 0, function* () {
-        try {
-            const token = core.getInput('token');
-            core.info(token);
-            const selfRunId = getRequiredEnv('GITHUB_RUN_ID');
-            const repository = getRequiredEnv('GITHUB_REPOSITORY');
-            const eventName = getRequiredEnv('GITHUB_EVENT_NAME');
-            const [owner, repo] = repository.split('/');
-            const branchPrefix = 'refs/heads/';
-            const tagPrefix = 'refs/tags/';
-            if ('schedule' === eventName) {
-                const workflowId = core.getInput('workflow');
-                if (!(workflowId.length > 0)) {
-                    throw new Error('Workflow must be specified for schedule event type');
-                }
-                yield cancelDuplicates(token, selfRunId, owner, repo, workflowId);
-                return;
-            }
-            if (!['push', 'pull_request'].includes(eventName)) {
-                core.info('Skipping unsupported event');
-                return;
-            }
-            const pullRequest = 'pull_request' === eventName;
-            let branch = getRequiredEnv(pullRequest ? 'GITHUB_HEAD_REF' : 'GITHUB_REF');
-            if (!pullRequest && !branch.startsWith(branchPrefix)) {
-                if (branch.startsWith(tagPrefix)) {
-                    core.info(`Skipping tag build`);
-                    return;
-                }
-                const message = `${branch} was not an expected branch ref (refs/heads/).`;
-                throw new Error(message);
-            }
-            branch = branch.replace(branchPrefix, '');
-            core.info(`Branch is ${branch}, repo is ${repo}, and owner is ${owner}, and id is ${selfRunId}`);
-            cancelDuplicates(token, selfRunId, owner, repo, undefined, branch, eventName);
-        }
-        catch (error) {
-            core.setFailed(error.message);
-        }
-    });
-}
-function cancelRun(
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-octokit, owner, repo, id) {
-    return __awaiter(this, void 0, void 0, function* () {
-        let reply;
-        try {
-            reply = yield octokit.actions.cancelWorkflowRun({
-                owner,
-                repo,
-                // eslint-disable-next-line @typescript-eslint/camelcase
-                run_id: id
-            });
-            core.info(`Previous run (id ${id}) cancelled, status = ${reply.status}`);
-        }
-        catch (error) {
-            core.info(`[warn] Could not cancel run (id ${id}): [${error.status}] ${error.message}`);
-        }
-    });
-}
-function getRequiredEnv(key) {
-    const value = process.env[key];
-    if (value === undefined) {
-        const message = `${key} was not defined.`;
-        throw new Error(message);
-    }
-    return value;
-}
-run();
-
-
-/***/ }),
-
 /***/ 141:
 /***/ (function(__unusedmodule, exports, __webpack_require__) {
 
@@ -2190,6 +1988,311 @@ function checkMode (stat, options) {
 
 /***/ }),
 
+/***/ 198:
+/***/ (function(__unusedmodule, exports, __webpack_require__) {
+
+"use strict";
+
+var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
+    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
+    return new (P || (P = Promise))(function (resolve, reject) {
+        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
+        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
+        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
+        step((generator = generator.apply(thisArg, _arguments || [])).next());
+    });
+};
+var __asyncValues = (this && this.__asyncValues) || function (o) {
+    if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
+    var m = o[Symbol.asyncIterator], i;
+    return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
+    function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
+    function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
+};
+var __importStar = (this && this.__importStar) || function (mod) {
+    if (mod && mod.__esModule) return mod;
+    var result = {};
+    if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
+    result["default"] = mod;
+    return result;
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+const github = __importStar(__webpack_require__(469));
+const core = __importStar(__webpack_require__(393));
+const treemap = __importStar(__webpack_require__(706));
+function createListRunsQueryForAllRuns(octokit, owner, repo, workflowId, status) {
+    const request = {
+        owner,
+        repo,
+        // eslint-disable-next-line @typescript-eslint/camelcase
+        workflow_id: workflowId,
+        status
+    };
+    return octokit.actions.listWorkflowRuns.endpoint.merge(request);
+}
+function createListRunsQueryForSelfRun(octokit, owner, repo, workflowId, status, branch, eventName) {
+    const request = {
+        owner,
+        repo,
+        // eslint-disable-next-line @typescript-eslint/camelcase
+        workflow_id: workflowId,
+        status,
+        branch,
+        event: eventName
+    };
+    return octokit.actions.listWorkflowRuns.endpoint.merge(request);
+}
+function createJobsForWorkflowRunQuery(octokit, owner, repo, runId) {
+    const request = {
+        owner,
+        repo,
+        // eslint-disable-next-line @typescript-eslint/camelcase
+        run_id: runId,
+    };
+    return octokit.actions.listJobsForWorkflowRun.endpoint.merge(request);
+}
+function cancelOnFailFastJobsFailed(octokit, owner, repo, runId, head, failFastJobNames) {
+    var e_1, _a;
+    return __awaiter(this, void 0, void 0, function* () {
+        const listJobs = createJobsForWorkflowRunQuery(octokit, owner, repo, runId);
+        core.info(`Cancelling runId ${runId} in case one of the ${failFastJobNames} failed`);
+        try {
+            for (var _b = __asyncValues(octokit.paginate.iterator(listJobs)), _c; _c = yield _b.next(), !_c.done;) {
+                const item = _c.value;
+                for (const job of item.data.jobs) {
+                    core.info(`The job name: ${job.name}, Conclusion: ${job.conclusion}`);
+                    if (job.conclusion == 'failure' &&
+                        failFastJobNames.some(jobNameRegexp => job.name.match(jobNameRegexp))) {
+                        core.info(`Job ${job.name} has failed and it matches one of the ${failFastJobNames} regexps`);
+                        core.info(`Cancelling the workflow run: ${runId}, head: ${head}`);
+                        yield cancelRun(octokit, owner, repo, runId);
+                        return;
+                    }
+                }
+            }
+        }
+        catch (e_1_1) { e_1 = { error: e_1_1 }; }
+        finally {
+            try {
+                if (_c && !_c.done && (_a = _b.return)) yield _a.call(_b);
+            }
+            finally { if (e_1) throw e_1.error; }
+        }
+    });
+}
+function getSelfWorkflowId(octokit, selfRunId, owner, repo) {
+    return __awaiter(this, void 0, void 0, function* () {
+        let workflowId;
+        const reply = yield octokit.actions.getWorkflowRun({
+            owner,
+            repo,
+            // eslint-disable-next-line @typescript-eslint/camelcase
+            run_id: Number.parseInt(selfRunId)
+        });
+        workflowId = reply.data.workflow_url.split('/').pop() || '';
+        if (!(workflowId.length > 0)) {
+            throw new Error('Could not resolve workflow');
+        }
+        return workflowId;
+    });
+}
+function getSortedWorkflowRuns(octokit, createListRunQuery) {
+    var e_2, _a;
+    return __awaiter(this, void 0, void 0, function* () {
+        const sortedWorkflowRuns = new treemap.TreeMap();
+        for (const status of ['queued', 'in_progress']) {
+            const listRuns = yield createListRunQuery(status);
+            try {
+                for (var _b = __asyncValues(octokit.paginate.iterator(listRuns)), _c; _c = yield _b.next(), !_c.done;) {
+                    const item = _c.value;
+                    // There is some sort of bug where the pagination URLs point to a
+                    // different endpoint URL which trips up the resulting representation
+                    // In that case, fallback to the actual REST 'workflow_runs' property
+                    const elements = item.data.length === undefined ? item.data.workflow_runs : item.data;
+                    for (const element of elements) {
+                        sortedWorkflowRuns.set(element.run_number, element);
+                    }
+                }
+            }
+            catch (e_2_1) { e_2 = { error: e_2_1 }; }
+            finally {
+                try {
+                    if (_c && !_c.done && (_a = _b.return)) yield _a.call(_b);
+                }
+                finally { if (e_2) throw e_2.error; }
+            }
+        }
+        core.info(`Found runs: ${Array.from(sortedWorkflowRuns.backward()).map(t => t[0])}`);
+        return sortedWorkflowRuns;
+    });
+}
+function shouldRunBeSkipped(runItem) {
+    if ('completed' === runItem.status.toString()) {
+        core.info(`Skip completed run: ${runItem.id}`);
+        return true;
+    }
+    if (!['push', 'pull_request'].includes(runItem.event.toString())) {
+        core.info(`Skip run: ${runItem.id} as it is neither push nor pull_request (${runItem.event}`);
+        return true;
+    }
+    return false;
+}
+function cancelRun(octokit, owner, repo, id) {
+    return __awaiter(this, void 0, void 0, function* () {
+        let reply;
+        try {
+            reply = yield octokit.actions.cancelWorkflowRun({
+                owner: owner,
+                repo: repo,
+                // eslint-disable-next-line @typescript-eslint/camelcase
+                run_id: id
+            });
+            core.info(`Previous run (id ${id}) cancelled, status = ${reply.status}`);
+        }
+        catch (error) {
+            core.info(`[warn] Could not cancel run (id ${id}): [${error.status}] ${error.message}`);
+        }
+    });
+}
+// Kills past runs for my own workflow.
+function findAndCancelPastRunsForSelf(octokit, selfRunId, owner, repo, branch, eventName) {
+    return __awaiter(this, void 0, void 0, function* () {
+        core.info(`findAndCancelPastRunsForSelf:  ${selfRunId}, ${owner}, ${repo}, ${branch}, ${eventName}`);
+        const workflowId = yield getSelfWorkflowId(octokit, selfRunId, owner, repo);
+        core.info(`My own workflow ID is: ${workflowId}`);
+        const sortedWorkflowRuns = yield getSortedWorkflowRuns(octokit, function (status) {
+            return createListRunsQueryForSelfRun(octokit, owner, repo, workflowId, status, branch, eventName);
+        });
+        let matched = false;
+        const headsToRunIdMap = new Map();
+        for (const [key, runItem] of sortedWorkflowRuns.backward()) {
+            core.info(`Run number: ${key}, RunId: ${runItem.id}, URL: ${runItem.workflow_url}. Status ${runItem.status}`);
+            if (!matched) {
+                if (runItem.id.toString() !== selfRunId) {
+                    core.info(`Skip run ${runItem.id} as it was started before my own id: ${selfRunId}`);
+                    continue;
+                }
+                matched = true;
+                core.info(`Matched ${selfRunId}. Reached my own ID, now looping through all remaining runs/`);
+                core.info("I will cancel all except the first for each 'head' available");
+            }
+            if (shouldRunBeSkipped(runItem)) {
+                continue;
+            }
+            // Head of the run
+            const head = `${runItem.head_repository.full_name}/${runItem.head_branch}`;
+            if (!headsToRunIdMap.has(head)) {
+                core.info(`First run for the head: ${head}. Skipping it. Next ones with same head will be cancelled.`);
+                headsToRunIdMap.set(head, runItem.id);
+                continue;
+            }
+            core.info(`Cancelling run: ${runItem.id}, head ${head}.`);
+            core.info(`There is a later run with same head: ${headsToRunIdMap.get(head)}`);
+            yield cancelRun(octokit, owner, repo, runItem.id);
+        }
+    });
+}
+// Kills past runs for my own workflow.
+function findAndCancelPastRunsForSchedule(octokit, workflowId, owner, repo, failFastJobNames) {
+    return __awaiter(this, void 0, void 0, function* () {
+        core.info(`findAndCancelPastRunsForSchedule: ${owner}, ${workflowId}, ${repo}`);
+        const sortedWorkflowRuns = yield getSortedWorkflowRuns(octokit, function (status) {
+            return createListRunsQueryForAllRuns(octokit, owner, repo, workflowId, status);
+        });
+        const headsToRunIdMap = new Map();
+        for (const [key, runItem] of sortedWorkflowRuns.backward()) {
+            core.info(` ${key} ${runItem.id} : ${runItem.workflow_url} : ${runItem.status} : ${runItem.run_number}`);
+            if (shouldRunBeSkipped(runItem)) {
+                continue;
+            }
+            // Head of the run
+            const head = `${runItem.head_repository.full_name}/${runItem.head_branch}`;
+            if (!headsToRunIdMap.has(head)) {
+                core.info(`First run for the head: ${head}. Next runs with the same head will be cancelled.`);
+                headsToRunIdMap.set(head, runItem.id);
+                if (failFastJobNames !== undefined) {
+                    core.info("Checking if the head run failed in specified jobs");
+                    yield cancelOnFailFastJobsFailed(octokit, owner, repo, runItem.id, head, failFastJobNames);
+                }
+                else {
+                    core.info("Skipping the head run.");
+                }
+                continue;
+            }
+            core.info(`Cancelling run: ${runItem.id}, head ${head}.`);
+            core.info(`There is a later run with same head: ${headsToRunIdMap.get(head)}`);
+            yield cancelRun(octokit, owner, repo, runItem.id);
+        }
+    });
+}
+function getRequiredEnv(key) {
+    const value = process.env[key];
+    if (value === undefined) {
+        const message = `${key} was not defined.`;
+        throw new Error(message);
+    }
+    return value;
+}
+function runScheduledRun(octokit, owner, repo) {
+    return __awaiter(this, void 0, void 0, function* () {
+        const workflowId = core.getInput('workflow');
+        if (!(workflowId.length > 0)) {
+            core.setFailed('Workflow must be specified for schedule event type');
+            return;
+        }
+        const failFastJobNames = JSON.parse(core.getInput('failFastJobNames'));
+        if (failFastJobNames !== undefined) {
+            core.info(`Checking also if last run failed in one of the jobs: ${failFastJobNames}`);
+        }
+        yield findAndCancelPastRunsForSchedule(octokit, workflowId, owner, repo, failFastJobNames);
+        return;
+    });
+}
+function runRegularRun(octokit, selfRunId, owner, repo, eventName) {
+    return __awaiter(this, void 0, void 0, function* () {
+        const pullRequest = 'pull_request' === eventName;
+        const branchPrefix = 'refs/heads/';
+        const tagPrefix = 'refs/tags/';
+        let branch = getRequiredEnv(pullRequest ? 'GITHUB_HEAD_REF' : 'GITHUB_REF');
+        if (!pullRequest && !branch.startsWith(branchPrefix)) {
+            if (branch.startsWith(tagPrefix)) {
+                core.info(`Skipping tag build`);
+                return;
+            }
+            core.setFailed(`${branch} was not an expected branch ref (refs/heads/).`);
+            return;
+        }
+        branch = branch.replace(branchPrefix, '');
+        core.info(`Branch is ${branch}, repo is ${repo}, and owner is ${owner}, and id is ${selfRunId}`);
+        yield findAndCancelPastRunsForSelf(octokit, selfRunId, owner, repo, branch, eventName);
+    });
+}
+function run() {
+    return __awaiter(this, void 0, void 0, function* () {
+        const token = core.getInput('token');
+        const octokit = new github.GitHub(token);
+        core.info(`Starting checking for workflows to cancel`);
+        const selfRunId = getRequiredEnv('GITHUB_RUN_ID');
+        const repository = getRequiredEnv('GITHUB_REPOSITORY');
+        const eventName = getRequiredEnv('GITHUB_EVENT_NAME');
+        const [owner, repo] = repository.split('/');
+        if ('schedule' === eventName) {
+            yield runScheduledRun(octokit, owner, repo);
+        }
+        else if (!['push', 'pull_request'].includes(eventName)) {
+            core.info('Skipping unsupported event');
+            return;
+        }
+        else {
+            yield runRegularRun(octokit, selfRunId, owner, repo, eventName);
+        }
+    });
+}
+run().then(() => core.info("Cancel complete")).catch(e => core.setFailed(e.message));
+
+
+/***/ }),
+
 /***/ 211:
 /***/ (function(module) {
 
@@ -2200,7 +2303,7 @@ module.exports = require("https");
 /***/ 215:
 /***/ (function(module) {
 
-module.exports = {"_args":[["@octokit/rest@16.43.0","/Users/jason/devel/typescript-action"]],"_from":"@octokit/rest@16.43.0","_id":"@octokit/rest@16.43.0","_inBundle":false,"_integrity":"sha512-u+OwrTxHuppVcssGmwCmb4jgPNzsRseJ2rS5PrZk2ASC+WkaF5Q7wu8zVtJ4OA24jK6aRymlwA2uwL36NU9nAA==","_location":"/@octokit/rest","_phantomChildren":{},"_requested":{"type":"version","registry":true,"raw":"@octokit/rest@16.43.0","name":"@octokit/rest","escapedName":"@octokit%2frest","scope":"@octokit","rawSp [...]
+module.exports = {"_args":[["@octokit/rest@16.43.0","/home/jarek/code/cancel-previous-runs"]],"_from":"@octokit/rest@16.43.0","_id":"@octokit/rest@16.43.0","_inBundle":false,"_integrity":"sha512-u+OwrTxHuppVcssGmwCmb4jgPNzsRseJ2rS5PrZk2ASC+WkaF5Q7wu8zVtJ4OA24jK6aRymlwA2uwL36NU9nAA==","_location":"/@octokit/rest","_phantomChildren":{},"_requested":{"type":"version","registry":true,"raw":"@octokit/rest@16.43.0","name":"@octokit/rest","escapedName":"@octokit%2frest","scope":"@octokit","rawS [...]
 
 /***/ }),
 
@@ -7692,12 +7795,22 @@ var HttpCodes;
     HttpCodes[HttpCodes["RequestTimeout"] = 408] = "RequestTimeout";
     HttpCodes[HttpCodes["Conflict"] = 409] = "Conflict";
     HttpCodes[HttpCodes["Gone"] = 410] = "Gone";
+    HttpCodes[HttpCodes["TooManyRequests"] = 429] = "TooManyRequests";
     HttpCodes[HttpCodes["InternalServerError"] = 500] = "InternalServerError";
     HttpCodes[HttpCodes["NotImplemented"] = 501] = "NotImplemented";
     HttpCodes[HttpCodes["BadGateway"] = 502] = "BadGateway";
     HttpCodes[HttpCodes["ServiceUnavailable"] = 503] = "ServiceUnavailable";
     HttpCodes[HttpCodes["GatewayTimeout"] = 504] = "GatewayTimeout";
 })(HttpCodes = exports.HttpCodes || (exports.HttpCodes = {}));
+var Headers;
+(function (Headers) {
+    Headers["Accept"] = "accept";
+    Headers["ContentType"] = "content-type";
+})(Headers = exports.Headers || (exports.Headers = {}));
+var MediaTypes;
+(function (MediaTypes) {
+    MediaTypes["ApplicationJson"] = "application/json";
+})(MediaTypes = exports.MediaTypes || (exports.MediaTypes = {}));
 /**
  * Returns the proxy URL, depending upon the supplied url and proxy environment variables.
  * @param serverUrl  The server URL where the request will be sent. For example, https://api.github.com
@@ -7707,8 +7820,18 @@ function getProxyUrl(serverUrl) {
     return proxyUrl ? proxyUrl.href : '';
 }
 exports.getProxyUrl = getProxyUrl;
-const HttpRedirectCodes = [HttpCodes.MovedPermanently, HttpCodes.ResourceMoved, HttpCodes.SeeOther, HttpCodes.TemporaryRedirect, HttpCodes.PermanentRedirect];
-const HttpResponseRetryCodes = [HttpCodes.BadGateway, HttpCodes.ServiceUnavailable, HttpCodes.GatewayTimeout];
+const HttpRedirectCodes = [
+    HttpCodes.MovedPermanently,
+    HttpCodes.ResourceMoved,
+    HttpCodes.SeeOther,
+    HttpCodes.TemporaryRedirect,
+    HttpCodes.PermanentRedirect
+];
+const HttpResponseRetryCodes = [
+    HttpCodes.BadGateway,
+    HttpCodes.ServiceUnavailable,
+    HttpCodes.GatewayTimeout
+];
 const RetryableHttpVerbs = ['OPTIONS', 'GET', 'DELETE', 'HEAD'];
 const ExponentialBackoffCeiling = 10;
 const ExponentialBackoffTimeSlice = 5;
@@ -7800,22 +7923,29 @@ class HttpClient {
      * Gets a typed object from an endpoint
      * Be aware that not found returns a null.  Other errors (4xx, 5xx) reject the promise
      */
-    async getJson(requestUrl, additionalHeaders) {
+    async getJson(requestUrl, additionalHeaders = {}) {
+        additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson);
         let res = await this.get(requestUrl, additionalHeaders);
         return this._processResponse(res, this.requestOptions);
     }
-    async postJson(requestUrl, obj, additionalHeaders) {
+    async postJson(requestUrl, obj, additionalHeaders = {}) {
         let data = JSON.stringify(obj, null, 2);
+        additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson);
+        additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.ContentType, MediaTypes.ApplicationJson);
         let res = await this.post(requestUrl, data, additionalHeaders);
         return this._processResponse(res, this.requestOptions);
     }
-    async putJson(requestUrl, obj, additionalHeaders) {
+    async putJson(requestUrl, obj, additionalHeaders = {}) {
         let data = JSON.stringify(obj, null, 2);
+        additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson);
+        additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.ContentType, MediaTypes.ApplicationJson);
         let res = await this.put(requestUrl, data, additionalHeaders);
         return this._processResponse(res, this.requestOptions);
     }
-    async patchJson(requestUrl, obj, additionalHeaders) {
+    async patchJson(requestUrl, obj, additionalHeaders = {}) {
         let data = JSON.stringify(obj, null, 2);
+        additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson);
+        additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.ContentType, MediaTypes.ApplicationJson);
         let res = await this.patch(requestUrl, data, additionalHeaders);
         return this._processResponse(res, this.requestOptions);
     }
@@ -7826,18 +7956,22 @@ class HttpClient {
      */
     async request(verb, requestUrl, data, headers) {
         if (this._disposed) {
-            throw new Error("Client has already been disposed.");
+            throw new Error('Client has already been disposed.');
         }
         let parsedUrl = url.parse(requestUrl);
         let info = this._prepareRequest(verb, parsedUrl, headers);
         // Only perform retries on reads since writes may not be idempotent.
-        let maxTries = (this._allowRetries && RetryableHttpVerbs.indexOf(verb) != -1) ? this._maxRetries + 1 : 1;
+        let maxTries = this._allowRetries && RetryableHttpVerbs.indexOf(verb) != -1
+            ? this._maxRetries + 1
+            : 1;
         let numTries = 0;
         let response;
         while (numTries < maxTries) {
             response = await this.requestRaw(info, data);
             // Check if it's an authentication challenge
-            if (response && response.message && response.message.statusCode === HttpCodes.Unauthorized) {
+            if (response &&
+                response.message &&
+                response.message.statusCode === HttpCodes.Unauthorized) {
                 let authenticationHandler;
                 for (let i = 0; i < this.handlers.length; i++) {
                     if (this.handlers[i].canHandleAuthentication(response)) {
@@ -7855,21 +7989,32 @@ class HttpClient {
                 }
             }
             let redirectsRemaining = this._maxRedirects;
-            while (HttpRedirectCodes.indexOf(response.message.statusCode) != -1
-                && this._allowRedirects
-                && redirectsRemaining > 0) {
-                const redirectUrl = response.message.headers["location"];
+            while (HttpRedirectCodes.indexOf(response.message.statusCode) != -1 &&
+                this._allowRedirects &&
+                redirectsRemaining > 0) {
+                const redirectUrl = response.message.headers['location'];
                 if (!redirectUrl) {
                     // if there's no location to redirect to, we won't
                     break;
                 }
                 let parsedRedirectUrl = url.parse(redirectUrl);
-                if (parsedUrl.protocol == 'https:' && parsedUrl.protocol != parsedRedirectUrl.protocol && !this._allowRedirectDowngrade) {
-                    throw new Error("Redirect from HTTPS to HTTP protocol. This downgrade is not allowed for security reasons. If you want to allow this behavior, set the allowRedirectDowngrade option to true.");
+                if (parsedUrl.protocol == 'https:' &&
+                    parsedUrl.protocol != parsedRedirectUrl.protocol &&
+                    !this._allowRedirectDowngrade) {
+                    throw new Error('Redirect from HTTPS to HTTP protocol. This downgrade is not allowed for security reasons. If you want to allow this behavior, set the allowRedirectDowngrade option to true.');
                 }
                 // we need to finish reading the response before reassigning response
                 // which will leak the open socket.
                 await response.readBody();
+                // strip authorization header if redirected to a different hostname
+                if (parsedRedirectUrl.hostname !== parsedUrl.hostname) {
+                    for (let header in headers) {
+                        // header names are case insensitive
+                        if (header.toLowerCase() === 'authorization') {
+                            delete headers[header];
+                        }
+                    }
+                }
                 // let's make the request with the new redirectUrl
                 info = this._prepareRequest(verb, parsedRedirectUrl, headers);
                 response = await this.requestRaw(info, data);
@@ -7920,8 +8065,8 @@ class HttpClient {
      */
     requestRawWithCallback(info, data, onResult) {
         let socket;
-        if (typeof (data) === 'string') {
-            info.options.headers["Content-Length"] = Buffer.byteLength(data, 'utf8');
+        if (typeof data === 'string') {
+            info.options.headers['Content-Length'] = Buffer.byteLength(data, 'utf8');
         }
         let callbackCalled = false;
         let handleResult = (err, res) => {
@@ -7934,7 +8079,7 @@ class HttpClient {
             let res = new HttpClientResponse(msg);
             handleResult(null, res);
         });
-        req.on('socket', (sock) => {
+        req.on('socket', sock => {
             socket = sock;
         });
         // If we ever get disconnected, we want the socket to timeout eventually
@@ -7949,10 +8094,10 @@ class HttpClient {
             // res should have headers
             handleResult(err, null);
         });
-        if (data && typeof (data) === 'string') {
+        if (data && typeof data === 'string') {
             req.write(data, 'utf8');
         }
-        if (data && typeof (data) !== 'string') {
+        if (data && typeof data !== 'string') {
             data.on('close', function () {
                 req.end();
             });
@@ -7979,29 +8124,40 @@ class HttpClient {
         const defaultPort = usingSsl ? 443 : 80;
         info.options = {};
         info.options.host = info.parsedUrl.hostname;
-        info.options.port = info.parsedUrl.port ? parseInt(info.parsedUrl.port) : defaultPort;
-        info.options.path = (info.parsedUrl.pathname || '') + (info.parsedUrl.search || '');
+        info.options.port = info.parsedUrl.port
+            ? parseInt(info.parsedUrl.port)
+            : defaultPort;
+        info.options.path =
+            (info.parsedUrl.pathname || '') + (info.parsedUrl.search || '');
         info.options.method = method;
         info.options.headers = this._mergeHeaders(headers);
         if (this.userAgent != null) {
-            info.options.headers["user-agent"] = this.userAgent;
+            info.options.headers['user-agent'] = this.userAgent;
         }
         info.options.agent = this._getAgent(info.parsedUrl);
         // gives handlers an opportunity to participate
         if (this.handlers) {
-            this.handlers.forEach((handler) => {
+            this.handlers.forEach(handler => {
                 handler.prepareRequest(info.options);
             });
         }
         return info;
     }
     _mergeHeaders(headers) {
-        const lowercaseKeys = obj => Object.keys(obj).reduce((c, k) => (c[k.toLowerCase()] = obj[k], c), {});
+        const lowercaseKeys = obj => Object.keys(obj).reduce((c, k) => ((c[k.toLowerCase()] = obj[k]), c), {});
         if (this.requestOptions && this.requestOptions.headers) {
             return Object.assign({}, lowercaseKeys(this.requestOptions.headers), lowercaseKeys(headers));
         }
         return lowercaseKeys(headers || {});
     }
+    _getExistingOrDefaultHeader(additionalHeaders, header, _default) {
+        const lowercaseKeys = obj => Object.keys(obj).reduce((c, k) => ((c[k.toLowerCase()] = obj[k]), c), {});
+        let clientHeader;
+        if (this.requestOptions && this.requestOptions.headers) {
+            clientHeader = lowercaseKeys(this.requestOptions.headers)[header];
+        }
+        return additionalHeaders[header] || clientHeader || _default;
+    }
     _getAgent(parsedUrl) {
         let agent;
         let proxyUrl = pm.getProxyUrl(parsedUrl);
@@ -8033,7 +8189,7 @@ class HttpClient {
                     proxyAuth: proxyUrl.auth,
                     host: proxyUrl.hostname,
                     port: proxyUrl.port
-                },
+                }
             };
             let tunnelAgent;
             const overHttps = proxyUrl.protocol === 'https:';
@@ -8060,7 +8216,9 @@ class HttpClient {
             // we don't want to set NODE_TLS_REJECT_UNAUTHORIZED=0 since that will affect request for entire process
             // http.RequestOptions doesn't expose a way to modify RequestOptions.agent.options
             // we have to cast it to any and change it directly
-            agent.options = Object.assign(agent.options || {}, { rejectUnauthorized: false });
+            agent.options = Object.assign(agent.options || {}, {
+                rejectUnauthorized: false
+            });
         }
         return agent;
     }
@@ -8121,7 +8279,7 @@ class HttpClient {
                     msg = contents;
                 }
                 else {
-                    msg = "Failed request: (" + statusCode + ")";
+                    msg = 'Failed request: (' + statusCode + ')';
                 }
                 let err = new Error(msg);
                 // attach statusCode and body obj (if available) to the error object
@@ -28666,12 +28824,10 @@ function getProxyUrl(reqUrl) {
     }
     let proxyVar;
     if (usingSsl) {
-        proxyVar = process.env["https_proxy"] ||
-            process.env["HTTPS_PROXY"];
+        proxyVar = process.env['https_proxy'] || process.env['HTTPS_PROXY'];
     }
     else {
-        proxyVar = process.env["http_proxy"] ||
-            process.env["HTTP_PROXY"];
+        proxyVar = process.env['http_proxy'] || process.env['HTTP_PROXY'];
     }
     if (proxyVar) {
         proxyUrl = url.parse(proxyVar);
@@ -28683,7 +28839,7 @@ function checkBypass(reqUrl) {
     if (!reqUrl.hostname) {
         return false;
     }
-    let noProxy = process.env["no_proxy"] || process.env["NO_PROXY"] || '';
+    let noProxy = process.env['no_proxy'] || process.env['NO_PROXY'] || '';
     if (!noProxy) {
         return false;
     }
@@ -28704,7 +28860,10 @@ function checkBypass(reqUrl) {
         upperReqHosts.push(`${upperReqHosts[0]}:${reqPort}`);
     }
     // Compare request host against noproxy
-    for (let upperNoProxyItem of noProxy.split(',').map(x => x.trim().toUpperCase()).filter(x => x)) {
+    for (let upperNoProxyItem of noProxy
+        .split(',')
+        .map(x => x.trim().toUpperCase())
+        .filter(x => x)) {
         if (upperReqHosts.some(x => x === upperNoProxyItem)) {
             return true;
         }
diff --git a/src/main.ts b/src/main.ts
index 0fededf..1bd5e3f 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -3,214 +3,260 @@ import * as core from '@actions/core'
 import Octokit from '@octokit/rest'
 import * as treemap from 'jstreemap'
 
-function createRunsQuery(
+function createListRunsQueryForAllRuns(
   octokit: github.GitHub,
   owner: string,
   repo: string,
   workflowId: string,
   status: string,
-  branch?: string,
-  event?: string
 ): Octokit.RequestOptions {
-  const request =
-    branch === undefined
-      ? {
-          owner,
-          repo,
-          // eslint-disable-next-line @typescript-eslint/camelcase
-          workflow_id: workflowId,
-          status
-        }
-      : {
-          owner,
-          repo,
-          // eslint-disable-next-line @typescript-eslint/camelcase
-          workflow_id: workflowId,
-          status,
-          branch,
-          event
-        }
+  const request = {
+    owner,
+    repo,
+    // eslint-disable-next-line @typescript-eslint/camelcase
+    workflow_id: workflowId,
+    status
+  }
+  return octokit.actions.listWorkflowRuns.endpoint.merge(request)
+}
 
+function createListRunsQueryForSelfRun(
+    octokit: github.GitHub,
+    owner: string,
+    repo: string,
+    workflowId: string,
+    status: string,
+    branch: string,
+    eventName: string
+): Octokit.RequestOptions {
+  const request = {
+    owner,
+    repo,
+    // eslint-disable-next-line @typescript-eslint/camelcase
+    workflow_id: workflowId,
+    status,
+    branch,
+    event: eventName
+  }
   return octokit.actions.listWorkflowRuns.endpoint.merge(request)
 }
 
-async function cancelDuplicates(
-  token: string,
-  selfRunId: string,
-  owner: string,
-  repo: string,
-  workflowId?: string,
-  branch?: string,
-  event?: string
-): Promise<void> {
-  const octokit = new github.GitHub(token)
+function createJobsForWorkflowRunQuery(
+    octokit: github.GitHub,
+    owner: string,
+    repo: string,
+    runId: number,
+): Octokit.RequestOptions {
+  const request = {
+    owner,
+    repo,
+    // eslint-disable-next-line @typescript-eslint/camelcase
+    run_id: runId,
+  }
+  return octokit.actions.listJobsForWorkflowRun.endpoint.merge(request)
+}
 
-  // Determine the workflow to reduce the result set, or reference another workflow
-  let resolvedId = ''
-  if (workflowId === undefined) {
-    const reply = await octokit.actions.getWorkflowRun({
+async function cancelOnFailFastJobsFailed(
+    octokit: github.GitHub,
+    owner: string,
+    repo: string,
+    runId: number,
+    head: string,
+    failFastJobNames: string[]
+): Promise<void> {
+  const listJobs = createJobsForWorkflowRunQuery(
+      octokit,
       owner,
       repo,
-      // eslint-disable-next-line @typescript-eslint/camelcase
-      run_id: Number.parseInt(selfRunId)
-    })
-
-    resolvedId = reply.data.workflow_url.split('/').pop() || ''
-    if (!(resolvedId.length > 0)) {
-      throw new Error('Could not resolve workflow')
+      runId,
+  )
+  core.info(`Cancelling runId ${runId} in case one of the ${failFastJobNames} failed`)
+  for await (const item of octokit.paginate.iterator(listJobs)) {
+    for (const job of item.data.jobs) {
+      core.info(`The job name: ${job.name}, Conclusion: ${job.conclusion}`)
+      if (job.conclusion == 'failure' &&
+          failFastJobNames.some(jobNameRegexp => job.name.match(jobNameRegexp) )) {
+        core.info(`Job ${job.name} has failed and it matches one of the ${failFastJobNames} regexps`)
+        core.info(`Cancelling the workflow run: ${runId}, head: ${head}`)
+        await cancelRun(octokit, owner, repo, runId)
+        return
+      }
     }
-  } else {
-    resolvedId = workflowId
   }
+}
 
-  core.info(`Workflow ID is: ${resolvedId}`)
+async function getSelfWorkflowId(
+    octokit: github.GitHub,
+    selfRunId: string,
+    owner: string,
+    repo: string) {
+  let workflowId: string
+  const reply = await octokit.actions.getWorkflowRun({
+    owner,
+    repo,
+    // eslint-disable-next-line @typescript-eslint/camelcase
+    run_id: Number.parseInt(selfRunId)
+  })
+  workflowId = reply.data.workflow_url.split('/').pop() || ''
+  if (!(workflowId.length > 0)) {
+    throw new Error('Could not resolve workflow')
+  }
+  return workflowId
+}
 
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  const sorted = new treemap.TreeMap<number, any>()
+async function getSortedWorkflowRuns(
+    octokit: github.GitHub,
+    createListRunQuery: CallableFunction,
+  ): Promise<treemap.TreeMap<number, Octokit.ActionsListWorkflowRunsResponseWorkflowRunsItem>>{
+  const sortedWorkflowRuns = new treemap.TreeMap<number, any>()
   for (const status of ['queued', 'in_progress']) {
-    const listRuns = createRunsQuery(
-      octokit,
-      owner,
-      repo,
-      resolvedId,
-      status,
-      branch,
-      event
-    )
+    const listRuns = await createListRunQuery(status)
     for await (const item of octokit.paginate.iterator(listRuns)) {
       // There is some sort of bug where the pagination URLs point to a
       // different endpoint URL which trips up the resulting representation
       // In that case, fallback to the actual REST 'workflow_runs' property
       const elements =
-        item.data.length === undefined ? item.data.workflow_runs : item.data
+          item.data.length === undefined ? item.data.workflow_runs : item.data
 
       for (const element of elements) {
-        sorted.set(element.run_number, element)
+        sortedWorkflowRuns.set(element.run_number, element)
       }
     }
   }
+  core.info(`Found runs: ${Array.from(sortedWorkflowRuns.backward()).map(t => t[0])}`)
+  return sortedWorkflowRuns
+}
 
-  // If a workflow was provided process everything
-  let matched = workflowId !== undefined
-  const heads = new Set()
-  for (const entry of sorted.backward()) {
-    const element = entry[1]
+function shouldRunBeSkipped(runItem: Octokit.ActionsListWorkflowRunsResponseWorkflowRunsItem) {
+  if ('completed' === runItem.status.toString()) {
+    core.info(`Skip completed run: ${runItem.id}`)
+    return true
+  }
+
+  if (!['push', 'pull_request'].includes(runItem.event.toString())) {
+    core.info(`Skip run: ${runItem.id} as it is neither push nor pull_request (${runItem.event}`)
+    return true
+  }
+  return false
+}
+
+async function cancelRun(
+    octokit: github.GitHub,
+    owner: string,
+    repo: string,
+    id: number,
+): Promise<void> {
+  let reply
+  try {
+    reply = await octokit.actions.cancelWorkflowRun({
+      owner: owner,
+      repo: repo,
+      // eslint-disable-next-line @typescript-eslint/camelcase
+      run_id: id
+    })
+    core.info(`Previous run (id ${id}) cancelled, status = ${reply.status}`)
+  } catch (error) {
     core.info(
-      `${element.id} : ${element.workflow_url} : ${element.status} : ${element.run_number}`
+        `[warn] Could not cancel run (id ${id}): [${error.status}] ${error.message}`
     )
+  }
+}
+
 
+// Kills past runs for my own workflow.
+async function findAndCancelPastRunsForSelf(
+    octokit: github.GitHub,
+    selfRunId: string,
+    owner: string,
+    repo: string,
+    branch: string,
+    eventName: string,
+): Promise<void> {
+  core.info(`findAndCancelPastRunsForSelf:  ${selfRunId}, ${owner}, ${repo}, ${branch}, ${eventName}`)
+  const workflowId = await getSelfWorkflowId(octokit, selfRunId, owner, repo)
+  core.info(`My own workflow ID is: ${workflowId}`)
+  const sortedWorkflowRuns = await getSortedWorkflowRuns(
+      octokit,function(status: string) {
+        return createListRunsQueryForSelfRun(octokit, owner, repo, workflowId,
+            status, branch, eventName )
+      }
+  )
+  let matched = false
+  const headsToRunIdMap = new Map<string, number>()
+  for (const [key, runItem] of sortedWorkflowRuns.backward()) {
+    core.info(
+      `Run number: ${key}, RunId: ${runItem.id}, URL: ${runItem.workflow_url}. Status ${runItem.status}`
+    )
     if (!matched) {
-      if (element.id.toString() !== selfRunId) {
-        // Skip everything up to this run
+      if (runItem.id.toString() !== selfRunId) {
+        core.info(`Skip run ${runItem.id} as it was started before my own id: ${selfRunId}`)
         continue
       }
-
       matched = true
-      core.info(`Matched ${selfRunId}`)
+      core.info(`Matched ${selfRunId}. Reached my own ID, now looping through all remaining runs/`)
+      core.info("I will cancel all except the first for each 'head' available")
     }
-
-    if (
-      'completed' === element.status.toString() ||
-      !['push', 'pull_request'].includes(element.event.toString())
-    ) {
+    if (shouldRunBeSkipped(runItem)){
       continue
     }
-
-    // This is a set of one in the non-schedule case, otherwise everything is a candidate
-    const head = `${element.head_repository.full_name}/${element.head_branch}`
-    if (!heads.has(head)) {
-      core.info(`First: ${head}`)
-      heads.add(head)
+    // Head of the run
+    const head = `${runItem.head_repository.full_name}/${runItem.head_branch}`
+    if (!headsToRunIdMap.has(head)) {
+      core.info(`First run for the head: ${head}. Skipping it. Next ones with same head will be cancelled.`)
+      headsToRunIdMap.set(head, runItem.id)
       continue
     }
-
-    core.info(`Cancelling: ${head}`)
-
-    await cancelRun(octokit, owner, repo, element.id)
+    core.info(`Cancelling run: ${runItem.id}, head ${head}.`)
+    core.info(`There is a later run with same head: ${headsToRunIdMap.get(head)}`)
+    await cancelRun(octokit, owner, repo, runItem.id)
   }
 }
 
-async function run(): Promise<void> {
-  try {
-    const token = core.getInput('token')
-
-    core.info(token)
-
-    const selfRunId = getRequiredEnv('GITHUB_RUN_ID')
-    const repository = getRequiredEnv('GITHUB_REPOSITORY')
-    const eventName = getRequiredEnv('GITHUB_EVENT_NAME')
-
-    const [owner, repo] = repository.split('/')
-    const branchPrefix = 'refs/heads/'
-    const tagPrefix = 'refs/tags/'
+// Kills past runs for my own workflow.
+async function findAndCancelPastRunsForSchedule(
+    octokit: github.GitHub,
+    workflowId: string,
+    owner: string,
+    repo: string,
+    failFastJobNames?: string[],
+): Promise<void> {
+  core.info(`findAndCancelPastRunsForSchedule: ${owner}, ${workflowId}, ${repo}`)
 
-    if ('schedule' === eventName) {
-      const workflowId = core.getInput('workflow')
-      if (!(workflowId.length > 0)) {
-        throw new Error('Workflow must be specified for schedule event type')
+  const sortedWorkflowRuns = await getSortedWorkflowRuns(
+      octokit,function(status: string) {
+        return createListRunsQueryForAllRuns(octokit, owner, repo, workflowId, status)
       }
-      await cancelDuplicates(token, selfRunId, owner, repo, workflowId)
-      return
-    }
+  )
 
-    if (!['push', 'pull_request'].includes(eventName)) {
-      core.info('Skipping unsupported event')
-      return
-    }
+  const headsToRunIdMap = new Map<string, number>()
+  for (const [key, runItem] of sortedWorkflowRuns.backward()) {
+    core.info(
+        ` ${key} ${runItem.id} : ${runItem.workflow_url} : ${runItem.status} : ${runItem.run_number}`
+    )
 
-    const pullRequest = 'pull_request' === eventName
+    if (shouldRunBeSkipped(runItem)){
+      continue
+    }
 
-    let branch = getRequiredEnv(pullRequest ? 'GITHUB_HEAD_REF' : 'GITHUB_REF')
-    if (!pullRequest && !branch.startsWith(branchPrefix)) {
-      if (branch.startsWith(tagPrefix)) {
-        core.info(`Skipping tag build`)
-        return
+    // Head of the run
+    const head = `${runItem.head_repository.full_name}/${runItem.head_branch}`
+    if (!headsToRunIdMap.has(head)) {
+      core.info(`First run for the head: ${head}. Next runs with the same head will be cancelled.`)
+      headsToRunIdMap.set(head, runItem.id)
+      if (failFastJobNames !== undefined) {
+        core.info("Checking if the head run failed in specified jobs")
+        await cancelOnFailFastJobsFailed(octokit, owner, repo, runItem.id, head, failFastJobNames)
+      } else {
+        core.info("Skipping the head run.")
       }
-      const message = `${branch} was not an expected branch ref (refs/heads/).`
-      throw new Error(message)
+      continue
     }
-    branch = branch.replace(branchPrefix, '')
-
-    core.info(
-      `Branch is ${branch}, repo is ${repo}, and owner is ${owner}, and id is ${selfRunId}`
-    )
-
-    cancelDuplicates(
-      token,
-      selfRunId,
-      owner,
-      repo,
-      undefined,
-      branch,
-      eventName
-    )
-  } catch (error) {
-    core.setFailed(error.message)
+    core.info(`Cancelling run: ${runItem.id}, head ${head}.`)
+    core.info(`There is a later run with same head: ${headsToRunIdMap.get(head)}`)
+    await cancelRun(octokit, owner, repo, runItem.id)
   }
 }
 
-async function cancelRun(
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  octokit: any,
-  owner: string,
-  repo: string,
-  id: string
-): Promise<void> {
-  let reply
-  try {
-    reply = await octokit.actions.cancelWorkflowRun({
-      owner,
-      repo,
-      // eslint-disable-next-line @typescript-eslint/camelcase
-      run_id: id
-    })
-    core.info(`Previous run (id ${id}) cancelled, status = ${reply.status}`)
-  } catch (error) {
-    core.info(
-      `[warn] Could not cancel run (id ${id}): [${error.status}] ${error.message}`
-    )
-  }
-}
 
 function getRequiredEnv(key: string): string {
   const value = process.env[key]
@@ -221,4 +267,70 @@ function getRequiredEnv(key: string): string {
   return value
 }
 
-run()
+
+async function runScheduledRun(octokit: github.GitHub, owner: string, repo: string) {
+  const workflowId = core.getInput('workflow')
+  if (!(workflowId.length > 0)) {
+    core.setFailed('Workflow must be specified for schedule event type')
+    return
+  }
+  const failFastJobNames =
+      JSON.parse(core.getInput('failFastJobNames'))
+  if (failFastJobNames !== undefined) {
+    core.info(`Checking also if last run failed in one of the jobs: ${failFastJobNames}`)
+  }
+
+  await findAndCancelPastRunsForSchedule(octokit, workflowId, owner, repo, failFastJobNames)
+  return
+}
+
+async function runRegularRun(
+    octokit: github.GitHub,
+    selfRunId: string,
+    owner:string,
+    repo:string,
+    eventName: string) {
+  const pullRequest = 'pull_request' === eventName
+  const branchPrefix = 'refs/heads/'
+  const tagPrefix = 'refs/tags/'
+
+  let branch = getRequiredEnv(pullRequest ? 'GITHUB_HEAD_REF' : 'GITHUB_REF')
+  if (!pullRequest && !branch.startsWith(branchPrefix)) {
+    if (branch.startsWith(tagPrefix)) {
+      core.info(`Skipping tag build`)
+      return
+    }
+    core.setFailed(`${branch} was not an expected branch ref (refs/heads/).`)
+    return
+  }
+  branch = branch.replace(branchPrefix, '')
+
+  core.info(
+      `Branch is ${branch}, repo is ${repo}, and owner is ${owner}, and id is ${selfRunId}`
+  )
+
+  await findAndCancelPastRunsForSelf(octokit, selfRunId, owner, repo, branch, eventName)
+
+}
+
+async function run(): Promise<void> {
+  const token = core.getInput('token')
+  const octokit = new github.GitHub(token)
+  core.info(`Starting checking for workflows to cancel`)
+  const selfRunId = getRequiredEnv('GITHUB_RUN_ID')
+  const repository = getRequiredEnv('GITHUB_REPOSITORY')
+  const eventName = getRequiredEnv('GITHUB_EVENT_NAME')
+
+  const [owner, repo] = repository.split('/')
+
+  if ('schedule' === eventName) {
+    await runScheduledRun(octokit, owner, repo);
+  } else if (!['push', 'pull_request'].includes(eventName)) {
+    core.info('Skipping unsupported event')
+    return
+  } else {
+    await runRegularRun(octokit, selfRunId, owner, repo, eventName)
+  }
+}
+
+run().then(() => core.info("Cancel complete")).catch(e => core.setFailed(e.message))