Skip to content
This repository has been archived by the owner on Sep 19, 2024. It is now read-only.

feat: incentive for PullRequest reviewers #657

Merged
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4b973fc
feat: incentiveForPullRequest reviewers
wannacfuture Aug 23, 2023
aa84e7d
feat: minor fix
wannacfuture Aug 23, 2023
109758e
feat: slight change
wannacfuture Aug 23, 2023
12bccae
feat: fix function declaration
wannacfuture Aug 23, 2023
25c4da5
feat: renamed some variables
wannacfuture Aug 23, 2023
efb941d
feat: get comments as markdown
wannacfuture Aug 23, 2023
06d688a
feat: revert changing function names
wannacfuture Aug 24, 2023
f59fd04
Merge remote-tracking branch 'origin' into feat/incentive-for-pr-revi…
wannacfuture Aug 24, 2023
07b2275
feat: minor fix
wannacfuture Aug 24, 2023
7104dd3
feat: minor fix
wannacfuture Aug 24, 2023
66b0b9b
Merge branch 'development' into feat/incentive-for-pr-reviewer
wannacfuture Aug 25, 2023
54f0273
Merge branch 'development' into feat/incentive-for-pr-reviewer
wannacfuture Aug 25, 2023
66d2d7b
feat: slight change
wannacfuture Aug 25, 2023
e4d5249
Merge branch 'development' into feat/incentive-for-pr-reviewer
wannacfuture Aug 28, 2023
5ec2d6a
feat: get correct linked pull request
wannacfuture Aug 28, 2023
d0f1b06
feat: check the organization and repo name
wannacfuture Aug 28, 2023
0109700
Merge branch 'development' into feat/incentive-for-pr-reviewer
wannacfuture Aug 29, 2023
5a0f18e
Merge branch 'development' into feat/incentive-for-pr-reviewer
wannacfuture Aug 29, 2023
9b8a28e
Merge branch 'development' into feat/incentive-for-pr-reviewer
wannacfuture Sep 4, 2023
8ce9053
feat: fix for nonse in incentive pr review
wannacfuture Sep 4, 2023
3f27933
Merge branch 'feat/incentive-for-pr-reviewer' of https://github.com/w…
wannacfuture Sep 4, 2023
b455c96
Merge branch 'development' into feat/incentive-for-pr-reviewer
wannacfuture Sep 12, 2023
c6b6709
feat: include comments on pr
wannacfuture Sep 12, 2023
7393511
Merge branch 'development' into feat/incentive-for-pr-reviewer
wannacfuture Sep 13, 2023
51fe4df
Merge branch 'development' into feat/incentive-for-pr-reviewer
wannacfuture Sep 14, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,16 @@
"@sinclair/typebox": "^0.25.9",
"@supabase/supabase-js": "^2.4.0",
"@types/ms": "^0.7.31",
"@types/parse5": "^7.0.0",
"@typescript-eslint/eslint-plugin": "^5.59.11",
"@typescript-eslint/parser": "^5.59.11",
"@uniswap/permit2-sdk": "^1.2.0",
"@vercel/ncc": "^0.34.0",
"ajv": "^8.11.2",
"ajv-formats": "^2.1.1",
"axios": "^1.3.2",
"decimal.js": "^10.4.3",
"copyfiles": "^2.4.1",
"decimal.js": "^10.4.3",
"ethers": "^5.7.2",
"husky": "^8.0.2",
"jimp": "^0.22.4",
Expand Down
144 changes: 143 additions & 1 deletion src/handlers/payout/post.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import { getWalletAddress } from "../../adapters/supabase";
import { getBotConfig, getBotContext, getLogger } from "../../bindings";
import { addCommentToIssue, generatePermit2Signature, getAllIssueComments, getIssueDescription, getTokenSymbol, parseComments } from "../../helpers";
import {
addCommentToIssue,
generatePermit2Signature,
getAllIssueComments,
getAllPullRequestReviews,
getAllPullRequests,
getIssueDescription,
getTokenSymbol,
parseComments,
} from "../../helpers";
import { gitLinkedIssueParser } from "../../helpers/parser";
import { Incentives, MarkdownItem, Payload, StateReason, UserType } from "../../types";
import { commentParser } from "../comment";
import Decimal from "decimal.js";
Expand Down Expand Up @@ -121,6 +131,138 @@ export const incentivizeComments = async () => {
await addCommentToIssue(comment, issue.number);
};

