-
Notifications
You must be signed in to change notification settings - Fork 143
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: gh action to apply PR review labels #5656
Merged
elycheea
merged 8 commits into
carbon-design-system:main
from
matthewgallo:5535-gh-action-for-pr-reviews
Sep 12, 2024
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
87a9866
feat: add action for labeling PRs
matthewgallo 0bcb4ee
chore: comment update
matthewgallo 14c875b
chore: update another comment
matthewgallo 3d0b681
chore: update gitignore
matthewgallo ca737da
chore: rename file
matthewgallo 68be051
chore: cleanup
matthewgallo 3d7ed77
chore: fix conflict
matthewgallo d9f73d9
Merge branch 'main' into 5535-gh-action-for-pr-reviews
davidmenendez File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
name: Process saved PR data and apply appropriate labels | ||
|
||
# We have access to repo secrets from here. If the `Received PR review` workflow has completed | ||
# it means there has been a workflow artifact created with the data we need to apply the appropriate | ||
# review labels from the custom action used from this workflow | ||
on: | ||
workflow_run: | ||
workflows: ['Received PR review'] | ||
types: | ||
- completed | ||
|
||
jobs: | ||
upload: | ||
runs-on: ubuntu-latest | ||
if: > | ||
github.event.workflow_run.event == 'pull_request_review' && | ||
github.event.workflow_run.conclusion == 'success' | ||
steps: | ||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 | ||
- name: Setup Node.js | ||
uses: actions/setup-node@v2 | ||
with: | ||
node-version: '20.x' | ||
cache: yarn | ||
- uses: ./actions/add-review-labels | ||
with: | ||
APP_ID: ${{ secrets.APP_ID }} | ||
APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
# Save PR review data to as artifact to process in a privileged workflow that is dispatched from this one | ||
name: Received PR review | ||
on: [pull_request_review] | ||
jobs: | ||
pr_review_submitted: | ||
runs-on: ubuntu-latest | ||
permissions: | ||
contents: write | ||
issues: write | ||
pull-requests: write | ||
if: ${{contains(github.event_name, 'pull_request_review')}} | ||
# First save github.event (from the unprivileged workflow) and then pass it through to the privileged action | ||
# where we have access to repo secrets | ||
steps: | ||
- name: Store GitHub event in a workflow artifact | ||
id: github_event_step | ||
env: | ||
JSON: ${{ toJSON(github.event) }} | ||
run: | | ||
mkdir -p ./pr_data | ||
echo $JSON > ./pr_data/github.json | ||
- name: Upload event data | ||
id: upload_artifact | ||
uses: actions/upload-artifact@v3 | ||
with: | ||
name: pr-data-to-process | ||
path: pr_data/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
**/__mocks__/** | ||
**/__tests__/** | ||
**/examples/** | ||
**/tasks/** |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
FROM node:slim | ||
|
||
WORKDIR /usr/src/action | ||
COPY . . | ||
RUN yarn install --production | ||
ENTRYPOINT ["node", "/usr/src/action/index.js"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
name: Add review labels | ||
description: A custom action that adds review labels to a Pull Request | ||
inputs: | ||
APP_ID: | ||
description: GitHub app id | ||
required: true | ||
APP_PRIVATE_KEY: | ||
description: GitHub app private key | ||
required: true | ||
runs: | ||
using: 'docker' | ||
image: 'Dockerfile' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,212 @@ | ||
/** | ||
* Copyright IBM Corp. 2020, 2024 | ||
* | ||
* This source code is licensed under the Apache-2.0 license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
*/ | ||
|
||
'use strict'; | ||
|
||
import github from '@actions/github'; | ||
import core from '@actions/core'; | ||
import { App } from 'octokit'; | ||
import util from 'util'; | ||
import decompress from 'decompress'; | ||
|
||
async function run() { | ||
const { context } = github; | ||
const appId = core.getInput('APP_ID', { | ||
required: true, | ||
}); | ||
const privateKey = core.getInput('APP_PRIVATE_KEY', { | ||
required: true, | ||
}); | ||
const app = new App({ appId, privateKey }); | ||
const octokit = await app.getInstallationOctokit(52238220); | ||
|
||
const { workflow_run, repository, organization } = context.payload; | ||
const workflowRunId = workflow_run.id; | ||
|
||
const { data: workflowArtifacts } = await octokit.request( | ||
'GET /repos/{owner}/{repo}/actions/runs/{run_id}/artifacts', | ||
{ | ||
owner: organization.login, | ||
repo: repository.name, | ||
run_id: workflowRunId, | ||
headers: { | ||
'X-GitHub-Api-Version': '2022-11-28', | ||
}, | ||
} | ||
); | ||
|
||
const matchArtifact = workflowArtifacts.artifacts.filter((artifact) => { | ||
return artifact.name == 'pr-data-to-process'; | ||
})[0]; | ||
|
||
const artifactResponse = await octokit.request( | ||
'GET /repos/{owner}/{repo}/actions/artifacts/{artifact_id}/{archive_format}', | ||
{ | ||
owner: organization.login, | ||
repo: repository.name, | ||
artifact_id: matchArtifact.id, | ||
archive_format: 'zip', | ||
headers: { | ||
'X-GitHub-Api-Version': '2022-11-28', | ||
}, | ||
} | ||
); | ||
|
||
// Decode the array buffer from the artifact to read initial review PR data from a privileged workflow | ||
|
||
// Convert ArrayBuffer to Buffer | ||
const buff = Buffer.from(artifactResponse.data); | ||
|
||
// Decompress the file | ||
const files = await decompress(buff); | ||
|
||
// Decode the decompressed buffer | ||
const decodedArtifact = new util.TextDecoder().decode(files[0].data); | ||
|
||
// Parse decoded buffer | ||
const parsedDecodedArtifact = JSON.parse(decodedArtifact); | ||
|
||
const { pull_request, review } = parsedDecodedArtifact; | ||
const { state, draft } = pull_request; | ||
|
||
// We only want to work with Pull Requests that are marked as open | ||
if (state !== 'open') { | ||
return; | ||
} | ||
|
||
// We only want to work with Pull Requests that are not draft PRs | ||
if (draft) { | ||
return; | ||
} | ||
|
||
// If the review was not an approval then we'll ignore the event | ||
if (review && review.state !== 'approved') { | ||
return; | ||
} | ||
|
||
const { data: allReviews } = await octokit.rest.pulls.listReviews({ | ||
owner: repository.owner.login, | ||
repo: repository.name, | ||
pull_number: pull_request.number, | ||
per_page: 100, | ||
}); | ||
|
||
// Get reviewer team data | ||
const { data } = await octokit.request('GET /orgs/{org}/teams/{team_slug}', { | ||
org: organization.login, | ||
team_slug: 'reviewing-team', // Should be only hardcoded value (outside of the labels) needed within this action. Replace with the appropriate reviewing team that is assigned to review PRs. | ||
headers: { | ||
'X-GitHub-Api-Version': '2022-11-28', | ||
}, | ||
}); | ||
const { members_url } = data; | ||
|
||
const org_id = members_url.split('organizations/').pop().split('/team')[0]; | ||
const team_id = members_url.split('team/').pop().split('/members')[0]; | ||
|
||
const { data: teamMembers } = await octokit.request( | ||
'GET /organizations/{org_id}/team/{team_id}/members', | ||
{ | ||
org_id, | ||
team_id, | ||
headers: { | ||
Accept: 'application/vnd.github+json', | ||
'X-GitHub-Api-Version': '2022-11-28', | ||
}, | ||
} | ||
); | ||
|
||
const additionalReviewLabel = 'status: one more review 👀'; | ||
const readyForReviewLabel = 'status: ready for review 👀'; | ||
|
||
// If we find that the reviewing user is not part of the reviewing team | ||
// then we don't want to count their review so we stop here | ||
const reviewingUser = review.user.login; | ||
if (!teamMembers.filter((user) => user.login === reviewingUser).length) { | ||
return; | ||
} | ||
|
||
// The `listReviews` endpoint will return all of the reviews for the pull | ||
// request. We only care about the most recent reviews so we'll go through the | ||
// list and get the most recent review for each reviewer | ||
const reviewers = {}; | ||
const reviews = []; | ||
|
||
// Process reviews in reverse order since they are listed from oldest to newest | ||
for (const review of allReviews.reverse()) { | ||
const { user } = review; | ||
// If we've already saved a review for this user we already have the most | ||
// recent review | ||
if (reviewers[user.login]) { | ||
continue; | ||
} | ||
|
||
// If the author of the review not part of the reviewer team we ignore it | ||
if (!teamMembers.filter((u) => u.login === user.login).length) { | ||
continue; | ||
} | ||
|
||
reviewers[user.login] = true; | ||
reviews.push(review); | ||
} | ||
|
||
const approved = reviews.filter((review) => { | ||
return review.state === 'APPROVED'; | ||
}); | ||
|
||
if (approved.length > 0) { | ||
const hasReadyLabel = pull_request.labels.find((label) => { | ||
return label.name === readyForReviewLabel; | ||
}); | ||
// Remove ready for review label if there is at least one approval | ||
if (hasReadyLabel) { | ||
await octokit.rest.issues.removeLabel({ | ||
owner: repository.owner.login, | ||
repo: repository.name, | ||
issue_number: pull_request.number, | ||
name: readyForReviewLabel, | ||
}); | ||
} | ||
} | ||
|
||
if (approved.length === 1) { | ||
const hasAdditionalReviewLabel = pull_request.labels.find((label) => { | ||
return label.name === additionalReviewLabel; | ||
}); | ||
// Add the one more review label if there's at least one approval and it doesn't have the label already | ||
if (!hasAdditionalReviewLabel) { | ||
await octokit.rest.issues.addLabels({ | ||
owner: repository.owner.login, | ||
repo: repository.name, | ||
issue_number: pull_request.number, | ||
labels: [additionalReviewLabel], | ||
}); | ||
} | ||
return; | ||
} | ||
|
||
if (approved.length >= 2) { | ||
const hasAdditionalReviewLabel = pull_request.labels.find((label) => { | ||
return label.name === additionalReviewLabel; | ||
}); | ||
// Remove the one more review label if there are at least 2 approvals from the reviewing team | ||
if (hasAdditionalReviewLabel) { | ||
await octokit.rest.issues.removeLabel({ | ||
owner: repository.owner.login, | ||
repo: repository.name, | ||
issue_number: pull_request.number, | ||
name: additionalReviewLabel, | ||
}); | ||
} | ||
return; | ||
} | ||
} | ||
|
||
run().catch((error) => { | ||
console.log(error); | ||
process.exit(1); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
{ | ||
"name": "@carbon/actions-add-review-labels", | ||
"private": true, | ||
"version": "0.0.0", | ||
"license": "Apache-2.0", | ||
"main": "index.js", | ||
"type": "module", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/carbon-design-system/carbon.git", | ||
"directory": "actions/add-review-labels" | ||
}, | ||
"bugs": "https://github.com/carbon-design-system/carbon/issues", | ||
"keywords": [ | ||
"ibm", | ||
"carbon", | ||
"carbon-design-system", | ||
"components", | ||
"react" | ||
], | ||
"dependencies": { | ||
"@actions/core": "^1.2.3", | ||
"@actions/github": "^6.0.0", | ||
"decompress": "^4.2.1", | ||
"octokit": "^4.0.2" | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don’t think we really need this step, unless there’s an operational reason we want to pull the label back off. But doesn’t really hurt either.