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

[airflow-cancel-workflow-runs] 33/44: Added functionality for PR retrieving and notifying. (#5)

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 e9e87cb7738dbb999654aa90d69359d62c9e4eae
Author: Jarek Potiuk <ja...@potiuk.com>
AuthorDate: Sun Oct 4 21:57:04 2020 +0200

    Added functionality for PR retrieving and notifying. (#5)
    
    In case the workflow was triggered by a Pull Request,
    it finds out the PR that triggered it and notifies it (controlled
    by the parameters specified):
    
    * when the workflow is started with specified message
    * when any other PR is cancelled, it is notificed with
      explainiing the reason why it has been cancelled.
---
 README.md     |  61 ++++++++++++++++------
 action.yml    |  10 ++++
 dist/index.js |  96 ++++++++++++++++++++++++++++++----
 src/main.ts   | 162 +++++++++++++++++++++++++++++++++++++++++++++++++++++-----
 4 files changed, 288 insertions(+), 41 deletions(-)

diff --git a/README.md b/README.md
index 021e34f..7946ea9 100644
--- a/README.md
+++ b/README.md
@@ -69,6 +69,14 @@ resolved during execution of the workflow using information about the linked wor
 at the workflow runtime. Hopefully this information will soon be available in GitHub Actions allowing
 removal of `namedJobs` cancel mode and simplifying the examples and workflows using the Action.
 
+Another feature of the Action is to notify the PRs linked to the workflows. Normally when workflows
+get cancelled there is no information why it happens, but this action can add an explanatory comment
+to the PR if the PR gets cancelled. This is controlled by `notifyPRCancel` boolean input.
+
+Also, for the `workflow_run` events, GitHub does not yet provide an easy interface linking the original
+Pull Request and the Workflow_run. You can ask the CancelWorkflowRun action to add extra comment to the PR
+adding explanatory message followed by a link to the `workflow_run` run.
+
 You can take a look at the description provided in the
 [Apache Airflow's CI](https://github.com/apache/airflow/blob/master/CI.rst) and
 [the workflows](https://github.com/apache/airflow/blob/master/.github/workflows)
@@ -97,12 +105,14 @@ 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. |
-| `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.                          |
+| 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.                                                                                            |
+| `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.                          |
 
 The job cancel modes work as follows:
 
@@ -116,13 +126,16 @@ The job cancel modes work as follows:
 
 ## Outputs
 
-| Output             | No `sourceRunId` specified                   | The `sourceRunId` set to `${{ github.event.workflow_run.id }}`                                      |
-|--------------------|----------------------------------------------|-----------------------------------------------------------------------------------------------------|
-| `sourceHeadRepo`   | Current repository. Format: `owner/repo`     | Repository of the run that triggered this `workflow_run`. Format: `owner/repo`                      |
-| `sourceHeadBranch` | Current branch.                              | Branch of the run that triggered this `workflow_run`. Might be forked repo, if it is a pull_requst. |
-| `sourceHeadSha`    | Current commit SHA: `{{ github.sha }}`       | Commit sha of the run that triggered this `workflow_run`.                                           |
-| `sourceEvent`      | Current event: ``${{ github.event }}``       | Event of the run that triggered this `workflow_run`                                                 |
-| `cancelledRuns`    | JSON-stringified array of cancelled run ids. | JSON-stringified array of cancelled run ids.                                                        |
+| Output              | No `sourceRunId` specified                              | The `sourceRunId` set to `${{ github.event.workflow_run.id }}`                                       |
+|---------------------|---------------------------------------------------------|------------------------------------------------------------------------------------------------------|
+| `sourceHeadRepo`    | Current repository. Format: `owner/repo`                | Repository of the run that triggered this `workflow_run`. Format: `owner/repo`                       |
+| `sourceHeadBranch`  | Current branch.                                         | Branch of the run that triggered this `workflow_run`. Might be forked repo, if it is a pull_request. |
+| `sourceHeadSha`     | Current commit SHA: `{{ github.sha }}`                  | Commit sha of the run that triggered this `workflow_run`.                                            |
+| `mergeCommitSha`    | Merge commit SHA if PR-triggered event.                 | Merge commit SHA if PR-triggered event.                                                              |
+| `targetCommitSha`   | Target commit SHA (merge if present, otherwise source). | Target commit SHA (merge if present, otherwise source).                                              |
+| `pullRequestNumber` | Number of the associated Pull Request (if PR triggered) | Number of the associated Pull Request (if PR triggered)                                              |
+| `sourceEvent`       | Current event: ``${{ github.event }}``                  | Event of the run that triggered this `workflow_run`                                                  |
+| `cancelledRuns`     | JSON-stringified array of cancelled run ids.            | JSON-stringified array of cancelled run ids.                                                         |
 
 # Examples
 
@@ -149,7 +162,8 @@ Cancels past runs for the same workflow (with the same branch).
 
 In the case below, any of the direct "push" events will cancel all past runs for the same branch as the
 one being pushed. However, it can be configured for "pull_request" (in the same repository) or "schedule"
-type of events as well.
+type of events as well. It will also notify the PR with the comment containining why it has been
+cancelled.
 
 ```yaml
 name: CI
@@ -163,7 +177,7 @@ jobs:
         name: "Cancel duplicate workflow runs"
         with:
           cancelMode: duplicates
-          token: ${{ secrets.GITHUB_TOKEN }}
+          notifyPRCancel: true
 ```
 
 ### Cancel "self" workflow run
@@ -186,7 +200,7 @@ jobs:
         with:
           cancelMode: self
           token: ${{ secrets.GITHUB_TOKEN }}
-
+          notifyPRCancel: true
 ```
 
 ### Fail-fast workflow runs with failed jobs
@@ -214,6 +228,7 @@ jobs:
           cancelMode: failedJobs
           token: ${{ secrets.GITHUB_TOKEN }}
           jobNameRegexps: '["^Static checks$", "^Build docs$", "^Build prod image.*"]'
+          notifyPRCancel: true
 ```
 
 ### Cancel all runs with named jobs
@@ -245,6 +260,7 @@ jobs:
           cancelMode: namedJobs
           token: ${{ secrets.GITHUB_TOKEN }}
           jobNameRegexps: '["^Static checks$", "^Build docs$", "^Build prod image.*"]'
+          notifyPRCancel: true
 ```
 
 ## Repositories that use Pull Requests from forks
@@ -304,6 +320,7 @@ jobs:
           cancelMode: duplicates
           token: ${{ secrets.GITHUB_TOKEN }}
           sourceRunId: ${{ github.event.workflow_run.id }}
+          notifyPRCancel: true
 ```
 
 Note that `duplicate` cancel mode cannot be used for `workflow_run` type of event without `sourceId` input.
@@ -362,11 +379,18 @@ jobs:
         with:
           cancelMode: duplicates
           token: ${{ secrets.GITHUB_TOKEN }}
+          notifyPRCancel: true
+          notifyPRMessageStart: |
+            Note! The Docker Images for the build are prepared in a separate workflow,
+            that you will not see in the list of checks.
+
+            You can checks the status of those images in:
       - uses: potiuk/cancel-workflow-runs@v2
         name: "Cancel duplicate Cancelling runs"
         with:
           cancelMode: namedJobs
           token: ${{ secrets.GITHUB_TOKEN }}
+          notifyPRCancel: true
           jobNameRegexps: >
             ["Build info
             repo: ${{ steps.cancel.outputs.sourceHeadRepo }}
@@ -414,6 +438,7 @@ on:
         uses: potiuk/cancel-workflow-runs@v2
         with:
           cancelMode: self
+          notifyPRCancel: true
           token: ${{ secrets.GITHUB_TOKEN }}
           sourceRunId: ${{ github.event.workflow_run.id }}
 ```
@@ -441,6 +466,7 @@ on:
         uses: potiuk/cancel-workflow-runs@v2
         with:
           cancelMode: self
+          notifyPRCancel: true
           token: ${{ secrets.GITHUB_TOKEN }}
 
 ```
@@ -476,6 +502,7 @@ jobs:
           cancelMode: failedJobs
           token: ${{ secrets.GITHUB_TOKEN }}
           sourceRunId: ${{ github.event.workflow_run.id }}
+          notifyPRCancel: true
           jobNameRegexps: '["^Static checks$", "^Build docs$", "^Build prod image.*"]'
 ```
 
@@ -517,6 +544,7 @@ jobs:
           cancelMode: failedJobs
           token: ${{ secrets.GITHUB_TOKEN }}
           sourceRunId: ${{ github.event.workflow_run.id }}
+          notifyPRCancel: true
           jobNameRegexps: '["^Static checks$", "^Build docs$", "^Build prod image.*"]'
       - name: "Extract canceled failed runs"
         id: extract-cancelled-failed-runs
@@ -536,6 +564,7 @@ jobs:
         with:
           cancelMode: namedJobs
           token: ${{ secrets.GITHUB_TOKEN }}
+          notifyPRCancel: true
           jobNameRegexps: ${{ steps.extract-cancelled-failed.runs.matching-regexp }}
 
 ```
diff --git a/action.yml b/action.yml
index 71f6ae2..932b45f 100644
--- a/action.yml
+++ b/action.yml
@@ -11,6 +11,16 @@ inputs:
       `$\{\{ github.event.workflow_run.id` variable \}\}` if used in `workflow_run` triggered run if
       you want to act on source workflow rather than the triggered run.
     required: false
+  notifyPRCancel:
+    description: |
+      Boolean. If set to true, it notifies the cancelled PRs with a comment containing reason why
+      they are being cancelled.
+    required: false
+  notifyPRMessageStart:
+    description: |
+      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.
+    required: false
   cancelMode:
     description: |
       The mode of cancel. One of:
diff --git a/dist/index.js b/dist/index.js
index 3c03be4..a7796dd 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -1705,7 +1705,7 @@ function cancelRun(octokit, owner, repo, runId) {
         }
     });
 }
-function findAndCancelRuns(octokit, sourceWorkflowId, sourceRunId, owner, repo, headRepo, headBranch, sourceEventName, cancelMode, jobNameRegexps) {
+function findAndCancelRuns(octokit, selfRunId, sourceWorkflowId, sourceRunId, owner, repo, headRepo, headBranch, sourceEventName, cancelMode, notifyPRCancel, notifyPRMessageStart, jobNameRegexps, reason) {
     return __awaiter(this, void 0, void 0, function* () {
         const statusValues = ['queued', 'in_progress'];
         const workflowRuns = yield getWorkflowRuns(octokit, statusValues, cancelMode, function (status) {
@@ -1731,9 +1731,16 @@ function findAndCancelRuns(octokit, sourceWorkflowId, sourceRunId, owner, repo,
             }
         });
         const idsToCancel = [];
+        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)) {
+                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);
             }
         }
@@ -1741,11 +1748,16 @@ function findAndCancelRuns(octokit, sourceWorkflowId, sourceRunId, owner, repo,
         const sortedIdsToCancel = idsToCancel.sort((id1, id2) => id1 - id2);
         if (sortedIdsToCancel.length > 0) {
             core.info('\n######  Cancelling runs starting from the oldest  ##########\n' +
-                `\n     Runs to cancel: ${sortedIdsToCancel.length}\n`);
+                `\n     Runs to cancel: ${sortedIdsToCancel.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 pullRequestNumber of pullRequestToNotify) {
+                const selfWorkflowRunUrl = `https://github.com/${owner}/${repo}/actions/runs/${selfRunId}`;
+                yield addCommentToPullRequest(octokit, owner, repo, pullRequestNumber, `[The Build Workflow run](${selfWorkflowRunUrl}) is cancelling this PR. ${reason}`);
+            }
             core.info('\n######  Finished cancelling runs                  ##########\n');
         }
         else {
@@ -1762,6 +1774,38 @@ function getRequiredEnv(key) {
     }
     return value;
 }
+function addCommentToPullRequest(octokit, owner, repo, pullRequestNumber, comment) {
+    return __awaiter(this, void 0, void 0, function* () {
+        core.info(`\nNotifying PR: ${pullRequestNumber} with '${comment}'.\n`);
+        yield octokit.issues.createComment({
+            owner,
+            repo,
+            // eslint-disable-next-line @typescript-eslint/camelcase
+            issue_number: pullRequestNumber,
+            body: comment
+        });
+    });
+}
+function findPullRequest(octokit, owner, repo, headRepo, headBranch, headSha) {
+    return __awaiter(this, void 0, void 0, function* () {
+        // Finds Pull request for this workflow run
+        core.info(`\nFinding PR request id for: owner: ${owner}, Repo:${repo}, Head:${headRepo}:${headBranch}.\n`);
+        const pullRequests = yield octokit.pulls.list({
+            owner,
+            repo,
+            head: `${headRepo}:${headBranch}`
+        });
+        for (const pullRequest of pullRequests.data) {
+            core.info(`\nComparing: ${pullRequest.number} sha: ${pullRequest.head.sha} with expected: ${headSha}.\n`);
+            if (pullRequest.head.sha === headSha) {
+                core.info(`\nFound PR: ${pullRequest.number}\n`);
+                return pullRequest;
+            }
+        }
+        core.info(`\nCould not find the PR for this build :(\n`);
+        return null;
+    });
+}
 function getOrigin(octokit, runId, owner, repo) {
     return __awaiter(this, void 0, void 0, function* () {
         const reply = yield octokit.actions.getWorkflowRun({
@@ -1774,40 +1818,55 @@ function getOrigin(octokit, runId, owner, repo) {
         core.info(`Source workflow: Head repo: ${sourceRun.head_repository.full_name}, ` +
             `Head branch: ${sourceRun.head_branch} ` +
             `Event: ${sourceRun.event}, Head sha: ${sourceRun.head_sha}, url: ${sourceRun.url}`);
+        let pullRequest = null;
+        if (sourceRun.event === 'pull_request') {
+            pullRequest = yield findPullRequest(octokit, owner, repo, sourceRun.head_repository.owner.login, sourceRun.head_branch, sourceRun.head_sha);
+        }
         return [
             reply.data.head_repository.full_name,
             reply.data.head_branch,
             reply.data.event,
-            reply.data.head_sha
+            reply.data.head_sha,
+            pullRequest ? pullRequest.merge_commit_sha : '',
+            pullRequest
         ];
     });
 }
-function performCancelJob(octokit, sourceWorkflowId, sourceRunId, owner, repo, headRepo, headBranch, sourceEventName, cancelMode, jobNameRegexps) {
+function performCancelJob(octokit, selfRunId, sourceWorkflowId, sourceRunId, owner, repo, headRepo, headBranch, sourceEventName, cancelMode, notifyPRCancel, notifyPRMessageStart, jobNameRegexps) {
     return __awaiter(this, void 0, void 0, function* () {
         core.info('\n###################################################################################\n');
         core.info(`All parameters: owner: ${owner}, repo: ${repo}, run id: ${sourceRunId}, ` +
             `head repo ${headRepo}, headBranch: ${headBranch}, ` +
             `sourceEventName: ${sourceEventName}, cancelMode: ${cancelMode}, jobNames: ${jobNameRegexps}`);
         core.info('\n###################################################################################\n');
+        let reason = '';
         if (cancelMode === CancelMode.SELF) {
             core.info(`# Cancelling source run: ${sourceRunId} for workflow ${sourceWorkflowId}.`);
+            reason = `The job has been cancelled by another workflow.`;
         }
         else if (cancelMode === CancelMode.FAILED_JOBS) {
             core.info(`# Cancel all runs for workflow ${sourceWorkflowId} where job names matching ${jobNameRegexps} failed.`);
+            reason = `It has some failed jobs matching ${jobNameRegexps}.`;
         }
         else if (cancelMode === CancelMode.NAMED_JOBS) {
             core.info(`# Cancel all runs for workflow ${sourceWorkflowId} have job names matching ${jobNameRegexps}.`);
+            reason = `It has jobs matching ${jobNameRegexps}.`;
         }
         else if (cancelMode === CancelMode.DUPLICATES) {
             core.info(`# Cancel duplicate runs started before ${sourceRunId} for workflow ${sourceWorkflowId}.`);
+            reason = `It in earlier duplicate of ${sourceWorkflowId} run.`;
         }
         else {
             throw Error(`Wrong cancel mode ${cancelMode}! This should never happen.`);
         }
         core.info('\n###################################################################################\n');
-        return yield findAndCancelRuns(octokit, sourceWorkflowId, sourceRunId, owner, repo, headRepo, headBranch, sourceEventName, cancelMode, jobNameRegexps);
+        return yield findAndCancelRuns(octokit, selfRunId, sourceWorkflowId, sourceRunId, owner, repo, headRepo, headBranch, sourceEventName, cancelMode, notifyPRCancel, notifyPRMessageStart, jobNameRegexps, reason);
     });
 }
+function verboseOutput(name, value) {
+    core.info(`Setting output: ${name}: ${value}`);
+    core.setOutput(name, value);
+}
 function run() {
     return __awaiter(this, void 0, void 0, function* () {
         const token = core.getInput('token', { required: true });
@@ -1816,6 +1875,8 @@ function run() {
         const repository = getRequiredEnv('GITHUB_REPOSITORY');
         const eventName = getRequiredEnv('GITHUB_EVENT_NAME');
         const cancelMode = core.getInput('cancelMode') || CancelMode.DUPLICATES;
+        const notifyPRCancel = (core.getInput('notifyPRCancel') || 'false').toLowerCase() === 'true';
+        const notifyPRMessageStart = core.getInput('notifyPRMessageStart');
         const sourceRunId = parseInt(core.getInput('sourceRunId')) || selfRunId;
         const jobNameRegexpsString = core.getInput('jobNameRegexps');
         const jobNameRegexps = jobNameRegexpsString
@@ -1844,12 +1905,25 @@ function run() {
                     'It will likely not work as you intended - it will cancel runs which are not duplicates!' +
                     'See the docs for details.');
         }
-        const [headRepo, headBranch, sourceEventName, headSha] = yield getOrigin(octokit, sourceRunId, owner, repo);
-        core.setOutput('sourceHeadRepo', headRepo);
-        core.setOutput('sourceHeadBranch', headBranch);
-        core.setOutput('sourceHeadSha', headSha);
-        core.setOutput('sourceEvent', sourceEventName);
-        const cancelledRuns = yield performCancelJob(octokit, sourceWorkflowId, sourceRunId, owner, repo, headRepo, headBranch, sourceEventName, cancelMode, jobNameRegexps);
+        const [headRepo, headBranch, sourceEventName, headSha, mergeCommitSha, pullRequest] = yield getOrigin(octokit, sourceRunId, owner, repo);
+        verboseOutput('sourceHeadRepo', headRepo);
+        verboseOutput('sourceHeadBranch', headBranch);
+        verboseOutput('sourceHeadSha', headSha);
+        verboseOutput('sourceEvent', sourceEventName);
+        verboseOutput('pullRequestNumber', pullRequest ? pullRequest.number.toString() : '');
+        verboseOutput('mergeCommitSha', mergeCommitSha);
+        verboseOutput('targetCommitSha', pullRequest ? mergeCommitSha : headSha);
+        const selfWorkflowRunUrl = `https://github.com/${owner}/${repo}/actions/runs/${selfRunId}`;
+        if (notifyPRMessageStart && pullRequest) {
+            yield octokit.issues.createComment({
+                owner,
+                repo,
+                // eslint-disable-next-line @typescript-eslint/camelcase
+                issue_number: pullRequest.number,
+                body: `${notifyPRMessageStart} [The workflow run](${selfWorkflowRunUrl})`
+            });
+        }
+        const cancelledRuns = yield performCancelJob(octokit, selfRunId, sourceWorkflowId, sourceRunId, owner, repo, headRepo, headBranch, sourceEventName, cancelMode, notifyPRCancel, notifyPRMessageStart, jobNameRegexps);
         core.setOutput('cancelledRuns', JSON.stringify(cancelledRuns));
     });
 }
diff --git a/src/main.ts b/src/main.ts
index 01a8a9d..056ccb8 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -321,6 +321,7 @@ async function cancelRun(
 
 async function findAndCancelRuns(
   octokit: github.GitHub,
+  selfRunId: number,
   sourceWorkflowId: number,
   sourceRunId: number,
   owner: string,
@@ -329,7 +330,10 @@ async function findAndCancelRuns(
   headBranch: string,
   sourceEventName: string,
   cancelMode: CancelMode,
-  jobNameRegexps: string[]
+  notifyPRCancel: boolean,
+  notifyPRMessageStart: string,
+  jobNameRegexps: string[],
+  reason: string
 ): Promise<number[]> {
   const statusValues = ['queued', 'in_progress']
   const workflowRuns = await getWorkflowRuns(
@@ -388,6 +392,7 @@ async function findAndCancelRuns(
     }
   )
   const idsToCancel: number[] = []
+  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`
@@ -404,6 +409,19 @@ async function findAndCancelRuns(
         jobNameRegexps
       )
     ) {
+      if (notifyPRCancel && runItem.event === 'pull_request') {
+        const pullRequest = await 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)
     }
   }
@@ -412,12 +430,23 @@ async function findAndCancelRuns(
   if (sortedIdsToCancel.length > 0) {
     core.info(
       '\n######  Cancelling runs starting from the oldest  ##########\n' +
-        `\n     Runs to cancel: ${sortedIdsToCancel.length}\n`
+        `\n     Runs to cancel: ${sortedIdsToCancel.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 pullRequestNumber of pullRequestToNotify) {
+      const selfWorkflowRunUrl = `https://github.com/${owner}/${repo}/actions/runs/${selfRunId}`
+      await addCommentToPullRequest(
+        octokit,
+        owner,
+        repo,
+        pullRequestNumber,
+        `[The Build Workflow run](${selfWorkflowRunUrl}) is cancelling this PR. ${reason}`
+      )
+    }
     core.info(
       '\n######  Finished cancelling runs                  ##########\n'
     )
@@ -438,12 +467,61 @@ function getRequiredEnv(key: string): string {
   return value
 }
 
+async function addCommentToPullRequest(
+  octokit: github.GitHub,
+  owner: string,
+  repo: string,
+  pullRequestNumber: number,
+  comment: string
+): Promise<void> {
+  core.info(`\nNotifying PR: ${pullRequestNumber} with '${comment}'.\n`)
+  await octokit.issues.createComment({
+    owner,
+    repo,
+    // eslint-disable-next-line @typescript-eslint/camelcase
+    issue_number: pullRequestNumber,
+    body: comment
+  })
+}
+
+async function findPullRequest(
+  octokit: github.GitHub,
+  owner: string,
+  repo: string,
+  headRepo: string,
+  headBranch: string,
+  headSha: string
+): Promise<rest.PullsListResponseItem | null> {
+  // Finds Pull request for this workflow run
+  core.info(
+    `\nFinding PR request id for: owner: ${owner}, Repo:${repo}, Head:${headRepo}:${headBranch}.\n`
+  )
+  const pullRequests = await octokit.pulls.list({
+    owner,
+    repo,
+    head: `${headRepo}:${headBranch}`
+  })
+  for (const pullRequest of pullRequests.data) {
+    core.info(
+      `\nComparing: ${pullRequest.number} sha: ${pullRequest.head.sha} with expected: ${headSha}.\n`
+    )
+    if (pullRequest.head.sha === headSha) {
+      core.info(`\nFound PR: ${pullRequest.number}\n`)
+      return pullRequest
+    }
+  }
+  core.info(`\nCould not find the PR for this build :(\n`)
+  return null
+}
+
 async function getOrigin(
   octokit: github.GitHub,
   runId: number,
   owner: string,
   repo: string
-): Promise<[string, string, string, string]> {
+): Promise<
+  [string, string, string, string, string, rest.PullsListResponseItem | null]
+> {
   const reply = await octokit.actions.getWorkflowRun({
     owner,
     repo,
@@ -456,16 +534,31 @@ async function getOrigin(
       `Head branch: ${sourceRun.head_branch} ` +
       `Event: ${sourceRun.event}, Head sha: ${sourceRun.head_sha}, url: ${sourceRun.url}`
   )
+  let pullRequest: rest.PullsListResponseItem | null = null
+  if (sourceRun.event === 'pull_request') {
+    pullRequest = await findPullRequest(
+      octokit,
+      owner,
+      repo,
+      sourceRun.head_repository.owner.login,
+      sourceRun.head_branch,
+      sourceRun.head_sha
+    )
+  }
+
   return [
     reply.data.head_repository.full_name,
     reply.data.head_branch,
     reply.data.event,
-    reply.data.head_sha
+    reply.data.head_sha,
+    pullRequest ? pullRequest.merge_commit_sha : '',
+    pullRequest
   ]
 }
 
 async function performCancelJob(
   octokit: github.GitHub,
+  selfRunId: number,
   sourceWorkflowId: number,
   sourceRunId: number,
   owner: string,
@@ -474,6 +567,8 @@ async function performCancelJob(
   headBranch: string,
   sourceEventName: string,
   cancelMode: CancelMode,
+  notifyPRCancel: boolean,
+  notifyPRMessageStart: string,
   jobNameRegexps: string[]
 ): Promise<number[]> {
   core.info(
@@ -487,22 +582,27 @@ async function performCancelJob(
   core.info(
     '\n###################################################################################\n'
   )
+  let reason = ''
   if (cancelMode === CancelMode.SELF) {
     core.info(
       `# Cancelling source run: ${sourceRunId} for workflow ${sourceWorkflowId}.`
     )
+    reason = `The job has been cancelled by another workflow.`
   } else if (cancelMode === CancelMode.FAILED_JOBS) {
     core.info(
       `# Cancel all runs for workflow ${sourceWorkflowId} where job names matching ${jobNameRegexps} failed.`
     )
+    reason = `It has some failed jobs matching ${jobNameRegexps}.`
   } else if (cancelMode === CancelMode.NAMED_JOBS) {
     core.info(
       `# Cancel all runs for workflow ${sourceWorkflowId} have job names matching ${jobNameRegexps}.`
     )
+    reason = `It has jobs matching ${jobNameRegexps}.`
   } else if (cancelMode === CancelMode.DUPLICATES) {
     core.info(
       `# Cancel duplicate runs started before ${sourceRunId} for workflow ${sourceWorkflowId}.`
     )
+    reason = `It in earlier duplicate of ${sourceWorkflowId} run.`
   } else {
     throw Error(`Wrong cancel mode ${cancelMode}! This should never happen.`)
   }
@@ -512,6 +612,7 @@ async function performCancelJob(
 
   return await findAndCancelRuns(
     octokit,
+    selfRunId,
     sourceWorkflowId,
     sourceRunId,
     owner,
@@ -520,10 +621,18 @@ async function performCancelJob(
     headBranch,
     sourceEventName,
     cancelMode,
-    jobNameRegexps
+    notifyPRCancel,
+    notifyPRMessageStart,
+    jobNameRegexps,
+    reason
   )
 }
 
+function verboseOutput(name: string, value: string): void {
+  core.info(`Setting output: ${name}: ${value}`)
+  core.setOutput(name, value)
+}
+
 async function run(): Promise<void> {
   const token = core.getInput('token', {required: true})
   const octokit = new github.GitHub(token)
@@ -532,6 +641,9 @@ async function run(): Promise<void> {
   const eventName = getRequiredEnv('GITHUB_EVENT_NAME')
   const cancelMode =
     (core.getInput('cancelMode') as CancelMode) || CancelMode.DUPLICATES
+  const notifyPRCancel =
+    (core.getInput('notifyPRCancel') || 'false').toLowerCase() === 'true'
+  const notifyPRMessageStart = core.getInput('notifyPRMessageStart')
   const sourceRunId = parseInt(core.getInput('sourceRunId')) || selfRunId
   const jobNameRegexpsString = core.getInput('jobNameRegexps')
   const jobNameRegexps = jobNameRegexpsString
@@ -577,20 +689,40 @@ async function run(): Promise<void> {
       )
   }
 
-  const [headRepo, headBranch, sourceEventName, headSha] = await getOrigin(
-    octokit,
-    sourceRunId,
-    owner,
-    repo
+  const [
+    headRepo,
+    headBranch,
+    sourceEventName,
+    headSha,
+    mergeCommitSha,
+    pullRequest
+  ] = await getOrigin(octokit, sourceRunId, owner, repo)
+
+  verboseOutput('sourceHeadRepo', headRepo)
+  verboseOutput('sourceHeadBranch', headBranch)
+  verboseOutput('sourceHeadSha', headSha)
+  verboseOutput('sourceEvent', sourceEventName)
+  verboseOutput(
+    'pullRequestNumber',
+    pullRequest ? pullRequest.number.toString() : ''
   )
+  verboseOutput('mergeCommitSha', mergeCommitSha)
+  verboseOutput('targetCommitSha', pullRequest ? mergeCommitSha : headSha)
 
-  core.setOutput('sourceHeadRepo', headRepo)
-  core.setOutput('sourceHeadBranch', headBranch)
-  core.setOutput('sourceHeadSha', headSha)
-  core.setOutput('sourceEvent', sourceEventName)
+  const selfWorkflowRunUrl = `https://github.com/${owner}/${repo}/actions/runs/${selfRunId}`
+  if (notifyPRMessageStart && pullRequest) {
+    await octokit.issues.createComment({
+      owner,
+      repo,
+      // eslint-disable-next-line @typescript-eslint/camelcase
+      issue_number: pullRequest.number,
+      body: `${notifyPRMessageStart} [The workflow run](${selfWorkflowRunUrl})`
+    })
+  }
 
   const cancelledRuns = await performCancelJob(
     octokit,
+    selfRunId,
     sourceWorkflowId,
     sourceRunId,
     owner,
@@ -599,6 +731,8 @@ async function run(): Promise<void> {
     headBranch,
     sourceEventName,
     cancelMode,
+    notifyPRCancel,
+    notifyPRMessageStart,
     jobNameRegexps
   )