export const incentivizePullRequestReviews = async () => {
const logger = getLogger();
const {
mode: { incentiveMode, paymentPermitMaxPrice },
price: { baseMultiplier, incentives },
payout: { paymentToken, rpc },
} = getBotConfig();
if (!incentiveMode) {
logger.info(`No incentive mode. skipping to process`);
// return;
}
const context = getBotContext();
const payload = context.payload as Payload;
const issue = payload.issue;
if (!issue) {
logger.info(`Incomplete payload. issue: ${issue}`);
return;
}

if (issue.state_reason !== StateReason.COMPLETED) {
logger.info("incentivizePullRequestReviews: comment incentives skipped because the issue was not closed as completed");
// return;
}

if (paymentPermitMaxPrice == 0 || !paymentPermitMaxPrice) {
logger.info(`incentivizePullRequestReviews: skipping to generate permit2 url, reason: { paymentPermitMaxPrice: ${paymentPermitMaxPrice}}`);
// return;
}

const issueDetailed = bountyInfo(issue);
if (!issueDetailed.isBounty) {
logger.info(`incentivizePullRequestReviews: its not a bounty`);
// return;
wannacfuture marked this conversation as resolved.
Show resolved Hide resolved
}

const pulls = await getAllPullRequests(context, "closed");
if (pulls.length === 0) {
logger.debug(`incentivizePullRequestReviews: No pull requests found at this time`);
return;
}

let linkedPull;

for (const pull of pulls) {
const pullRequestLinked = await gitLinkedIssueParser({
wannacfuture marked this conversation as resolved.
Show resolved Hide resolved
owner: payload.repository.owner.login,
repo: payload.repository.name,
issue_number: pull.number,
wannacfuture marked this conversation as resolved.
Show resolved Hide resolved
});
const linkedIssueNumber = pullRequestLinked.substring(pullRequestLinked.lastIndexOf("/") + 1);
if (linkedIssueNumber === issue.number.toString()) {
linkedPull = pull;
break;
}
}

if (!linkedPull) {
logger.debug(`incentivizePullRequestReviews: No linked pull requests found`);
return;
}

const comments = await getAllIssueComments(issue.number);
const permitComments = comments.filter(
(content) => content.body.includes("Reviewer Rewards") && content.body.includes("https://pay.ubq.fi?claim=") && content.user.type == UserType.Bot
wannacfuture marked this conversation as resolved.
Show resolved Hide resolved
);
if (permitComments.length > 0) {
logger.info(`incentivizePullRequestReviews: skip to generate a permit url because it has been already posted`);
return;
}

const assignees = issue?.assignees ?? [];
const assignee = assignees.length > 0 ? assignees[0] : undefined;
if (!assignee) {
logger.info("incentivizePullRequestReviews: skipping payment permit generation because `assignee` is `undefined`.");
return;
}

const prReviews = await getAllPullRequestReviews(context, linkedPull.number);
logger.info(`Getting the PR reviews done. comments: ${JSON.stringify(prReviews)}`);
const prReviewsByUser: Record<string, string[]> = {};
for (const review of prReviews) {
const user = review.user;
if (!user) continue;
if (user.type == UserType.Bot || user.login == assignee) continue;
if (!review.body) {
logger.info(`incentivizePullRequestReviews: Skipping to parse the comment because body_html is undefined. comment: ${JSON.stringify(review)}`);
continue;
}
if (!prReviewsByUser[user.login]) {
prReviewsByUser[user.login] = [];
}
prReviewsByUser[user.login].push(review.body);
wannacfuture marked this conversation as resolved.
Show resolved Hide resolved
wannacfuture marked this conversation as resolved.
Show resolved Hide resolved
}
const tokenSymbol = await getTokenSymbol(paymentToken, rpc);
logger.info(`incentivizePullRequestReviews: Filtering by the user type done. commentsByUser: ${JSON.stringify(prReviewsByUser)}`);

// The mapping between gh handle and comment with a permit url
const reward: Record<string, string> = {};

// The mapping between gh handle and amount in ETH
const fallbackReward: Record<string, Decimal> = {};
wannacfuture marked this conversation as resolved.
Show resolved Hide resolved
let comment = `#### Reviewer Rewards\n`;
for (const user of Object.keys(prReviewsByUser)) {
const comments = prReviewsByUser[user];
const commentsByNode = await parseComments(comments, ItemsToExclude);
const rewardValue = calculateRewardValue(commentsByNode, incentives);
if (rewardValue.equals(0)) {
logger.info(`incentivizePullRequestReviews: Skipping to generate a permit url because the reward value is 0. user: ${user}`);
continue;
}
logger.info(`incentivizePullRequestReviews: Comment parsed for the user: ${user}. comments: ${JSON.stringify(commentsByNode)}, sum: ${rewardValue}`);
const account = await getWalletAddress(user);
const amountInETH = rewardValue.mul(baseMultiplier).div(1000);
wannacfuture marked this conversation as resolved.
Show resolved Hide resolved
if (amountInETH.gt(paymentPermitMaxPrice)) {
logger.info(`incentivizePullRequestReviews: Skipping comment reward for user ${user} because reward is higher than payment permit max price`);
continue;
}
if (account) {
const { payoutUrl } = await generatePermit2Signature(account, amountInETH, issue.node_id);
comment = `${comment}### [ **${user}: [ CLAIM ${amountInETH} ${tokenSymbol.toUpperCase()} ]** ](${payoutUrl})\n`;
wannacfuture marked this conversation as resolved.
Show resolved Hide resolved
reward[user] = payoutUrl;
} else {
fallbackReward[user] = amountInETH;
}
}

logger.info(`incentivizePullRequestReviews: Permit url generated for pull request reviewers. reward: ${JSON.stringify(reward)}`);
logger.info(`incentivizePullRequestReviews: Skipping to generate a permit url for missing accounts. fallback: ${JSON.stringify(fallbackReward)}`);

await addCommentToIssue(comment, issue.number);
};

