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:54 UTC

[airflow-cancel-workflow-runs] 38/44: Add feature of cancelling future duplicates (enabled by default) (#9)

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 c8448eb1e435664b3731ea1ead2efa0d1bb83b5b
Author: Jarek Potiuk <ja...@potiuk.com>
AuthorDate: Fri Oct 30 19:32:07 2020 +0100

    Add feature of cancelling future duplicates (enabled by default) (#9)
---
 README.md     | 27 ++++++++++++---------
 action.yml    |  5 ++++
 dist/index.js | 71 +++++++++++++++++++++++++++++++++----------------------
 src/main.ts   | 76 +++++++++++++++++++++++++++++++++++++++--------------------
 4 files changed, 115 insertions(+), 64 deletions(-)

diff --git a/README.md b/README.md
index 90c9a22..0a13160 100644
--- a/README.md
+++ b/README.md
@@ -106,17 +106,18 @@ and `schedule` events are no longer needed.
 
 ## Inputs
 
-| Input                   | Required | Default      | Comment                                                                                                                                                                                                          |
-|-------------------------|----------|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `token`                 | yes      |              | The github token passed from `${{ secrets.GITHUB_TOKEN }}`                                                                                                                                                       |
-| `cancelMode`            | no       | `duplicates` | The mode to run cancel on. The available options are `duplicates`, `self`, `failedJobs`, `namedJobs`                                                                                                             |
-| `sourceRunId`           | no       |              | Useful only in `workflow_run` triggered events. It should be set to the id of the workflow triggering the run `${{ github.event.workflow_run.id }}`  in case cancel operation should cancel the source workflow. |
-| `notifyPRCancel`        | no       |              | Boolean. If set to true, it notifies the cancelled PRs with a comment containing reason why they are being cancelled.                                                                                            |
-| `notifyPRCancelMessage` | no       |              | Optional cancel message to use instead of the default one when notifyPRCancel is true.  It is only used in 'self' cancelling mode.                                                                               |
-| `notifyPRMessageStart`  | no       |              | Only for workflow_run events triggered by the PRs. If not empty, it notifies those PRs with the message specified at the start of the workflow - adding the link to the triggered workflow_run.                  |
-| `jobNameRegexps`        | no       |              | An array of job name regexps. Only runs containing any job name matching any of of the regexp in this array are considered for cancelling in `failedJobs` and `namedJobs` cancel modes.                          |
-| `skipEventTypes`        | no       |              | Array of event names that should be skipped when cancelling (JSON-encoded string). This might be used in order to skip direct pushes or scheduled events.                                                        |
-| `workflowFileName`      | no       |              | Name of the workflow file. It can be used if you want to cancel a different workflow than yours.                                                                                                                 |
+| Input                    | Required | Default      | Comment                                                                                                                                                                                                          |
+|--------------------------|----------|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `token`                  | yes      |              | The github token passed from `${{ secrets.GITHUB_TOKEN }}`                                                                                                                                                       |
+| `cancelMode`             | no       | `duplicates` | The mode to run cancel on. The available options are `duplicates`, `self`, `failedJobs`, `namedJobs`                                                                                                             |
+| `cancelFutureDuplicates` | no       | true         | In case of duplicate canceling, cancel also future duplicates leaving only the "freshest" running job and not all the future jobs. By default it is set to true.                                                 |
+| `sourceRunId`            | no       |              | Useful only in `workflow_run` triggered events. It should be set to the id of the workflow triggering the run `${{ github.event.workflow_run.id }}`  in case cancel operation should cancel the source workflow. |
+| `notifyPRCancel`         | no       |              | Boolean. If set to true, it notifies the cancelled PRs with a comment containing reason why they are being cancelled.                                                                                            |
+| `notifyPRCancelMessage`  | no       |              | Optional cancel message to use instead of the default one when notifyPRCancel is true.  It is only used in 'self' cancelling mode.                                                                               |
+| `notifyPRMessageStart`   | no       |              | Only for workflow_run events triggered by the PRs. If not empty, it notifies those PRs with the message specified at the start of the workflow - adding the link to the triggered workflow_run.                  |
+| `jobNameRegexps`         | no       |              | An array of job name regexps. Only runs containing any job name matching any of of the regexp in this array are considered for cancelling in `failedJobs` and `namedJobs` cancel modes.                          |
+| `skipEventTypes`         | no       |              | Array of event names that should be skipped when cancelling (JSON-encoded string). This might be used in order to skip direct pushes or scheduled events.                                                        |
+| `workflowFileName`       | no       |              | Name of the workflow file. It can be used if you want to cancel a different workflow than yours.                                                                                                                 |
 
 
 The job cancel modes work as follows:
@@ -205,6 +206,7 @@ jobs:
         name: "Cancel duplicate workflow runs"
         with:
           cancelMode: duplicates
+          cancelFutureDuplicates: true
           token: ${{ secrets.GITHUB_TOKEN }}
           sourceRunId: ${{ github.event.workflow_run.id }}
           notifyPRCancel: true
@@ -266,6 +268,7 @@ jobs:
         name: "Cancel duplicate CI runs"
         with:
           cancelMode: duplicates
+          cancelFutureDuplicates: true
           token: ${{ secrets.GITHUB_TOKEN }}
           notifyPRCancel: true
           notifyPRMessageStart: |
@@ -512,6 +515,7 @@ on:
         uses: potiuk/cancel-workflow-runs@v2
         with:
           cancelMode: duplicates
+          cancelFutureDuplicates: true
           token: ${{ secrets.GITHUB_TOKEN }}
           workflowFileName: other_workflow.yml
 ```
@@ -553,6 +557,7 @@ jobs:
         name: "Cancel duplicate workflow runs"
         with:
           cancelMode: duplicates
+          cancelFutureDuplicates: true
           notifyPRCancel: true
 ```
 
diff --git a/action.yml b/action.yml
index 103d4e4..aaec312 100644
--- a/action.yml
+++ b/action.yml
@@ -36,6 +36,11 @@ inputs:
           * `failedJobs`  - cancels all runs that failed in jobs matching one of the regexps
           * `namedJobs`   - cancels runs where names of some jobs match some of regexps
     required: false
+  cancelFutureDuplicates:
+    description: |
+      In case of duplicate canceling, cancel also future duplicates leaving only the "freshest" running
+      job and not all the future jobs. By default it is set to true.
+    required: false
   jobNameRegexps:
     description: |
       Array of job name regexps (JSON-encoded string). Used by `failedJobs` and `namedJobs` cancel modes
diff --git a/dist/index.js b/dist/index.js
index a347a2c..69bd164 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -1622,7 +1622,7 @@ function getWorkflowRuns(octokit, statusValues, cancelMode, createListRunQuery)
         return workflowRuns;
     });
 }
