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-label-when-approved.git

commit 47b5dec5fb00d9050c71fd33a458201efff85515
Author: Tobiasz KÄ™dzierski <[email protected]>
AuthorDate: Mon Oct 26 15:34:16 2020 +0100

    Handle reviews
---
 LICENSE       |   2 +-
 README.md     |  57 +++++++++++++++++--
 action.yml    |   5 +-
 dist/index.js | 137 ++++++++++++++++++++++++++++++++++++++++------
 package.json  |   2 +-
 src/main.ts   | 172 +++++++++++++++++++++++++++++++++++++++++++++++++++-------
 6 files changed, 328 insertions(+), 47 deletions(-)

diff --git a/LICENSE b/LICENSE
index a67dca8..7d02166 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,7 +1,7 @@
 
 The MIT License (MIT)
 
-Copyright (c) 2018 GitHub, Inc. and contributors
+Copyright (c) 2020 GitHub, Inc. and contributors
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index c808581..a23ec7a 100644
--- a/README.md
+++ b/README.md
@@ -13,6 +13,8 @@
 - [Inputs and outputs](#inputs-and-outputs)
   - [Inputs](#inputs)
   - [Outputs](#outputs)
+- [Examples](#examples)
+    - [Workflow Run event](#workflow-run-event)
   - [Development environment](#development-environment)
   - [License](#license)
 
@@ -20,19 +22,64 @@
 
 # Context and motivation
 
-TODO
+Label When Approved is an action that checks is Pull Request is approved and 
assign label to it.
+Label is not set or removed when Pull Request has awaiting requested changes.
+
+Setting label is optional that only output can be used in the workflow.
+
+The required input `require_committers_approval` says is approval can be done 
by people with read access to the repo
+or by anyone. It may be useful in repositories which requires committers 
approvals like [Apache Software Foundation](https://github.com/apache/)
+projects.
 
 # Inputs and outputs
 
 ## Inputs
 
-| Input                   | Required | Default      | Comment                  
                                                                                
                                                                                
                        |
-|-------------------------|----------|--------------|-----------------------------------------------------------------------------------------------------|
-| `token`                 | yes      |              | The github token passed 
from `${{ secrets.GITHUB_TOKEN }}`                                          |
+| Input                         | Required | Example                       | 
Comment                                                                 |
+|-------------------------------|----------|-------------------------------|-------------------------------------------------------------------------|
+| `token`                       | yes      | `${{ secrets.GITHUB_TOKEN }}` | 
The github token passed from `${{ secrets.GITHUB_TOKEN }}`              |
+| `label`                       | no       | `Approved by committers`      | 
Label to be added/removed to the Pull Request if approved/not approved  |
+| `require_committers_approval` | no       | `true`                        | 
Is approval from user with write permission required                    |
 
 ## Outputs
 
-TODO
+| Output         |                              |
+|----------------|------------------------------|
+| `isApproved`   | is Pull Reqeuest is approved |
+| `labelSet`     | is label was set             |
+| `labelRemoved` | is label was removed         |
+
+# Examples
+
+### Workflow Run event
+
+```yaml
+name: Label when approved
+on: pull_request_review
+
+jobs:
+
+  label-when-approved:
+    name: "Label when approved"
+    runs-on: ubuntu-latest
+    outputs:
+      isApprovedByCommiters: ${{ 
steps.label-when-approved-by-commiters.outputs.isApproved }}
+      isApprovedByAnyone: ${{ 
steps.label-when-approved-by-anyone.outputs.isApproved }}
+    steps:
+      - name: Label when approved by commiters
+        uses: TobKed/[email protected]
+        id: label-when-approved-by-commiters
+        with:
+          token: ${{ secrets.GITHUB_TOKEN }}
+          label: 'ready to merge (committers)'
+          require_committers_approval: 'true'
+      - name: Label when approved by anyone
+        uses: TobKed/[email protected]
+        id: label-when-approved-by-anyone
+        with:
+          token: ${{ secrets.GITHUB_TOKEN }}
+```
+
 
 ## Development environment
 
diff --git a/action.yml b/action.yml
index caa7b4d..bd6f4e3 100644
--- a/action.yml
+++ b/action.yml
@@ -7,10 +7,11 @@ inputs:
     required: true
   label:
     description: A label to be checked/added/removed
-    required: true
+    required: false
   require_committers_approval:
     description: Is approval from person with write access to repo required
-    required: true
+    required: false
+    default: 'false'
 runs:
   using: 'node12'
   main: 'dist/index.js'
diff --git a/dist/index.js b/dist/index.js
index 2ab317a..c7ea88f 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -1466,6 +1466,10 @@ function getRequiredEnv(key) {
     }
     return value;
 }
+function verboseOutput(name, value) {
+    core.info(`Setting output: ${name}: ${value}`);
+    core.setOutput(name, value);
+}
 function getPullRequest(octokit, context, owner, repo) {
     return __awaiter(this, void 0, void 0, function* () {
         const pullRequestNumber = context.payload.pull_request
@@ -1492,31 +1496,38 @@ function getPullRequestLabels(pullRequest) {
 }
 function getReviews(octokit, owner, repo, number, getComitters) {
     return __awaiter(this, void 0, void 0, function* () {
-        const reviews = yield octokit.pulls.listReviews({
+        let reviews = [];
+        const options = octokit.pulls.listReviews.endpoint.merge({
             owner,
             repo,
             // eslint-disable-next-line @typescript-eslint/camelcase
             pull_number: number
         });
-        const reviewers = reviews ? reviews.data.map(review => 
review.user.login) : [];
+        yield octokit.paginate(options).then(r => {
+            reviews = r;
+        });
+        const reviewers = reviews ? reviews.map(review => review.user.login) : 
[];
+        const reviewersAlreadyChecked = [];
         const committers = [];
         if (getComitters) {
+            core.info('Checking reviewers permissions:');
             for (const reviewer of reviewers) {
-                if (!committers.includes(reviewer)) {
+                if (!reviewersAlreadyChecked.includes(reviewer)) {
                     const p = yield 
octokit.repos.getCollaboratorPermissionLevel({
                         owner,
                         repo,
                         username: reviewer
                     });
                     const permission = p.data.permission;
-                    core.info(`\nChecking: "${reviewer}" permissions: 
${permission}.\n`);
                     if (permission === 'admin' || permission === 'write') {
                         committers.push(reviewer);
                     }
+                    core.info(`\t${reviewer}: ${permission}`);
+                    reviewersAlreadyChecked.push(reviewer);
                 }
             }
         }
-        return [reviews.data, reviewers, committers];
+        return [reviews, reviewers, committers];
     });
 }
 function processReviews(reviews, reviewers, committers, 
requireCommittersApproval) {
@@ -1532,9 +1543,9 @@ function processReviews(reviews, reviewers, committers, 
requireCommittersApprova
             }
         }
     }
-    core.info(`User reviews:`);
+    core.info(`Reviews:`);
     for (const user in reviewStates) {
-        core.info(`User "${user}" : "${reviewStates[user]}"`);
+        core.info(`\t${user}: ${reviewStates[user].toLowerCase()}`);
     }
     for (const user in reviewStates) {
         if (reviewStates[user] === 'APPROVED') {
@@ -1552,6 +1563,7 @@ function processReviews(reviews, reviewers, committers, 
requireCommittersApprova
 }
 function setLabel(octokit, owner, repo, pullRequestNumber, label) {
     return __awaiter(this, void 0, void 0, function* () {
+        core.info(`Setting label: ${label}`);
         yield octokit.issues.addLabels({
             // eslint-disable-next-line @typescript-eslint/camelcase
             issue_number: pullRequestNumber,
@@ -1563,6 +1575,7 @@ function setLabel(octokit, owner, repo, 
pullRequestNumber, label) {
 }
 function removeLabel(octokit, owner, repo, pullRequestNumber, label) {
     return __awaiter(this, void 0, void 0, function* () {
+        core.info(`Removing label: ${label}`);
         yield octokit.issues.removeLabel({
             // eslint-disable-next-line @typescript-eslint/camelcase
             issue_number: pullRequestNumber,
@@ -1572,6 +1585,61 @@ function removeLabel(octokit, owner, repo, 
pullRequestNumber, label) {
         });
     });
 }
+function getWorkflowId(octokit, runId, owner, repo) {
+    return __awaiter(this, void 0, void 0, function* () {
+        const reply = yield octokit.actions.getWorkflowRun({
+            owner,
+            repo,
+            // eslint-disable-next-line @typescript-eslint/camelcase
+            run_id: runId
+        });
+        core.info(`The source run ${runId} is in ${reply.data.workflow_url} 
workflow`);
+        const workflowIdString = reply.data.workflow_url.split('/').pop() || 
'';
+        if (!(workflowIdString.length > 0)) {
+            throw new Error('Could not resolve workflow');
+        }
+        return parseInt(workflowIdString);
+    });
+}
+function getPrWorkflowRunsIds(octokit, owner, repo, branch, sha, skipRunId) {
+    return __awaiter(this, void 0, void 0, function* () {
+        const workflowRuns = yield octokit.actions.listRepoWorkflowRuns({
+            owner,
+            repo,
+            branch,
+            event: 'pull_request',
+            status: 'completed',
+            // eslint-disable-next-line @typescript-eslint/camelcase
+            per_page: 100
+        });
+        // may be no need to rerun pending/queued runs
+        const filteredRunsIds = [];
+        const filteredWorklowRunsIds = [];
+        for (const workflowRun of workflowRuns.data.workflow_runs) {
+            const workflowId = 
parseInt(workflowRun.workflow_url.split('/').pop() || '0');
+            if (workflowRun.head_sha === sha &&
+                !filteredRunsIds.includes(workflowId) &&
+                workflowId !== skipRunId) {
+                filteredRunsIds.push(workflowId);
+                filteredWorklowRunsIds.push(workflowRun.id);
+            }
+        }
+        return filteredWorklowRunsIds;
+    });
+}
+function rerunWorkflows(octokit, owner, repo, runIds) {
+    return __awaiter(this, void 0, void 0, function* () {
+        core.info(`Rerun worklowws: ${runIds}`);
+        for (const runId of runIds) {
+            yield octokit.actions.reRunWorkflow({
+                owner,
+                repo,
+                // eslint-disable-next-line @typescript-eslint/camelcase
+                run_id: runId
+            });
+        }
+    });
+}
 function printDebug(item, description) {
     return __awaiter(this, void 0, void 0, function* () {
         const itemJson = JSON.stringify(item);
@@ -1579,17 +1647,25 @@ function printDebug(item, description) {
     });
 }
 function run() {
+    var _a, _b;
     return __awaiter(this, void 0, void 0, function* () {
         const token = core.getInput('token', { required: true });
-        const userLabel = core.getInput('label', { required: true });
+        const userLabel = core.getInput('label', { required: false }) || 'not 
set';
         const requireCommittersApproval = 
core.getInput('require_committers_approval', {
-            required: true
-        });
+            required: false
+        }) === 'true';
         const octokit = new github.GitHub(token);
         const context = github.context;
         const repository = getRequiredEnv('GITHUB_REPOSITORY');
         const eventName = getRequiredEnv('GITHUB_EVENT_NAME');
+        const selfRunId = parseInt(getRequiredEnv('GITHUB_RUN_ID'));
         const [owner, repo] = repository.split('/');
+        const selfWorkflowId = yield getWorkflowId(octokit, selfRunId, owner, 
repo);
+        const branch = (_a = context.payload.pull_request) === null || _a === 
void 0 ? void 0 : _a.head.ref;
+        const sha = (_b = context.payload.pull_request) === null || _b === 
void 0 ? void 0 : _b.head.sha;
+        core.info(`\n############### Set Label When Approved start 
##################\n` +
+            `label: "${userLabel}"\n` +
+            `requireCommittersApproval: ${requireCommittersApproval}`);
         if (eventName !== 'pull_request_review') {
             throw Error(`This action is only useful in "pull_request_review" 
triggered runs and you used it in "${eventName}"`);
         }
@@ -1598,15 +1674,42 @@ function run() {
         // LABELS
         const labelNames = getPullRequestLabels(pullRequest);
         // REVIEWS
-        const [reviews, reviewers, committers] = yield getReviews(octokit, 
owner, repo, pullRequest.number, requireCommittersApproval === 'true');
-        const isApproved = processReviews(reviews, reviewers, committers, 
requireCommittersApproval === 'true');
+        const [reviews, reviewers, committers] = yield getReviews(octokit, 
owner, repo, pullRequest.number, requireCommittersApproval);
+        const isApproved = processReviews(reviews, reviewers, committers, 
requireCommittersApproval);
         // HANDLE LABEL
-        if (isApproved && !labelNames.includes(userLabel)) {
-            setLabel(octokit, owner, repo, pullRequest.number, userLabel);
-        }
-        else if (!isApproved && labelNames.includes(userLabel)) {
-            removeLabel(octokit, owner, repo, pullRequest.number, userLabel);
+        let isLabelShouldBeSet = false;
+        let isLabelShouldBeRemoved = false;
+        if (userLabel !== 'not set') {
+            isLabelShouldBeSet = isApproved && !labelNames.includes(userLabel);
+            isLabelShouldBeRemoved = !isApproved && 
labelNames.includes(userLabel);
+            if (isLabelShouldBeSet) {
+                setLabel(octokit, owner, repo, pullRequest.number, userLabel);
+            }
+            else if (isLabelShouldBeRemoved) {
+                removeLabel(octokit, owner, repo, pullRequest.number, 
userLabel);
+            }
         }
+        //// Future option to rerun workflows if PR approved
+        //// Rerun workflow can have dynamic matrixes which check presence of 
labels
+        //// it is not possible to rerun successful runs
+        //// 
https://github.community/t/cannot-re-run-a-successful-workflow-run-using-the-rest-api/123661
+        //
+        // if (isLabelShouldBeSet) {
+        //   const prWorkflowRunsIds = await getPrWorkflowRunsIds(
+        //     octokit,
+        //     owner,
+        //     repo,
+        //     branch,
+        //     sha,
+        //     selfWorkflowId
+        //   )
+        //
+        //   await rerunWorkflows(octokit, owner, repo, prWorkflowRunsIds)
+        // }
+        // OUTPUT
+        verboseOutput('isApproved', String(isApproved));
+        verboseOutput('labelSet', String(isLabelShouldBeSet));
+        verboseOutput('labelRemoved', String(isLabelShouldBeRemoved));
     });
 }
 run()
diff --git a/package.json b/package.json
index ec8796a..49111ea 100644
--- a/package.json
+++ b/package.json
@@ -23,7 +23,7 @@
     "node",
     "setup"
   ],
-  "author": "YourNameOrOrganization",
+  "author": "Tobiasz Kedzierski",
   "license": "MIT",
   "dependencies": {
     "@actions/core": "^1.2.2",
diff --git a/src/main.ts b/src/main.ts
index 0d5469c..dac2b83 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -12,6 +12,11 @@ function getRequiredEnv(key: string): string {
   return value
 }
 
+function verboseOutput(name: string, value: string): void {
+  core.info(`Setting output: ${name}: ${value}`)
+  core.setOutput(name, value)
+}
+
 async function getPullRequest(
   octokit: github.GitHub,
   context: Context,
@@ -48,31 +53,39 @@ async function getReviews(
   number: number,
   getComitters: boolean
 ): Promise<[rest.PullsListReviewsResponseItem[], string[], string[]]> {
-  const reviews = await octokit.pulls.listReviews({
+  let reviews: rest.PullsListReviewsResponseItem[] = []
+  const options = octokit.pulls.listReviews.endpoint.merge({
     owner,
     repo,
     // eslint-disable-next-line @typescript-eslint/camelcase
     pull_number: number
   })
-  const reviewers = reviews ? reviews.data.map(review => review.user.login) : 
[]
+  await octokit.paginate(options).then(r => {
+    reviews = r
+  })
+
+  const reviewers = reviews ? reviews.map(review => review.user.login) : []
+  const reviewersAlreadyChecked: string[] = []
   const committers: string[] = []
   if (getComitters) {
+    core.info('Checking reviewers permissions:')
     for (const reviewer of reviewers) {
-      if (!committers.includes(reviewer)) {
+      if (!reviewersAlreadyChecked.includes(reviewer)) {
         const p = await octokit.repos.getCollaboratorPermissionLevel({
           owner,
           repo,
           username: reviewer
         })
         const permission = p.data.permission
-        core.info(`\nChecking: "${reviewer}" permissions: ${permission}.\n`)
         if (permission === 'admin' || permission === 'write') {
           committers.push(reviewer)
         }
+        core.info(`\t${reviewer}: ${permission}`)
+        reviewersAlreadyChecked.push(reviewer)
       }
     }
   }
-  return [reviews.data, reviewers, committers]
+  return [reviews, reviewers, committers]
 }
 
 function processReviews(
@@ -94,9 +107,9 @@ function processReviews(
     }
   }
 
-  core.info(`User reviews:`)
+  core.info(`Reviews:`)
   for (const user in reviewStates) {
-    core.info(`User "${user}" : "${reviewStates[user]}"`)
+    core.info(`\t${user}: ${reviewStates[user].toLowerCase()}`)
   }
 
   for (const user in reviewStates) {
@@ -122,6 +135,7 @@ async function setLabel(
   pullRequestNumber: number,
   label: string
 ): Promise<void> {
+  core.info(`Setting label: ${label}`)
   await octokit.issues.addLabels({
     // eslint-disable-next-line @typescript-eslint/camelcase
     issue_number: pullRequestNumber,
@@ -138,6 +152,7 @@ async function removeLabel(
   pullRequestNumber: number,
   label: string
 ): Promise<void> {
+  core.info(`Removing label: ${label}`)
   await octokit.issues.removeLabel({
     // eslint-disable-next-line @typescript-eslint/camelcase
     issue_number: pullRequestNumber,
@@ -147,8 +162,84 @@ async function removeLabel(
   })
 }
 
+async function getWorkflowId(
+  octokit: github.GitHub,
+  runId: number,
+  owner: string,
+  repo: string
+): Promise<number> {
+  const reply = await octokit.actions.getWorkflowRun({
+    owner,
+    repo,
+    // eslint-disable-next-line @typescript-eslint/camelcase
+    run_id: runId
+  })
+  core.info(`The source run ${runId} is in ${reply.data.workflow_url} 
workflow`)
+  const workflowIdString = reply.data.workflow_url.split('/').pop() || ''
+  if (!(workflowIdString.length > 0)) {
+    throw new Error('Could not resolve workflow')
+  }
+  return parseInt(workflowIdString)
+}
+
+async function getPrWorkflowRunsIds(
+  octokit: github.GitHub,
+  owner: string,
+  repo: string,
+  branch: string,
+  sha: string,
+  skipRunId: number
+): Promise<number[]> {
+  const workflowRuns = await octokit.actions.listRepoWorkflowRuns({
+    owner,
+    repo,
+    branch,
+    event: 'pull_request',
+    status: 'completed',
+    // eslint-disable-next-line @typescript-eslint/camelcase
+    per_page: 100
+  })
+  // may be no need to rerun pending/queued runs
+  const filteredRunsIds: number[] = []
+  const filteredWorklowRunsIds: number[] = []
+
+  for (const workflowRun of workflowRuns.data.workflow_runs) {
+    const workflowId = parseInt(
+      workflowRun.workflow_url.split('/').pop() || '0'
+    )
+
+    if (
+      workflowRun.head_sha === sha &&
+      !filteredRunsIds.includes(workflowId) &&
+      workflowId !== skipRunId
+    ) {
+      filteredRunsIds.push(workflowId)
+      filteredWorklowRunsIds.push(workflowRun.id)
+    }
+  }
+
+  return filteredWorklowRunsIds
+}
+
+async function rerunWorkflows(
+  octokit: github.GitHub,
+  owner: string,
+  repo: string,
+  runIds: number[]
+): Promise<void> {
+  core.info(`Rerun worklowws: ${runIds}`)
+  for (const runId of runIds) {
+    await octokit.actions.reRunWorkflow({
+      owner,
+      repo,
+      // eslint-disable-next-line @typescript-eslint/camelcase
+      run_id: runId
+    })
+  }
+}
+
 async function printDebug(
-  item: object | string | boolean,
+  item: object | string | boolean | number,
   description: string
 ): Promise<void> {
   const itemJson = JSON.stringify(item)
@@ -157,18 +248,26 @@ async function printDebug(
 
 async function run(): Promise<void> {
   const token = core.getInput('token', {required: true})
-  const userLabel = core.getInput('label', {required: true})
-  const requireCommittersApproval = core.getInput(
-    'require_committers_approval',
-    {
-      required: true
-    }
-  )
+  const userLabel = core.getInput('label', {required: false}) || 'not set'
+  const requireCommittersApproval =
+    core.getInput('require_committers_approval', {
+      required: false
+    }) === 'true'
   const octokit = new github.GitHub(token)
   const context = github.context
   const repository = getRequiredEnv('GITHUB_REPOSITORY')
   const eventName = getRequiredEnv('GITHUB_EVENT_NAME')
+  const selfRunId = parseInt(getRequiredEnv('GITHUB_RUN_ID'))
   const [owner, repo] = repository.split('/')
+  const selfWorkflowId = await getWorkflowId(octokit, selfRunId, owner, repo)
+  const branch = context.payload.pull_request?.head.ref
+  const sha = context.payload.pull_request?.head.sha
+
+  core.info(
+    `\n############### Set Label When Approved start ##################\n` +
+      `label: "${userLabel}"\n` +
+      `requireCommittersApproval: ${requireCommittersApproval}`
+  )
 
   if (eventName !== 'pull_request_review') {
     throw Error(
@@ -188,21 +287,52 @@ async function run(): Promise<void> {
     owner,
     repo,
     pullRequest.number,
-    requireCommittersApproval === 'true'
+    requireCommittersApproval
   )
   const isApproved = processReviews(
     reviews,
     reviewers,
     committers,
-    requireCommittersApproval === 'true'
+    requireCommittersApproval
   )
 
   // HANDLE LABEL
-  if (isApproved && !labelNames.includes(userLabel)) {
-    setLabel(octokit, owner, repo, pullRequest.number, userLabel)
-  } else if (!isApproved && labelNames.includes(userLabel)) {
-    removeLabel(octokit, owner, repo, pullRequest.number, userLabel)
+  let isLabelShouldBeSet = false
+  let isLabelShouldBeRemoved = false
+
+  if (userLabel !== 'not set') {
+    isLabelShouldBeSet = isApproved && !labelNames.includes(userLabel)
+    isLabelShouldBeRemoved = !isApproved && labelNames.includes(userLabel)
+
+    if (isLabelShouldBeSet) {
+      setLabel(octokit, owner, repo, pullRequest.number, userLabel)
+    } else if (isLabelShouldBeRemoved) {
+      removeLabel(octokit, owner, repo, pullRequest.number, userLabel)
+    }
   }
+
+  //// Future option to rerun workflows if PR approved
+  //// Rerun workflow can have dynamic matrixes which check presence of labels
+  //// it is not possible to rerun successful runs
+  //// 
https://github.community/t/cannot-re-run-a-successful-workflow-run-using-the-rest-api/123661
+  //
+  // if (isLabelShouldBeSet) {
+  //   const prWorkflowRunsIds = await getPrWorkflowRunsIds(
+  //     octokit,
+  //     owner,
+  //     repo,
+  //     branch,
+  //     sha,
+  //     selfWorkflowId
+  //   )
+  //
+  //   await rerunWorkflows(octokit, owner, repo, prWorkflowRunsIds)
+  // }
+
+  // OUTPUT
+  verboseOutput('isApproved', String(isApproved))
+  verboseOutput('labelSet', String(isLabelShouldBeSet))
+  verboseOutput('labelRemoved', String(isLabelShouldBeRemoved))
 }
 
 run()

Reply via email to