export const incentivizeCreatorComment = async () => {
const logger = getLogger();
const {
Expand Down
4 changes: 2 additions & 2 deletions src/handlers/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { handleComment, issueClosedCallback, issueReopenedCallback } from "./com
import { checkPullRequests } from "./assign/auto";
import { createDevPoolPR } from "./pull-request";
import { runOnPush } from "./push";
import { incentivizeComments, incentivizeCreatorComment } from "./payout";
import { incentivizeComments, incentivizeCreatorComment, incentivizePullRequestReviews } from "./payout";

export const processors: Record<string, Handler> = {
[GithubEvent.ISSUES_OPENED]: {
Expand Down Expand Up @@ -53,7 +53,7 @@ export const processors: Record<string, Handler> = {
[GithubEvent.ISSUES_CLOSED]: {
pre: [nullHandler],
action: [issueClosedCallback],
post: [incentivizeCreatorComment, incentivizeComments],
post: [incentivizeCreatorComment, incentivizeComments, incentivizePullRequestReviews],
whilefoo marked this conversation as resolved.
Show resolved Hide resolved
},
[GithubEvent.PULL_REQUEST_OPENED]: {
pre: [nullHandler],
Expand Down
15 changes: 12 additions & 3 deletions src/helpers/issue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -507,13 +507,13 @@ export const closePullRequest = async (pull_number: number) => {
}
};

export const getAllPullRequestReviews = async (context: Context, pull_number: number) => {
export const getAllPullRequestReviews = async (context: Context, pull_number: number, format: "raw" | "html" | "text" | "full" = "raw") => {
const prArr = [];
let fetchDone = false;
const perPage = 30;
let curPage = 1;
while (!fetchDone) {
const prs = await getPullRequestReviews(context, pull_number, perPage, curPage);
const prs = await getPullRequestReviews(context, pull_number, perPage, curPage, format);

// push the objects to array
prArr.push(...prs);
Expand All @@ -524,7 +524,13 @@ export const getAllPullRequestReviews = async (context: Context, pull_number: nu
return prArr;
};

export const getPullRequestReviews = async (context: Context, pull_number: number, per_page: number, page: number) => {
export const getPullRequestReviews = async (
context: Context,
pull_number: number,
per_page: number,
page: number,
format: "raw" | "html" | "text" | "full" = "raw"
) => {
const logger = getLogger();
const payload = context.payload as Payload;
try {
Expand All @@ -534,6 +540,9 @@ export const getPullRequestReviews = async (context: Context, pull_number: numbe
pull_number,
per_page,
page,
mediaType: {
format,
},
});
return reviews;
} catch (e: unknown) {
Expand Down
12 changes: 6 additions & 6 deletions src/helpers/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,19 @@ export const gitIssueParser = async ({ owner, repo, issue_number }: GitParser):
}
};

export const gitLinkedIssueParser = async ({ owner, repo, issue_number }: GitParser): Promise<string> => {
export const gitLinkedIssueParser = async ({ owner, repo, issue_number: pull_number }: GitParser): Promise<string> => {
wannacfuture marked this conversation as resolved.
Show resolved Hide resolved
try {
const { data } = await axios.get(`https://github.com/${owner}/${repo}/pull/${issue_number}`);
const { data } = await axios.get(`https://github.com/${owner}/${repo}/pull/${pull_number}`);
const dom = parse(data);
const devForm = dom.querySelector("[data-target='create-branch.developmentForm']") as HTMLElement;
const linkedPRs = devForm.querySelectorAll(".my-1");
const linkedIssues = devForm.querySelectorAll(".my-1");

if (linkedPRs.length === 0) {
if (linkedIssues.length === 0) {
return "";
wannacfuture marked this conversation as resolved.
Show resolved Hide resolved
}

const prUrl = linkedPRs[0].querySelector("a")?.attrs?.href || "";
return prUrl;
const issueUrl = linkedIssues[0].querySelector("a")?.attrs?.href || "";
return issueUrl;
} catch (error) {
return "";
wannacfuture marked this conversation as resolved.
Show resolved Hide resolved
}
Expand Down