-function shouldBeCancelled(octokit, owner, repo, runItem, headRepo, cancelMode, sourceRunId, jobNamesRegexps, skipEventTypes) {
+function shouldBeCancelled(octokit, owner, repo, runItem, headRepo, cancelMode, cancelFutureDuplicates, sourceRunId, jobNamesRegexps, skipEventTypes) {
     return __awaiter(this, void 0, void 0, function* () {
         if ('completed' === runItem.status.toString()) {
             core.info(`\nThe run ${runItem.id} is completed. Not cancelling it.\n`);
@@ -1651,17 +1651,17 @@ function shouldBeCancelled(octokit, owner, repo, runItem, headRepo, cancelMode,
         else if (cancelMode === CancelMode.NAMED_JOBS) {
             // Cancel all jobs that have failed jobs (no matter when started)
             if (yield jobsMatchingNames(octokit, owner, repo, runItem.id, jobNamesRegexps, false)) {
-                core.info(`\nSome jobs have matching names in ${runItem.id} . Cancelling it.\n`);
+                core.info(`\nSome jobs have matching names in ${runItem.id} . Returning it.\n`);
                 return true;
             }
             else {
-                core.info(`\nNone of the jobs match name in ${runItem.id}. Not cancelling it.\n`);
+                core.info(`\nNone of the jobs match name in ${runItem.id}. Returning it.\n`);
                 return false;
             }
         }
         else if (cancelMode === CancelMode.SELF) {
             if (runItem.id === sourceRunId) {
-                core.info(`\nCancelling the "source" run: ${runItem.id}.\n`);
+                core.info(`\nReturning the "source" run: ${runItem.id}.\n`);
                 return true;
             }
             else {
@@ -1675,16 +1675,20 @@ function shouldBeCancelled(octokit, owner, repo, runItem, headRepo, cancelMode,
                     `repo: ${runHeadRepo} (expected ${headRepo}). Not cancelling it\n`);
                 return false;
             }
-            if (runItem.id === sourceRunId) {
-                core.info(`\nThis is my own run ${runItem.id}. I have self-preservation mechanism. Not cancelling myself!\n`);
-                return false;
-            }
-            else if (runItem.id > sourceRunId) {
-                core.info(`\nThe run ${runItem.id} is started later than mt own run ${sourceRunId}. Not cancelling it\n`);
-                return false;
+            if (cancelFutureDuplicates) {
+                core.info(`\nCancel Future Duplicates: Returning run id that might be duplicate or my own run: ${runItem.id}.\n`);
+                return true;
             }
             else {
-                core.info(`\nCancelling duplicate of my own run: ${runItem.id}.\n`);
+                if (runItem.id === sourceRunId) {
+                    core.info(`\nThis is my own run ${runItem.id}. Not returning myself!\n`);
+                    return false;
+                }
+                else if (runItem.id > sourceRunId) {
+                    core.info(`\nThe run ${runItem.id} is started later than my own run ${sourceRunId}. Not returning it\n`);
+                    return false;
+                }
+                core.info(`\nFound duplicate of my own run: ${runItem.id}.\n`);
                 return true;
             }
         }
@@ -1710,7 +1714,7 @@ function cancelRun(octokit, owner, repo, runId) {
         }
     });
 }
-function findAndCancelRuns(octokit, selfRunId, sourceWorkflowId, sourceRunId, owner, repo, headRepo, headBranch, sourceEventName, cancelMode, notifyPRCancel, notifyPRMessageStart, jobNameRegexps, skipEventTypes, reason) {
+function findAndCancelRuns(octokit, selfRunId, sourceWorkflowId, sourceRunId, owner, repo, headRepo, headBranch, sourceEventName, cancelMode, cancelFutureDuplicates, notifyPRCancel, notifyPRMessageStart, jobNameRegexps, skipEventTypes, reason) {
     return __awaiter(this, void 0, void 0, function* () {
         const statusValues = ['queued', 'in_progress'];
         const workflowRuns = yield getWorkflowRuns(octokit, statusValues, cancelMode, function (status) {
@@ -1735,29 +1739,39 @@ function findAndCancelRuns(octokit, selfRunId, sourceWorkflowId, sourceRunId, ow
                 throw Error(`\nWrong cancel mode ${cancelMode}! This should never happen.\n`);
             }
         });
-        const idsToCancel = [];
+        const workflowsToCancel = [];
         const pullRequestToNotify = [];
         for (const [key, runItem] of workflowRuns) {
-            core.info(`\nChecking run number: ${key}, RunId: ${runItem.id}, Url: ${runItem.url}. Status ${runItem.status}\n`);
-            if (yield shouldBeCancelled(octokit, owner, repo, runItem, headRepo, cancelMode, sourceRunId, jobNameRegexps, skipEventTypes)) {
+            core.info(`\nChecking run number: ${key}, RunId: ${runItem.id}, Url: ${runItem.url}. Status ${runItem.status},` +
+                ` Created at ${runItem.created_at}\n`);
+            if (yield shouldBeCancelled(octokit, owner, repo, runItem, headRepo, cancelMode, cancelFutureDuplicates, sourceRunId, jobNameRegexps, skipEventTypes)) {
                 if (notifyPRCancel && runItem.event === 'pull_request') {
                     const pullRequest = yield findPullRequest(octokit, owner, repo, runItem.head_repository.owner.login, runItem.head_branch, runItem.head_sha);
                     if (pullRequest) {
                         pullRequestToNotify.push(pullRequest.number);
                     }
                 }
-                idsToCancel.push(runItem.id);
+                workflowsToCancel.push([runItem.id, runItem.created_at]);
             }
         }
-        // Sort from smallest number - this way we always kill current one at the end (if we kill it at all)
-        const sortedIdsToCancel = idsToCancel.sort((id1, id2) => id1 - id2);
-        if (sortedIdsToCancel.length > 0) {
+        // Sort from most recent date - this way we always kill current one at the end (if we kill it at all)
+        const sortedRunTuplesToCancel = workflowsToCancel.sort((runTuple1, runTuple2) => runTuple2[1].localeCompare(runTuple1[1]));
+        if (sortedRunTuplesToCancel.length > 0) {
+            if (cancelMode === CancelMode.DUPLICATES && cancelFutureDuplicates) {
+                core.info(`\nSkipping the first run (${sortedRunTuplesToCancel[0]}) of all the matching ` +
+                    `duplicates - this one we are going to leave in peace!\n`);
+                sortedRunTuplesToCancel.shift();
+            }
+            if (sortedRunTuplesToCancel.length === 0) {
+                core.info(`\nNo duplicates to cancel!\n`);
+                return sortedRunTuplesToCancel.map(runTuple => runTuple[0]);
+            }
             core.info('\n######  Cancelling runs starting from the oldest  ##########\n' +
-                `\n     Runs to cancel: ${sortedIdsToCancel.length}\n` +
+                `\n     Runs to cancel: ${sortedRunTuplesToCancel.length}\n` +
                 `\n     PRs to notify: ${pullRequestToNotify.length}\n`);
-            for (const runId of sortedIdsToCancel) {
-                core.info(`\nCancelling run: ${runId}.\n`);
-                yield cancelRun(octokit, owner, repo, runId);
+            for (const runTuple of sortedRunTuplesToCancel) {
+                core.info(`\nCancelling run: ${runTuple}.\n`);
+                yield cancelRun(octokit, owner, repo, runTuple[0]);
             }
             for (const pullRequestNumber of pullRequestToNotify) {
                 const selfWorkflowRunUrl = `https://github.com/${owner}/${repo}/actions/runs/${selfRunId}`;
@@ -1768,7 +1782,7 @@ function findAndCancelRuns(octokit, selfRunId, sourceWorkflowId, sourceRunId, ow
         else {
             core.info('\n######  There are no runs to cancel!              ##########\n');
         }
-        return sortedIdsToCancel;
+        return sortedRunTuplesToCancel.map(runTuple => runTuple[0]);
     });
 }
 function getRequiredEnv(key) {
@@ -1837,7 +1851,7 @@ function getOrigin(octokit, runId, owner, repo) {
         ];
     });
 }
-function performCancelJob(octokit, selfRunId, sourceWorkflowId, sourceRunId, owner, repo, headRepo, headBranch, sourceEventName, cancelMode, notifyPRCancel, notifyPRCancelMessage, notifyPRMessageStart, jobNameRegexps, skipEventTypes) {
+function performCancelJob(octokit, selfRunId, sourceWorkflowId, sourceRunId, owner, repo, headRepo, headBranch, sourceEventName, cancelMode, notifyPRCancel, notifyPRCancelMessage, notifyPRMessageStart, jobNameRegexps, skipEventTypes, cancelFutureDuplicates) {
     return __awaiter(this, void 0, void 0, function* () {
         core.info('\n###################################################################################\n');
         core.info(`All parameters: owner: ${owner}, repo: ${repo}, run id: ${sourceRunId}, ` +
@@ -1867,7 +1881,7 @@ function performCancelJob(octokit, selfRunId, sourceWorkflowId, sourceRunId, own
             throw Error(`Wrong cancel mode ${cancelMode}! This should never happen.`);
         }
         core.info('\n###################################################################################\n');
-        return yield findAndCancelRuns(octokit, selfRunId, sourceWorkflowId, sourceRunId, owner, repo, headRepo, headBranch, sourceEventName, cancelMode, notifyPRCancel, notifyPRMessageStart, jobNameRegexps, skipEventTypes, reason);
+        return yield findAndCancelRuns(octokit, selfRunId, sourceWorkflowId, sourceRunId, owner, repo, headRepo, headBranch, sourceEventName, cancelMode, cancelFutureDuplicates, notifyPRCancel, notifyPRMessageStart, jobNameRegexps, skipEventTypes, reason);
     });
 }
 function verboseOutput(name, value) {
@@ -1887,6 +1901,7 @@ function run() {
         const notifyPRMessageStart = core.getInput('notifyPRMessageStart');
         const sourceRunId = parseInt(core.getInput('sourceRunId')) || selfRunId;
         const jobNameRegexpsString = core.getInput('jobNameRegexps');
+        const cancelFutureDuplicates = (core.getInput('cancelFutureDuplicates') || 'true').toLowerCase() === 'true';
         const jobNameRegexps = jobNameRegexpsString
             ? JSON.parse(jobNameRegexpsString)
             : [];
@@ -1944,7 +1959,7 @@ function run() {
                 body: `${notifyPRMessageStart} [The workflow run](${selfWorkflowRunUrl})`
             });
         }
-        const cancelledRuns = yield performCancelJob(octokit, selfRunId, sourceWorkflowId, sourceRunId, owner, repo, headRepo, headBranch, sourceEventName, cancelMode, notifyPRCancel, notifyPRCancelMessage, notifyPRMessageStart, jobNameRegexps, skipEventTypes);
+        const cancelledRuns = yield performCancelJob(octokit, selfRunId, sourceWorkflowId, sourceRunId, owner, repo, headRepo, headBranch, sourceEventName, cancelMode, notifyPRCancel, notifyPRCancelMessage, notifyPRMessageStart, jobNameRegexps, skipEventTypes, cancelFutureDuplicates);
         verboseOutput('cancelledRuns', JSON.stringify(cancelledRuns));
     });
 }
diff --git a/src/main.ts b/src/main.ts
index 9f60f3d..71da8d9 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -203,6 +203,7 @@ async function shouldBeCancelled(
   runItem: rest.ActionsListWorkflowRunsResponseWorkflowRunsItem,
   headRepo: string,
   cancelMode: CancelMode,
+  cancelFutureDuplicates: boolean,
   sourceRunId: number,
   jobNamesRegexps: string[],
   skipEventTypes: string[]
@@ -259,18 +260,18 @@ async function shouldBeCancelled(
       )
     ) {
       core.info(
-        `\nSome jobs have matching names in ${runItem.id} . Cancelling it.\n`
+        `\nSome jobs have matching names in ${runItem.id} . Returning it.\n`
       )
       return true
     } else {
       core.info(
-        `\nNone of the jobs match name in ${runItem.id}. Not cancelling it.\n`
+        `\nNone of the jobs match name in ${runItem.id}. Returning it.\n`
       )
       return false
     }
   } else if (cancelMode === CancelMode.SELF) {
     if (runItem.id === sourceRunId) {
-      core.info(`\nCancelling the "source" run: ${runItem.id}.\n`)
+      core.info(`\nReturning the "source" run: ${runItem.id}.\n`)
       return true
     } else {
       return false
@@ -284,18 +285,22 @@ async function shouldBeCancelled(
       )
       return false
     }
-    if (runItem.id === sourceRunId) {
+    if (cancelFutureDuplicates) {
       core.info(
-        `\nThis is my own run ${runItem.id}. I have self-preservation mechanism. Not cancelling myself!\n`
+        `\nCancel Future Duplicates: Returning run id that might be duplicate or my own run: ${runItem.id}.\n`
       )
-      return false
-    } else if (runItem.id > sourceRunId) {
-      core.info(
-        `\nThe run ${runItem.id} is started later than mt own run ${sourceRunId}. Not cancelling it\n`
-      )
-      return false
+      return true
     } else {
-      core.info(`\nCancelling duplicate of my own run: ${runItem.id}.\n`)
+      if (runItem.id === sourceRunId) {
+        core.info(`\nThis is my own run ${runItem.id}. Not returning myself!\n`)
+        return false
+      } else if (runItem.id > sourceRunId) {
+        core.info(
+          `\nThe run ${runItem.id} is started later than my own run ${sourceRunId}. Not returning it\n`
+        )
+        return false
+      }
+      core.info(`\nFound duplicate of my own run: ${runItem.id}.\n`)
       return true
     }
   } else {
@@ -338,6 +343,7 @@ async function findAndCancelRuns(
   headBranch: string,
   sourceEventName: string,
   cancelMode: CancelMode,
+  cancelFutureDuplicates: boolean,
   notifyPRCancel: boolean,
   notifyPRMessageStart: string,
   jobNameRegexps: string[],
@@ -400,11 +406,12 @@ async function findAndCancelRuns(
       }
     }
   )
-  const idsToCancel: number[] = []
+  const workflowsToCancel: [number, string][] = []
   const pullRequestToNotify: number[] = []
   for (const [key, runItem] of workflowRuns) {
     core.info(
-      `\nChecking run number: ${key}, RunId: ${runItem.id}, Url: ${runItem.url}. Status ${runItem.status}\n`
+      `\nChecking run number: ${key}, RunId: ${runItem.id}, Url: ${runItem.url}. Status ${runItem.status},` +
+        ` Created at ${runItem.created_at}\n`
     )
     if (
       await shouldBeCancelled(
@@ -414,6 +421,7 @@ async function findAndCancelRuns(
         runItem,
         headRepo,
         cancelMode,
+        cancelFutureDuplicates,
         sourceRunId,
         jobNameRegexps,
         skipEventTypes
@@ -432,20 +440,33 @@ async function findAndCancelRuns(
           pullRequestToNotify.push(pullRequest.number)
         }
       }
-      idsToCancel.push(runItem.id)
+      workflowsToCancel.push([runItem.id, runItem.created_at])
     }
   }
-  // Sort from smallest number - this way we always kill current one at the end (if we kill it at all)
-  const sortedIdsToCancel = idsToCancel.sort((id1, id2) => id1 - id2)
-  if (sortedIdsToCancel.length > 0) {
+  // Sort from most recent date - this way we always kill current one at the end (if we kill it at all)
+  const sortedRunTuplesToCancel = workflowsToCancel.sort(
+    (runTuple1, runTuple2) => runTuple2[1].localeCompare(runTuple1[1])
+  )
+  if (sortedRunTuplesToCancel.length > 0) {
+    if (cancelMode === CancelMode.DUPLICATES && cancelFutureDuplicates) {
+      core.info(
+        `\nSkipping the first run (${sortedRunTuplesToCancel[0]}) of all the matching ` +
+          `duplicates - this one we are going to leave in peace!\n`
+      )
+      sortedRunTuplesToCancel.shift()
+    }
+    if (sortedRunTuplesToCancel.length === 0) {
+      core.info(`\nNo duplicates to cancel!\n`)
+      return sortedRunTuplesToCancel.map(runTuple => runTuple[0])
+    }
     core.info(
       '\n######  Cancelling runs starting from the oldest  ##########\n' +
-        `\n     Runs to cancel: ${sortedIdsToCancel.length}\n` +
+        `\n     Runs to cancel: ${sortedRunTuplesToCancel.length}\n` +
         `\n     PRs to notify: ${pullRequestToNotify.length}\n`
     )
-    for (const runId of sortedIdsToCancel) {
-      core.info(`\nCancelling run: ${runId}.\n`)
-      await cancelRun(octokit, owner, repo, runId)
+    for (const runTuple of sortedRunTuplesToCancel) {
+      core.info(`\nCancelling run: ${runTuple}.\n`)
+      await cancelRun(octokit, owner, repo, runTuple[0])
     }
     for (const pullRequestNumber of pullRequestToNotify) {
       const selfWorkflowRunUrl = `https://github.com/${owner}/${repo}/actions/runs/${selfRunId}`
@@ -465,7 +486,7 @@ async function findAndCancelRuns(
       '\n######  There are no runs to cancel!              ##########\n'
     )
   }
-  return sortedIdsToCancel
+  return sortedRunTuplesToCancel.map(runTuple => runTuple[0])
 }
 
 function getRequiredEnv(key: string): string {
@@ -581,7 +602,8 @@ async function performCancelJob(
   notifyPRCancelMessage: string,
   notifyPRMessageStart: string,
   jobNameRegexps: string[],
-  skipEventTypes: string[]
+  skipEventTypes: string[],
+  cancelFutureDuplicates: boolean
 ): Promise<number[]> {
   core.info(
     '\n###################################################################################\n'
@@ -635,6 +657,7 @@ async function performCancelJob(
     headBranch,
     sourceEventName,
     cancelMode,
+    cancelFutureDuplicates,
     notifyPRCancel,
     notifyPRMessageStart,
     jobNameRegexps,
@@ -662,6 +685,8 @@ async function run(): Promise<void> {
   const notifyPRMessageStart = core.getInput('notifyPRMessageStart')
   const sourceRunId = parseInt(core.getInput('sourceRunId')) || selfRunId
   const jobNameRegexpsString = core.getInput('jobNameRegexps')
+  const cancelFutureDuplicates =
+    (core.getInput('cancelFutureDuplicates') || 'true').toLowerCase() === 'true'
   const jobNameRegexps = jobNameRegexpsString
     ? JSON.parse(jobNameRegexpsString)
     : []
@@ -761,7 +786,8 @@ async function run(): Promise<void> {
     notifyPRCancelMessage,
     notifyPRMessageStart,
     jobNameRegexps,
-    skipEventTypes
+    skipEventTypes,
+    cancelFutureDuplicates
   )
 
   verboseOutput('cancelledRuns', JSON.stringify(cancelledRuns))