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 <[email protected]>
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/[email protected]","/Users/jason/devel/typescript-action"]],"_from":"@octokit/[email protected]","_id":"@octokit/[email protected]","_inBundle":false,"_integrity":"sha512-u+OwrTxHuppVcssGmwCmb4jgPNzsRseJ2rS5PrZk2ASC+WkaF5Q7wu8zVtJ4OA24jK6aRymlwA2uwL36NU9nAA==","_location":"/@octokit/rest","_phantomChildren":{},"_requested":{"type":"version","registry":true,"raw":"@octokit/[email protected]","name":"@octokit/rest","escapedName":"@octokit%2frest","scope":"@octokit","rawSp
 [...]
+module.exports = 
{"_args":[["@octokit/[email protected]","/home/jarek/code/cancel-previous-runs"]],"_from":"@octokit/[email protected]","_id":"@octokit/[email protected]","_inBundle":false,"_integrity":"sha512-u+OwrTxHuppVcssGmwCmb4jgPNzsRseJ2rS5PrZk2ASC+WkaF5Q7wu8zVtJ4OA24jK6aRymlwA2uwL36NU9nAA==","_location":"/@octokit/rest","_phantomChildren":{},"_requested":{"type":"version","registry":true,"raw":"@octokit/[email protected]","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))

Reply via email to