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 <[email protected]>
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
   )
 

Reply via email to