diff --git a/app/(hacktoberfest)/oss-issues/page.tsx b/app/(hacktoberfest)/oss-issues/page.tsx
index 03ec515..7e60390 100644
--- a/app/(hacktoberfest)/oss-issues/page.tsx
+++ b/app/(hacktoberfest)/oss-issues/page.tsx
@@ -1,19 +1,23 @@
-import { getAllOssGgIssuesOfRepos } from "@/lib/github/service";
+import { getOssIssuesForRepo } from "@/lib/github/service";
import { getAllRepositories } from "@/lib/repository/service";
import { TPullRequest } from "@/types/pullRequest";
import Link from "next/link";
export default async function IssuesPage() {
const ossGgRepositories = await getAllRepositories();
- const pullRequests: TPullRequest[] = await getAllOssGgIssuesOfRepos(
- ossGgRepositories.map((repo) => ({ id: repo.githubId, fullName: `${repo.owner}/${repo.name}` }))
+
+ const issuesPromises = ossGgRepositories.map((repo) =>
+ getOssIssuesForRepo(repo.githubId, `${repo.owner}/${repo.name}`)
);
+ const issuesArrays = await Promise.all(issuesPromises);
+ const issues: TPullRequest[] = issuesArrays.flat();
+
return (
-
available issues ({pullRequests.length})
+
available issues ({issues.length})
- {pullRequests.map((pullRequest) => (
+ {issues.map((pullRequest) => (
-
{pullRequest.repositoryFullName && {pullRequest.repositoryFullName}}
diff --git a/app/api/github-webhook/route.ts b/app/api/github-webhook/route.ts
new file mode 100644
index 0000000..3c826a1
--- /dev/null
+++ b/app/api/github-webhook/route.ts
@@ -0,0 +1,41 @@
+import { registerHooks } from "@/lib/github";
+import { EmitterWebhookEvent, EmitterWebhookEventName } from "@octokit/webhooks";
+import { headers } from "next/headers";
+import { NextResponse } from "next/server";
+
+// Set to store processed event IDs
+const processedEvents = new Set();
+
+export async function POST(req: Request) {
+ const headersList = headers();
+ const eventId = headersList.get("x-github-delivery") as string;
+ const githubEvent = headersList.get("x-github-event") as string;
+
+ let body: EmitterWebhookEvent<"issue_comment" | "pull_request" | "installation">["payload"];
+
+ try {
+ body = await req.json();
+ } catch (error) {
+ return NextResponse.json({ error: "Invalid JSON payload" }, { status: 400 });
+ }
+
+ if (!eventId) {
+ return NextResponse.json({ error: "Missing X-GitHub-Delivery header" }, { status: 400 });
+ }
+
+ if (processedEvents.has(eventId)) {
+ return NextResponse.json({ message: `Event ${eventId} already processed, skipping` }, { status: 200 });
+ }
+
+ await registerHooks(githubEvent as EmitterWebhookEventName, body);
+
+ processedEvents.add(eventId);
+ setTimeout(
+ () => {
+ processedEvents.delete(eventId);
+ },
+ 24 * 60 * 60 * 1000
+ ); // 24 hours
+
+ return NextResponse.json({ message: `Event ${eventId} processed` }, { status: 200 });
+}
diff --git a/lib/github/hooks/bounty.ts b/lib/github/hooks/bounty.ts
index 7a8f969..23cc591 100644
--- a/lib/github/hooks/bounty.ts
+++ b/lib/github/hooks/bounty.ts
@@ -15,233 +15,227 @@ import {
import { extractIssueNumbersFromPrBody, getOctokitInstance } from "@/lib/github/utils";
import { getRepositoryByGithubId } from "@/lib/repository/service";
import { createUser, getUser, getUserByGithubId } from "@/lib/user/service";
-import { Webhooks } from "@octokit/webhooks";
+import { EmitterWebhookEvent, Webhooks } from "@octokit/webhooks";
/**
* Handles the event when a bounty is created.
*
* @param webhooks - The Webhooks instance.
*/
-export const onBountyCreated = async (webhooks: Webhooks) => {
- webhooks.on(EVENT_TRIGGERS.ISSUE_COMMENTED, async (context) => {
- try {
- const octokit = getOctokitInstance(context.payload.installation?.id!);
- const repo = context.payload.repository.name;
- const issueCommentBody = context.payload.comment.body;
- const bountyCommentRegex = new RegExp(`${BOUNTY_IDENTIFIER}\\s+(\\d+)`);
- const bountyMatch = issueCommentBody.match(bountyCommentRegex);
- const isPR = Boolean(context.payload.issue.pull_request);
- const issueNumber = context.payload.issue.number;
- const owner = context.payload.repository.owner.login;
- const hasOssGgLabel = context.payload.issue.labels.some((label) => label.name === OSS_GG_LABEL);
+export const onBountyCreated = async (payload: EmitterWebhookEvent<"issue_comment.created">["payload"]) => {
+ try {
+ const octokit = getOctokitInstance(payload.installation?.id!);
+ const repo = payload.repository.name;
+ const issueCommentBody = payload.comment.body;
+ const bountyCommentRegex = new RegExp(`${BOUNTY_IDENTIFIER}\\s+(\\d+)`);
+ const bountyMatch = issueCommentBody.match(bountyCommentRegex);
+ const isPR = Boolean(payload.issue.pull_request);
+ const issueNumber = payload.issue.number;
+ const owner = payload.repository.owner.login;
+ const hasOssGgLabel = payload.issue.labels.some((label) => label.name === OSS_GG_LABEL);
+
+ const commentOnIssue = async (comment: string) => {
+ await octokit.issues.createComment({
+ body: comment,
+ issue_number: issueNumber,
+ repo,
+ owner,
+ });
+ };
+
+ if (bountyMatch) {
+ if (isPR) {
+ await commentOnIssue("Bounties can be setup in issues only, not in PRs.");
+ return;
+ }
- const commentOnIssue = async (comment: string) => {
- await octokit.issues.createComment({
- body: comment,
- issue_number: issueNumber,
- repo,
- owner,
- });
- };
+ if (!hasOssGgLabel) {
+ await commentOnIssue(`Bounties can be setup only in issues with the ${OSS_GG_LABEL} label.`);
+ return;
+ }
+
+ const newBountyAmount = parseInt(bountyMatch[1], 10);
+ const newBountyLabel = `${BOUNTY_EMOJI} ${newBountyAmount} ${USD_CURRENCY_CODE}`;
- if (bountyMatch) {
- if (isPR) {
- await commentOnIssue("Bounties can be setup in issues only, not in PRs.");
+ // Check if the repo is registered in oss.gg
+ const ossGgRepo = await getRepositoryByGithubId(payload.repository.id);
+ if (!ossGgRepo) {
+ await commentOnIssue(
+ "If you are the repo owner, please register at https://oss.gg to be able to create bounties."
+ );
+ return;
+ } else if (ossGgRepo) {
+ const bountySettings = await getBountySettingsByRepositoryId(ossGgRepo.id)();
+ if (bountySettings?.maxBounty && newBountyAmount > bountySettings.maxBounty) {
+ await commentOnIssue(
+ `Bounty amount exceeds the maximum bounty amount of ${bountySettings.maxBounty} ${USD_CURRENCY_CODE} set by the repo owner.`
+ );
return;
}
- if (!hasOssGgLabel) {
- await commentOnIssue(`Bounties can be setup only in issues with the ${OSS_GG_LABEL} label.`);
+ let usersThatCanCreateBounty = ossGgRepo?.installation.memberships.map((m) => m.userId);
+ if (!usersThatCanCreateBounty) {
+ await commentOnIssue("No admins for the given repo in oss.gg!");
return;
}
-
- const newBountyAmount = parseInt(bountyMatch[1], 10);
- const newBountyLabel = `${BOUNTY_EMOJI} ${newBountyAmount} ${USD_CURRENCY_CODE}`;
-
- // Check if the repo is registered in oss.gg
- const ossGgRepo = await getRepositoryByGithubId(context.payload.repository.id);
- if (!ossGgRepo) {
- await commentOnIssue(
- "If you are the repo owner, please register at https://oss.gg to be able to create bounties."
- );
+ const ossGgUsers = await Promise.all(
+ usersThatCanCreateBounty.map(async (userId) => {
+ const user = await getUser(userId);
+ return user?.githubId;
+ })
+ );
+ const isUserAllowedToCreateBounty = ossGgUsers?.includes(payload.comment.user.id);
+ if (!isUserAllowedToCreateBounty) {
+ await commentOnIssue("You are not allowed to create bounties! Please contact the repo admin.");
return;
- } else if (ossGgRepo) {
- const bountySettings = await getBountySettingsByRepositoryId(ossGgRepo.id)();
- if (bountySettings?.maxBounty && newBountyAmount > bountySettings.maxBounty) {
- await commentOnIssue(
- `Bounty amount exceeds the maximum bounty amount of ${bountySettings.maxBounty} ${USD_CURRENCY_CODE} set by the repo owner.`
- );
- return;
- }
-
- let usersThatCanCreateBounty = ossGgRepo?.installation.memberships.map((m) => m.userId);
- if (!usersThatCanCreateBounty) {
- await commentOnIssue("No admins for the given repo in oss.gg!");
- return;
- }
- const ossGgUsers = await Promise.all(
- usersThatCanCreateBounty.map(async (userId) => {
- const user = await getUser(userId);
- return user?.githubId;
- })
- );
- const isUserAllowedToCreateBounty = ossGgUsers?.includes(context.payload.comment.user.id);
- if (!isUserAllowedToCreateBounty) {
- await commentOnIssue("You are not allowed to create bounties! Please contact the repo admin.");
- return;
- }
+ }
- // Regex that matches the bounty label format like "đ¸ 50 USD"
- const previousBountyLabel = context.payload.issue?.labels?.find((label) =>
- label.name.match(BOUNTY_LABEL_REGEX)
- );
+ // Regex that matches the bounty label format like "đ¸ 50 USD"
+ const previousBountyLabel = payload.issue?.labels?.find((label) =>
+ label.name.match(BOUNTY_LABEL_REGEX)
+ );
- if (previousBountyLabel) {
- const previousBountyAmount = parseInt(previousBountyLabel.name.split(" ")[1], 10);
- if (previousBountyAmount === newBountyAmount) {
- return;
- } else {
- await octokit.issues.updateLabel({
- owner,
- repo,
- name: previousBountyLabel.name,
- new_name: newBountyLabel,
- color: "0E8A16",
- });
-
- await commentOnIssue(
- `The bounty amount was updated to ${newBountyAmount} ${USD_CURRENCY_CODE}`
- );
- }
+ if (previousBountyLabel) {
+ const previousBountyAmount = parseInt(previousBountyLabel.name.split(" ")[1], 10);
+ if (previousBountyAmount === newBountyAmount) {
+ return;
} else {
- await octokit.issues.addLabels({
+ await octokit.issues.updateLabel({
owner,
repo,
- issue_number: issueNumber,
- labels: [newBountyLabel],
+ name: previousBountyLabel.name,
+ new_name: newBountyLabel,
+ color: "0E8A16",
});
- await commentOnIssue(
- `A bounty of ${newBountyAmount} ${USD_CURRENCY_CODE} has been added to this issue. Happy hacking!`
- );
+
+ await commentOnIssue(`The bounty amount was updated to ${newBountyAmount} ${USD_CURRENCY_CODE}`);
}
+ } else {
+ await octokit.issues.addLabels({
+ owner,
+ repo,
+ issue_number: issueNumber,
+ labels: [newBountyLabel],
+ });
+ await commentOnIssue(
+ `A bounty of ${newBountyAmount} ${USD_CURRENCY_CODE} has been added to this issue. Happy hacking!`
+ );
}
}
- } catch (err) {
- console.error(err);
- throw new Error(err);
}
- });
+ } catch (err) {
+ console.error(err);
+ throw new Error(err);
+ }
};
/**
* Handles the event when a bounty pull request is merged.
* @param webhooks - The Webhooks instance.
*/
-export const onBountyPullRequestMerged = async (webhooks: Webhooks) => {
- webhooks.on(EVENT_TRIGGERS.PULL_REQUEST_CLOSED, async (context) => {
- try {
- const octokit = getOctokitInstance(context.payload.installation?.id!);
- const repo = context.payload.repository.name;
- const owner = context.payload.repository.owner.login;
- const isPrMerged = context.payload.pull_request.merged;
- const prNumber = context.payload.pull_request.number;
- const prBody = context.payload.pull_request.body ?? "";
- const prAuthorGithubId = context.payload.pull_request.user.id;
- const prAuthorUsername = context.payload.pull_request.user.login;
-
- if (!isPrMerged || !prBody) {
- return;
- }
+export const onBountyPullRequestMerged = async (
+ payload: EmitterWebhookEvent<"pull_request.closed">["payload"]
+) => {
+ try {
+ const octokit = getOctokitInstance(payload.installation?.id!);
+ const repo = payload.repository.name;
+ const owner = payload.repository.owner.login;
+ const isPrMerged = payload.pull_request.merged;
+ const prNumber = payload.pull_request.number;
+ const prBody = payload.pull_request.body ?? "";
+ const prAuthorGithubId = payload.pull_request.user.id;
+ const prAuthorUsername = payload.pull_request.user.login;
+
+ if (!isPrMerged || !prBody) {
+ return;
+ }
- const linkedIssueNumbers = extractIssueNumbersFromPrBody(prBody); // This assumes that a PR body can be linked to multiple issues
+ const linkedIssueNumbers = extractIssueNumbersFromPrBody(prBody); // This assumes that a PR body can be linked to multiple issues
- const awardBountyToUser = async (issueNumber: number) => {
- const commentOnIssue = async (comment: string) => {
- await octokit.issues.createComment({
- body: comment,
- issue_number: issueNumber,
- repo,
- owner,
- });
- };
-
- const issue = await octokit.issues.get({
- owner,
- repo,
+ const awardBountyToUser = async (issueNumber: number) => {
+ const commentOnIssue = async (comment: string) => {
+ await octokit.issues.createComment({
+ body: comment,
issue_number: issueNumber,
+ repo,
+ owner,
});
+ };
- const issueLabels = issue.data.labels.map((label) =>
- typeof label === "string" ? label : label?.name
- );
+ const issue = await octokit.issues.get({
+ owner,
+ repo,
+ issue_number: issueNumber,
+ });
- // Check if the issue has the required labels and is assigned to the pull request author
- const hasOssGgLabel = issueLabels?.some((label) => label === OSS_GG_LABEL);
- const bountyLabel = issueLabels?.find((label) => label?.match(BOUNTY_LABEL_REGEX));
- const isIssueAssignedToPrAuthor = issue.data.assignees?.some(
- (assignee) => assignee.id === prAuthorGithubId
- );
- const isIssueValid = hasOssGgLabel && bountyLabel && isIssueAssignedToPrAuthor;
+ const issueLabels = issue.data.labels.map((label) => (typeof label === "string" ? label : label?.name));
- // If the issue is not valid, return
- if (!isIssueValid) {
- return;
- } else {
- const bountyAmount = parseInt(bountyLabel.split(" ")[1], 10);
- const { data: prAuthorProfile } = await octokit.users.getByUsername({
- username: prAuthorUsername,
+ // Check if the issue has the required labels and is assigned to the pull request author
+ const hasOssGgLabel = issueLabels?.some((label) => label === OSS_GG_LABEL);
+ const bountyLabel = issueLabels?.find((label) => label?.match(BOUNTY_LABEL_REGEX));
+ const isIssueAssignedToPrAuthor = issue.data.assignees?.some(
+ (assignee) => assignee.id === prAuthorGithubId
+ );
+ const isIssueValid = hasOssGgLabel && bountyLabel && isIssueAssignedToPrAuthor;
+
+ // If the issue is not valid, return
+ if (!isIssueValid) {
+ return;
+ } else {
+ const bountyAmount = parseInt(bountyLabel.split(" ")[1], 10);
+ const { data: prAuthorProfile } = await octokit.users.getByUsername({
+ username: prAuthorUsername,
+ });
+ const ossGgRepo = await getRepositoryByGithubId(payload.repository.id);
+ let user = await getUserByGithubId(prAuthorGithubId);
+ if (!user) {
+ user = await createUser({
+ githubId: prAuthorGithubId,
+ login: prAuthorUsername,
+ email: prAuthorProfile.email,
+ name: prAuthorProfile.name,
+ avatarUrl: prAuthorProfile.avatar_url,
});
- const ossGgRepo = await getRepositoryByGithubId(context.payload.repository.id);
- let user = await getUserByGithubId(prAuthorGithubId);
- if (!user) {
- user = await createUser({
- githubId: prAuthorGithubId,
- login: prAuthorUsername,
- email: prAuthorProfile.email,
- name: prAuthorProfile.name,
- avatarUrl: prAuthorProfile.avatar_url,
- });
- }
+ }
- const bountyExists = await checkIfBountyExists(issue.data.html_url);
- if (bountyExists) {
- await commentOnIssue("Bounty already exists for this issue. Please check your inbox!");
- console.error(`Bounty already exists for issue: ${issue.data.html_url}`);
- return;
- }
+ const bountyExists = await checkIfBountyExists(issue.data.html_url);
+ if (bountyExists) {
+ await commentOnIssue("Bounty already exists for this issue. Please check your inbox!");
+ console.error(`Bounty already exists for issue: ${issue.data.html_url}`);
+ return;
+ }
- const bountyOrder = await dispatchBountyOrder({
- fundingSource: "BALANCE",
- amount: bountyAmount,
- currencyCode: USD_CURRENCY_CODE,
- deliveryMethod: "EMAIL",
- recipientName: user?.name!,
- recipientEmail: user?.email!,
- });
+ const bountyOrder = await dispatchBountyOrder({
+ fundingSource: "BALANCE",
+ amount: bountyAmount,
+ currencyCode: USD_CURRENCY_CODE,
+ deliveryMethod: "EMAIL",
+ recipientName: user?.name!,
+ recipientEmail: user?.email!,
+ });
- if (bountyOrder) {
- await storeBounty({
- usdAmount: bountyAmount,
- issueUrl: issue.data.html_url,
- status: "open",
- orderId: bountyOrder.order.id,
- rewardId: bountyOrder.order.rewards?.[0].id!,
- userId: user?.id,
- repositoryId: ossGgRepo?.id!,
- });
+ if (bountyOrder) {
+ await storeBounty({
+ usdAmount: bountyAmount,
+ issueUrl: issue.data.html_url,
+ status: "open",
+ orderId: bountyOrder.order.id,
+ rewardId: bountyOrder.order.rewards?.[0].id!,
+ userId: user?.id,
+ repositoryId: ossGgRepo?.id!,
+ });
- await commentOnIssue(
- `Thanks a lot for your valuable contribution! Pls check your inbox for all details to redeem your bounty of ${bountyAmount} ${USD_CURRENCY_CODE}!`
- );
- }
+ await commentOnIssue(
+ `Thanks a lot for your valuable contribution! Pls check your inbox for all details to redeem your bounty of ${bountyAmount} ${USD_CURRENCY_CODE}!`
+ );
}
- };
-
- // Award bounty to each linked issue number
- await Promise.all(linkedIssueNumbers.map(awardBountyToUser));
- } catch (err) {
- console.error(err);
- throw new Error(err);
- }
- });
+ }
+ };
+
+ // Award bounty to each linked issue number
+ await Promise.all(linkedIssueNumbers.map(awardBountyToUser));
+ } catch (err) {
+ console.error(err);
+ throw new Error(err);
+ }
};
diff --git a/lib/github/hooks/cache.ts b/lib/github/hooks/cache.ts
new file mode 100644
index 0000000..c196769
--- /dev/null
+++ b/lib/github/hooks/cache.ts
@@ -0,0 +1,18 @@
+import { revalidateTag } from "next/cache";
+
+interface RevalidateProps {
+ repositoryId?: number;
+}
+
+export const githubCache = {
+ tag: {
+ byRepositoryId(repositoryId: number) {
+ return `github-${repositoryId}`;
+ },
+ },
+ revalidate({ repositoryId }: RevalidateProps): void {
+ if (repositoryId) {
+ revalidateTag(this.tag.byRepositoryId(repositoryId));
+ }
+ },
+};
diff --git a/lib/github/hooks/installation.ts b/lib/github/hooks/installation.ts
index be5fb75..b0a131e 100644
--- a/lib/github/hooks/installation.ts
+++ b/lib/github/hooks/installation.ts
@@ -1,7 +1,7 @@
-import { EVENT_TRIGGERS } from "@/lib/constants";
-import { Webhooks } from "@octokit/webhooks";
+import { EmitterWebhookEvent } from "@octokit/webhooks";
import { sendInstallationDetails } from "../services/user";
+import { githubCache } from "./cache";
type GitHubRepository = {
id: number;
@@ -12,12 +12,12 @@ type GitHubRepository = {
private: boolean;
};
-export const onInstallationCreated = async (webhooks: Webhooks) => {
- webhooks.on(EVENT_TRIGGERS.INSTALLATION_CREATED, async (context) => {
- const installationId = context.payload.installation.id;
- const appId = context.payload.installation.app_id;
- const repos = context.payload.repositories as GitHubRepository[];
-
- await sendInstallationDetails(installationId, appId, repos, context.payload.installation);
+export const onInstallationCreated = async (payload: EmitterWebhookEvent<"installation">["payload"]) => {
+ const installationId = payload.installation.id;
+ const appId = payload.installation.app_id;
+ const repos = payload.repositories as GitHubRepository[];
+ repos.forEach((repo) => {
+ githubCache.revalidate({ repositoryId: repo.id });
});
+ await sendInstallationDetails(installationId, appId, repos, payload.installation);
};
diff --git a/lib/github/hooks/issue.ts b/lib/github/hooks/issue.ts
index a8f27b3..e35d06e 100644
--- a/lib/github/hooks/issue.ts
+++ b/lib/github/hooks/issue.ts
@@ -15,7 +15,7 @@ import {
import { getRepositoryByGithubId } from "@/lib/repository/service";
import { getUser } from "@/lib/user/service";
import { issueReminderTask } from "@/src/trigger/issueReminder";
-import { Webhooks } from "@octokit/webhooks";
+import { EmitterWebhookEvent, Webhooks } from "@octokit/webhooks";
import { isMemberOfRepository } from "../services/user";
import {
@@ -29,120 +29,115 @@ import {
processAndComment,
processUserPoints,
} from "../utils";
+import { githubCache } from "./cache";
+
+export const onIssueOpened = async (payload: EmitterWebhookEvent<"issues.opened">["payload"]) => {
+ //TODO:
+ //1. check if the issue has the oss label
+ //2. if it has the OSS label find all the users that are currently subscribed to the repo, have the right points/permission, then send them an email
+
+ // const isProjectRegistered = await getProject(projectId)
+ // if (!isProjectRegistered) {
+ // await octokit.issues.createComment(
+ // issue({
+ // body: ON_REPO_NOT_REGISTERED,
+ // })
+ // )
+ // return
+ // }
+ const labels = payload.issue.labels?.map((label) => label.name);
+ const isLevelLabel = labels?.includes(LEVEL_LABEL);
+
+ if (!isLevelLabel) {
+ return;
+ }
-export const onIssueOpened = async (webhooks: Webhooks) => {
- webhooks.on(EVENT_TRIGGERS.ISSUE_OPENED, async (context) => {
- const projectId = context.payload.repository.id;
- //TODO:
- //1. check if the issue has the oss label
- //2. if it has the OSS label find all the users that are currently subscribed to the repo, have the right points/permission, then send them an email
-
- // const isProjectRegistered = await getProject(projectId)
- // if (!isProjectRegistered) {
- // await context.octokit.issues.createComment(
- // context.issue({
- // body: ON_REPO_NOT_REGISTERED,
- // })
- // )
- // return
- // }
-
- const labels = context.payload.issue.labels?.map((label) => label.name);
- const isLevelLabel = labels?.includes(LEVEL_LABEL);
-
- if (!isLevelLabel) {
- return;
- }
-
- // await sendNewIssue(
- // context.payload.repository.id,
- // context.payload.issue.user.id,
- // context.payload.issue.id
- // )
-
- // await context.octokit.issues.createComment(
- // context.issue({
- // body: ON_NEW_ISSUE,
- // })
- // )
- });
+ // await sendNewIssue(
+ // payload.repository.id,
+ // payload.issue.user.id,
+ // payload.issue.id
+ // )
+
+ // await octokit.issues.createComment(
+ // issue({
+ // body: ON_NEW_ISSUE,
+ // })
+ // )
};
-export const onAssignCommented = async (webhooks: Webhooks) => {
- webhooks.on(EVENT_TRIGGERS.ISSUE_COMMENTED, async (context) => {
- try {
- const issueCommentBody = context.payload.comment.body;
- const [identifier, points] = issueCommentBody.split(" ");
- const issueNumber = context.payload.issue.number;
- const repo = context.payload.repository.name;
- const owner = context.payload.repository.owner.login;
- const commenter = context.payload.comment.user.login;
- const installationId = context.payload.installation?.id!;
- const octokit = getOctokitInstance(installationId);
- const isOssGgLabel = context.payload.issue.labels.some((label) => label.name === OSS_GG_LABEL);
-
- if (issueCommentBody.trim() === ASSIGN_IDENTIFIER) {
- if (!isOssGgLabel) return;
-
- const isAssigned = context.payload.issue.assignees.length > 0;
- if (isAssigned) {
- const assignee = context.payload.issue.assignees[0].login;
- const message =
- assignee === commenter
- ? `This issue is already assigned to you. Let's get this shipped!`
- : `This issue is already assigned to another person. Please find more issues [here](https://oss.gg).`;
- await octokit.issues.createComment({
- owner,
- repo,
- issue_number: issueNumber,
- body: message,
- });
- return;
- }
-
- //users who haven't linked the issue to the PR will be able to assign themselves again even if their pr was rejected, because their names won't be added to the "Attempted:user1" comment in the issue.
- const allCommentsInTheIssue = await octokit.issues.listComments({
+export const onAssignCommented = async (payload: EmitterWebhookEvent<"issue_comment">["payload"]) => {
+ try {
+ const issueCommentBody = payload.comment.body;
+ const [identifier, points] = issueCommentBody.split(" ");
+ const issueNumber = payload.issue.number;
+ const repo = payload.repository.name;
+ const owner = payload.repository.owner.login;
+ const commenter = payload.comment.user.login;
+ const installationId = payload.installation?.id!;
+ const octokit = getOctokitInstance(installationId);
+ const isOssGgLabel = payload.issue.labels.some((label) => label.name === OSS_GG_LABEL);
+
+ if (issueCommentBody.trim() === ASSIGN_IDENTIFIER) {
+ if (!isOssGgLabel) return;
+
+ const isAssigned = payload.issue.assignees.length > 0;
+ if (isAssigned) {
+ const assignee = payload.issue.assignees[0].login;
+ const message =
+ assignee === commenter
+ ? `This issue is already assigned to you. Let's get this shipped!`
+ : `This issue is already assigned to another person. Please find more issues [here](https://oss.gg).`;
+ await octokit.issues.createComment({
owner,
repo,
issue_number: issueNumber,
- per_page: 100,
+ body: message,
});
- let { extractedUserNames } =
- await extractUserNamesFromCommentsForRejectCommand(allCommentsInTheIssue);
+ return;
+ }
- const isUserPrRejectedBefore = extractedUserNames?.includes(context.payload.comment.user.login);
- if (isUserPrRejectedBefore) {
- await octokit.issues.createComment({
- owner,
- repo,
- issue_number: issueNumber,
- body: "You have already attempted this issue. We will open the issue up for a different contributor to work on. Feel free to stick around in the community and pick up a different issue.",
- });
- return;
- }
+ //users who haven't linked the issue to the PR will be able to assign themselves again even if their pr was rejected, because their names won't be added to the "Attempted:user1" comment in the issue.
+ const allCommentsInTheIssue = await octokit.issues.listComments({
+ owner,
+ repo,
+ issue_number: issueNumber,
+ per_page: 100,
+ });
+ let { extractedUserNames } = await extractUserNamesFromCommentsForRejectCommand(allCommentsInTheIssue);
- const { data: userIssues } = await octokit.issues.listForRepo({
+ const isUserPrRejectedBefore = extractedUserNames?.includes(payload.comment.user.login);
+ if (isUserPrRejectedBefore) {
+ await octokit.issues.createComment({
owner,
repo,
- assignee: commenter,
- state: "open",
+ issue_number: issueNumber,
+ body: "You have already attempted this issue. We will open the issue up for a different contributor to work on. Feel free to stick around in the community and pick up a different issue.",
});
+ return;
+ }
- if (userIssues.length > 0) {
- const assignedIssue = userIssues[0];
- await octokit.issues.createComment({
- owner,
- repo,
- issue_number: issueNumber,
- body: `You already have an open issue assigned to you [here](${assignedIssue.html_url}). Once that's closed or unassigned, only then we recommend you to take up more.`,
- });
- return;
- }
+ const { data: userIssues } = await octokit.issues.listForRepo({
+ owner,
+ repo,
+ assignee: commenter,
+ state: "open",
+ });
- /*
+ if (userIssues.length > 0) {
+ const assignedIssue = userIssues[0];
+ await octokit.issues.createComment({
+ owner,
+ repo,
+ issue_number: issueNumber,
+ body: `You already have an open issue assigned to you [here](${assignedIssue.html_url}). Once that's closed or unassigned, only then we recommend you to take up more.`,
+ });
+ return;
+ }
+
+ /*
//checking if the current level of user has the power to solve the issue on which the /assign comment was made.
- const currentRepo = await getRepositoryByGithubId(context.payload.repository.id);
- const user = await getUserByGithubId(context.payload.comment.user.id);
+ const currentRepo = await getRepositoryByGithubId(payload.repository.id);
+ const user = await getUserByGithubId(payload.comment.user.id);
if (currentRepo && user) {
const userTotalPoints = await getPointsForPlayerInRepoByRepositoryId(currentRepo.id, user.id);
@@ -154,7 +149,7 @@ export const onAssignCommented = async (webhooks: Webhooks) => {
const levels = currentRepo?.levels as TLevel[];
const modifiedTagsArray = calculateAssignabelNonAssignableIssuesForUserInALevel(levels); //gets all assignable tags be it from the current level and from lower levels.
- const labels = context.payload.issue.labels;
+ const labels = payload.issue.labels;
const tags = modifiedTagsArray.find((item) => item.levelId === currentLevelOfUser?.id); //finds the curent level in the modifiedTagsArray.
const isAssignable = labels.some((label) => {
@@ -172,282 +167,275 @@ export const onAssignCommented = async (webhooks: Webhooks) => {
}
} */
- await octokit.issues.addAssignees({
- owner,
- repo,
- issue_number: issueNumber,
- assignees: [commenter],
- });
+ await octokit.issues.addAssignees({
+ owner,
+ repo,
+ issue_number: issueNumber,
+ assignees: [commenter],
+ });
- //send trigger event to wait for 36hrs then send a reminder if the user has not created a pull request
- try {
- if (context.payload.installation?.id) {
- await issueReminderTask.trigger({
- issueNumber,
- repo,
- owner,
- commenter,
- installationId: context.payload.installation.id ?? "",
- });
- }
- } catch (error) {
- console.error("Error sending event:", error.message);
- if (error.response) {
- const responseText = await error.response.text(); // Capture response text
- console.error("Response:", responseText);
- }
+ //send trigger event to wait for 36hrs then send a reminder if the user has not created a pull request
+ try {
+ if (payload.installation?.id) {
+ await issueReminderTask.trigger({
+ issueNumber,
+ repo,
+ owner,
+ commenter,
+ installationId: payload.installation.id ?? "",
+ });
}
+ } catch (error) {
+ console.error("Error sending event:", error.message);
+ if (error.response) {
+ const responseText = await error.response.text(); // Capture response text
+ console.error("Response:", responseText);
+ }
+ }
+
+ await octokit.issues.createComment({
+ owner,
+ repo,
+ issue_number: issueNumber,
+ body: `Assigned to @${commenter}! Please open a draft PR linking this issue within 48h â ī¸ If we can't detect a PR from you linking this issue in 48h, you'll be unassigned automatically đšī¸ Excited to have you ship this đ`,
+ });
+ }
+ if (identifier === CREATE_IDENTIFIER) {
+ //check if the user is a member of the repository in our database
+ const isMember = await isMemberOfRepository(commenter, installationId);
+ if (!isMember) {
await octokit.issues.createComment({
owner,
repo,
issue_number: issueNumber,
- body: `Assigned to @${commenter}! Please open a draft PR linking this issue within 48h â ī¸ If we can't detect a PR from you linking this issue in 48h, you'll be unassigned automatically đšī¸ Excited to have you ship this đ`,
+ body: `@${commenter}, ${ON_USER_NOT_REGISTERED}`,
});
+ return;
}
-
- if (identifier === CREATE_IDENTIFIER) {
- //check if the user is a member of the repository in our database
- const isMember = await isMemberOfRepository(commenter, installationId);
- if (!isMember) {
+ if (isOssGgLabel) {
+ return;
+ } else {
+ if (isNaN(parseInt(points))) {
await octokit.issues.createComment({
owner,
repo,
issue_number: issueNumber,
- body: `@${commenter}, ${ON_USER_NOT_REGISTERED}`,
+ body: `@${commenter}, ${POINT_IS_NOT_A_NUMBER}`,
});
return;
}
- if (isOssGgLabel) {
- return;
- } else {
- if (isNaN(parseInt(points))) {
- await octokit.issues.createComment({
- owner,
- repo,
- issue_number: issueNumber,
- body: `@${commenter}, ${POINT_IS_NOT_A_NUMBER}`,
- });
- return;
- }
- await octokit.issues.addLabels({
- owner: owner,
- repo: repo,
- issue_number: issueNumber,
- labels: [OSS_GG_LABEL, `:joystick: ${points} points`],
- });
- await octokit.issues.createComment({
- owner: owner,
- repo: repo,
- issue_number: issueNumber,
- body: ON_NEW_ISSUE,
- });
- }
- }
- } catch (err) {
- console.error(err);
- }
- });
-};
-
-export const onUnassignCommented = async (webhooks: Webhooks) => {
- webhooks.on(EVENT_TRIGGERS.ISSUE_COMMENTED, async (context) => {
- try {
- const issueCommentBody = context.payload.comment.body;
- if (issueCommentBody.trim() !== UNASSIGN_IDENTIFIER) {
- return;
- }
-
- const isOssGgLabel = context.payload.issue.labels.some((label) => label.name === OSS_GG_LABEL);
- if (!isOssGgLabel) {
- return;
- }
-
- const issueNumber = context.payload.issue.number;
- const repo = context.payload.repository.name;
- const owner = context.payload.repository.owner.login;
- const commenter = context.payload.comment.user.login;
- const octokit = getOctokitInstance(context.payload.installation?.id!);
-
- const isAssigned = context.payload.issue.assignees.length > 0;
- if (!isAssigned) {
- await octokit.issues.createComment({
- owner,
- repo,
- issue_number: issueNumber,
- body: "This issue is not assigned to anyone.",
- });
- return;
- }
-
- const assignee = context.payload.issue.assignees[0].login;
- if (assignee === commenter) {
- await octokit.issues.removeAssignees({
- owner,
- repo,
+ await octokit.issues.addLabels({
+ owner: owner,
+ repo: repo,
issue_number: issueNumber,
- assignees: [assignee],
+ labels: [OSS_GG_LABEL, `:joystick: ${points} points`],
});
await octokit.issues.createComment({
- owner,
- repo,
+ owner: owner,
+ repo: repo,
issue_number: issueNumber,
- body: "Issue unassigned.",
+ body: ON_NEW_ISSUE,
});
- return;
}
+ }
+ } catch (err) {
+ console.error(err);
+ }
+};
- const ossGgRepo = await getRepositoryByGithubId(context.payload.repository.id);
- const usersThatCanUnassign = ossGgRepo?.installation.memberships.map((m) => m.userId) || [];
- const ossGgUsers = await Promise.all(
- usersThatCanUnassign.map(async (userId) => {
- const user = await getUser(userId);
- return user?.githubId;
- })
- );
+export const onUnassignCommented = async (payload: EmitterWebhookEvent<"issue_comment">["payload"]) => {
+ try {
+ const issueCommentBody = payload.comment.body;
+ if (issueCommentBody.trim() !== UNASSIGN_IDENTIFIER) {
+ return;
+ }
- const isUserAllowedToUnassign = ossGgUsers?.includes(context.payload.comment.user.id);
- if (!isUserAllowedToUnassign) {
- await octokit.issues.createComment({
- owner,
- repo,
- issue_number: issueNumber,
- body: "You cannot unassign this issue as it is not assigned to you.",
- });
- return;
- }
+ const isOssGgLabel = payload.issue.labels.some((label) => label.name === OSS_GG_LABEL);
+ if (!isOssGgLabel) {
+ return;
+ }
+ const issueNumber = payload.issue.number;
+ const repo = payload.repository.name;
+ const owner = payload.repository.owner.login;
+ const commenter = payload.comment.user.login;
+ const octokit = getOctokitInstance(payload.installation?.id!);
+
+ const isAssigned = payload.issue.assignees.length > 0;
+ if (!isAssigned) {
await octokit.issues.createComment({
owner,
repo,
issue_number: issueNumber,
- body: "Issue unassigned.",
+ body: "This issue is not assigned to anyone.",
});
+ return;
+ }
+ const assignee = payload.issue.assignees[0].login;
+ if (assignee === commenter) {
await octokit.issues.removeAssignees({
owner,
repo,
issue_number: issueNumber,
assignees: [assignee],
});
- } catch (err) {
- console.error(err);
+ await octokit.issues.createComment({
+ owner,
+ repo,
+ issue_number: issueNumber,
+ body: "Issue unassigned.",
+ });
+ return;
+ }
+
+ const ossGgRepo = await getRepositoryByGithubId(payload.repository.id);
+ const usersThatCanUnassign = ossGgRepo?.installation.memberships.map((m) => m.userId) || [];
+ const ossGgUsers = await Promise.all(
+ usersThatCanUnassign.map(async (userId) => {
+ const user = await getUser(userId);
+ return user?.githubId;
+ })
+ );
+
+ const isUserAllowedToUnassign = ossGgUsers?.includes(payload.comment.user.id);
+ if (!isUserAllowedToUnassign) {
+ await octokit.issues.createComment({
+ owner,
+ repo,
+ issue_number: issueNumber,
+ body: "You cannot unassign this issue as it is not assigned to you.",
+ });
+ return;
}
- });
+
+ await octokit.issues.createComment({
+ owner,
+ repo,
+ issue_number: issueNumber,
+ body: "Issue unassigned.",
+ });
+
+ await octokit.issues.removeAssignees({
+ owner,
+ repo,
+ issue_number: issueNumber,
+ assignees: [assignee],
+ });
+ } catch (err) {
+ console.error(err);
+ }
};
-export const onAwardPoints = async (webhooks: Webhooks) => {
- webhooks.on(EVENT_TRIGGERS.ISSUE_COMMENTED, async (context) => {
- try {
- const repo = context.payload.repository.name;
- const issueCommentBody = context.payload.comment.body;
- const awardPointsRegex = new RegExp(`${AWARD_POINTS_IDENTIFIER}\\s+(\\d+)`);
- const match = issueCommentBody.match(awardPointsRegex);
- const issueNumber = context.payload.issue.number;
- const owner = context.payload.repository.owner.login;
- let comment: string = "";
-
- if (match) {
- const points = parseInt(match[1], 10);
-
- const ossGgRepo = await getRepositoryByGithubId(context.payload.repository.id);
-
- let usersThatCanAwardPoints = ossGgRepo?.installation.memberships.map((m) => m.userId);
- if (!usersThatCanAwardPoints) {
- throw new Error("No admins for the given repo in oss.gg!");
- }
- const ossGgUsers = await Promise.all(
- usersThatCanAwardPoints.map(async (userId) => {
- const user = await getUser(userId);
- return user?.githubId;
- })
- );
- const isUserAllowedToAwardPoints = ossGgUsers?.includes(context.payload.comment.user.id);
- if (!isUserAllowedToAwardPoints) {
- comment = "You are not allowed to award points! Please contact an admin.";
+export const onAwardPoints = async (payload: EmitterWebhookEvent<"issue_comment">["payload"]) => {
+ try {
+ const repo = payload.repository.name;
+ const issueCommentBody = payload.comment.body;
+ const awardPointsRegex = new RegExp(`${AWARD_POINTS_IDENTIFIER}\\s+(\\d+)`);
+ const match = issueCommentBody.match(awardPointsRegex);
+ const issueNumber = payload.issue.number;
+ const owner = payload.repository.owner.login;
+ let comment: string = "";
+
+ if (match) {
+ const points = parseInt(match[1], 10);
+
+ const ossGgRepo = await getRepositoryByGithubId(payload.repository.id);
+
+ let usersThatCanAwardPoints = ossGgRepo?.installation.memberships.map((m) => m.userId);
+ if (!usersThatCanAwardPoints) {
+ throw new Error("No admins for the given repo in oss.gg!");
+ }
+ const ossGgUsers = await Promise.all(
+ usersThatCanAwardPoints.map(async (userId) => {
+ const user = await getUser(userId);
+ return user?.githubId;
+ })
+ );
+ const isUserAllowedToAwardPoints = ossGgUsers?.includes(payload.comment.user.id);
+ if (!isUserAllowedToAwardPoints) {
+ comment = "You are not allowed to award points! Please contact an admin.";
+ } else {
+ if (!ossGgRepo) {
+ comment = "If you are the repo owner, please register at oss.gg to be able to award points";
} else {
- if (!ossGgRepo) {
- comment = "If you are the repo owner, please register at oss.gg to be able to award points";
- } else {
- const authorUsername = context.payload.issue.user.login;
-
- //process user points
- let user = await processUserPoints({
- installationId: context.payload.installation?.id!,
- prAuthorGithubId: context.payload.issue.user.id,
- prAuthorUsername: authorUsername,
- avatarUrl: context.payload.issue.user.avatar_url,
- points,
- url: context.payload.comment.html_url,
- repoId: ossGgRepo?.id,
- comment,
- });
+ const authorUsername = payload.issue.user.login;
+
+ //process user points
+ let user = await processUserPoints({
+ installationId: payload.installation?.id!,
+ prAuthorGithubId: payload.issue.user.id,
+ prAuthorUsername: authorUsername,
+ avatarUrl: payload.issue.user.avatar_url,
+ points,
+ url: payload.comment.html_url,
+ repoId: ossGgRepo?.id,
+ comment,
+ });
- comment =
- `Awarding ${user.login}: ${points} points đšī¸ Well done! Check out your new contribution on [oss.gg/${user.login}](https://oss.gg/${user.login})` +
- " " +
- comment;
- }
+ comment =
+ `Awarding ${user.login}: ${points} points đšī¸ Well done! Check out your new contribution on [oss.gg/${user.login}](https://oss.gg/${user.login})` +
+ " " +
+ comment;
}
+ }
- // Add logging before posting comment
- console.log("Attempting to post comment:", {
- installationId: context.payload.installation?.id,
+ // Add logging before posting comment
+ console.log("Attempting to post comment:", {
+ installationId: payload.installation?.id,
+ body: comment,
+ issueNumber: issueNumber,
+ repo,
+ owner,
+ });
+
+ // Wrap postComment in a try-catch block
+ try {
+ await postComment({
+ installationId: payload.installation?.id!,
body: comment,
issueNumber: issueNumber,
repo,
owner,
});
-
- // Wrap postComment in a try-catch block
- try {
- await postComment({
- installationId: context.payload.installation?.id!,
- body: comment,
- issueNumber: issueNumber,
- repo,
- owner,
- });
- console.log("Comment posted successfully");
- } catch (postCommentError) {
- console.error("Error posting comment:", postCommentError);
- // Optionally, you can rethrow the error if you want it to be caught by the outer catch block
- // throw postCommentError;
- }
+ console.log("Comment posted successfully");
+ } catch (postCommentError) {
+ console.error("Error posting comment:", postCommentError);
+ // Optionally, you can rethrow the error if you want it to be caught by the outer catch block
+ // throw postCommentError;
}
- } catch (err) {
- console.error("Error in onAwardPoints:", err);
- throw new Error(err);
}
- });
+ } catch (err) {
+ console.error("Error in onAwardPoints:", err);
+ throw new Error(err);
+ }
};
-export const onPullRequestMerged = async (webhooks: Webhooks) => {
- webhooks.on(EVENT_TRIGGERS.PULL_REQUEST_CLOSED, async (context) => {
- const { pull_request: pullRequest, repository, installation } = context.payload;
+export const onPullRequestMerged = async (payload: EmitterWebhookEvent<"pull_request">["payload"]) => {
+ const { pull_request: pullRequest, repository, installation } = payload;
- if (!pullRequest.merged) {
- console.log("Pull request was not merged.");
- return;
- }
+ if (!pullRequest.merged) {
+ console.log("Pull request was not merged.");
+ return;
+ }
- const {
- name: repo,
- owner: { login: owner },
- } = repository;
- const octokit = getOctokitInstance(installation?.id!);
+ const {
+ name: repo,
+ owner: { login: owner },
+ } = repository;
+ const octokit = getOctokitInstance(installation?.id!);
- const ossGgRepo = await getRepositoryByGithubId(repository.id);
- if (!ossGgRepo) {
- console.log("Repository is not enrolled in oss.gg.");
- return;
- }
+ const ossGgRepo = await getRepositoryByGithubId(repository.id);
+ if (!ossGgRepo) {
+ console.log("Repository is not enrolled in oss.gg.");
+ return;
+ }
- await processPullRequest(context, octokit, pullRequest, repo, owner, ossGgRepo.id);
- });
+ await processPullRequest(payload, octokit, pullRequest, repo, owner, ossGgRepo.id);
};
-async function processPullRequest(context, octokit, pullRequest, repo, owner, ossGgRepoId) {
+async function processPullRequest(payload, octokit, pullRequest, repo, owner, ossGgRepoId) {
const validPrLabels = filterValidLabels(pullRequest.labels);
const isPrOssGgLabel = checkOssGgLabel(validPrLabels);
@@ -455,7 +443,7 @@ async function processPullRequest(context, octokit, pullRequest, repo, owner, os
const points = extractPointsFromLabels(validPrLabels);
if (points) {
await processAndComment({
- context,
+ payload,
pullRequest,
repo,
owner,
@@ -468,7 +456,7 @@ async function processPullRequest(context, octokit, pullRequest, repo, owner, os
}
console.log(`Pull request #${pullRequest.number} does not have the đšī¸ oss.gg label.`);
- await processLinkedIssues(context, octokit, pullRequest, repo, owner, ossGgRepoId);
+ await processLinkedIssues(payload, octokit, pullRequest, repo, owner, ossGgRepoId);
}
async function processLinkedIssues(context, octokit, pullRequest, repo, owner, ossGgRepoId) {
@@ -483,7 +471,7 @@ async function processLinkedIssues(context, octokit, pullRequest, repo, owner, o
}
}
-async function processIssue(context, octokit, pullRequest, repo, owner, issueNumber, ossGgRepoId) {
+async function processIssue(payload, octokit, pullRequest, repo, owner, issueNumber, ossGgRepoId) {
const { data: issue } = await octokit.issues.get({ owner, repo, issue_number: issueNumber });
const validLabels = filterValidLabels(issue.labels);
@@ -496,7 +484,7 @@ async function processIssue(context, octokit, pullRequest, repo, owner, issueNum
if (points) {
console.log(`Points for issue #${issueNumber}:`, points);
await processAndComment({
- context,
+ payload,
pullRequest,
repo,
owner,
@@ -509,151 +497,149 @@ async function processIssue(context, octokit, pullRequest, repo, owner, issueNum
}
}
-export const onRejectCommented = async (webhooks: Webhooks) => {
- webhooks.on(EVENT_TRIGGERS.ISSUE_COMMENTED, async (context) => {
- try {
- const issueCommentBody = context.payload.comment.body;
- const prNumber = context.payload.issue.number; //this is pr number if comment made from pr,else issue number when made from issue.
- const repo = context.payload.repository.name;
- const owner = context.payload.repository.owner.login;
- const octokit = getOctokitInstance(context.payload.installation?.id!);
- const rejectRegex = new RegExp(`${REJECT_IDENTIFIER}\\s+(.*)`, "i");
- const match = issueCommentBody.match(rejectRegex);
- const isCommentOnPullRequest = context.payload.issue.pull_request;
- let comment: string = "";
-
- if (!match) {
- return;
- }
+export const onRejectCommented = async (payload: EmitterWebhookEvent<"issue_comment">["payload"]) => {
+ try {
+ const issueCommentBody = payload.comment.body;
+ const prNumber = payload.issue.number; //this is pr number if comment made from pr,else issue number when made from issue.
+ const repo = payload.repository.name;
+ const owner = payload.repository.owner.login;
+ const octokit = getOctokitInstance(payload.installation?.id!);
+ const rejectRegex = new RegExp(`${REJECT_IDENTIFIER}\\s+(.*)`, "i");
+ const match = issueCommentBody.match(rejectRegex);
+ const isCommentOnPullRequest = payload.issue.pull_request;
+ let comment: string = "";
+
+ if (!match) {
+ return;
+ }
- if (!isCommentOnPullRequest) {
- await octokit.issues.createComment({
- owner,
- repo,
- issue_number: prNumber,
- body: `The command ${REJECT_IDENTIFIER} only works in PRs, not on issues. Please use it in a Pull Request.`,
- });
- return;
- }
+ if (!isCommentOnPullRequest) {
+ await octokit.issues.createComment({
+ owner,
+ repo,
+ issue_number: prNumber,
+ body: `The command ${REJECT_IDENTIFIER} only works in PRs, not on issues. Please use it in a Pull Request.`,
+ });
+ return;
+ }
- const message = match[1];
- const ossGgRepo = await getRepositoryByGithubId(context.payload.repository.id);
+ const message = match[1];
+ const ossGgRepo = await getRepositoryByGithubId(payload.repository.id);
- let usersThatCanRejectPr = ossGgRepo?.installation.memberships.map((m) => m.userId);
- if (!usersThatCanRejectPr) {
- throw new Error("No admins for the given repo in oss.gg!");
- }
- const ossGgUsers = await Promise.all(
- usersThatCanRejectPr.map(async (userId) => {
- const user = await getUser(userId);
- return user?.githubId;
- })
- );
- const isUserAllowedToRejectPr = ossGgUsers?.includes(context.payload.comment.user.id);
- if (!isUserAllowedToRejectPr) {
- comment = "You are not allowed to reject a pull request.";
+ let usersThatCanRejectPr = ossGgRepo?.installation.memberships.map((m) => m.userId);
+ if (!usersThatCanRejectPr) {
+ throw new Error("No admins for the given repo in oss.gg!");
+ }
+ const ossGgUsers = await Promise.all(
+ usersThatCanRejectPr.map(async (userId) => {
+ const user = await getUser(userId);
+ return user?.githubId;
+ })
+ );
+ const isUserAllowedToRejectPr = ossGgUsers?.includes(payload.comment.user.id);
+ if (!isUserAllowedToRejectPr) {
+ comment = "You are not allowed to reject a pull request.";
+ await octokit.issues.createComment({
+ owner,
+ repo,
+ issue_number: prNumber,
+ body: comment,
+ });
+ return;
+ } else {
+ const extractIssueNumbersFromPrBody = extractIssueNumbers(payload.issue.body || "");
+ const prAuthor = payload.issue.user.login;
+ const rejectionMessage = REJECTION_MESSAGE_TEMPLATE(prAuthor, message);
+
+ await octokit.issues.createComment({
+ owner,
+ repo,
+ issue_number: prNumber,
+ body: rejectionMessage,
+ });
+
+ await octokit.pulls.update({
+ owner,
+ repo,
+ pull_number: prNumber,
+ state: "closed",
+ });
+
+ if (extractIssueNumbersFromPrBody.length === 0) {
await octokit.issues.createComment({
owner,
repo,
issue_number: prNumber,
- body: comment,
+ body: "This PR is not linked to an issue. Please update the issue status manually.",
});
return;
} else {
- const extractIssueNumbersFromPrBody = extractIssueNumbers(context.payload.issue.body || "");
- const prAuthor = context.payload.issue.user.login;
- const rejectionMessage = REJECTION_MESSAGE_TEMPLATE(prAuthor, message);
-
- await octokit.issues.createComment({
- owner,
- repo,
- issue_number: prNumber,
- body: rejectionMessage,
- });
-
- await octokit.pulls.update({
- owner,
- repo,
- pull_number: prNumber,
- state: "closed",
- });
+ extractIssueNumbersFromPrBody.forEach(async (issueNumber: number) => {
+ //assumption: taking only first 100 comments because first rejection will happen in first 100 comments.If comments are more than 100 then such heavy discussed issue mostly would be given to a core team member.Even if it is given to a non core team member, our requirements would fulfill within 100 comments.
+ const allCommentsInTheIssue = await octokit.issues.listComments({
+ owner,
+ repo,
+ issue_number: issueNumber,
+ per_page: 100,
+ });
- if (extractIssueNumbersFromPrBody.length === 0) {
- await octokit.issues.createComment({
+ const issue = await octokit.issues.get({
owner,
repo,
- issue_number: prNumber,
- body: "This PR is not linked to an issue. Please update the issue status manually.",
+ issue_number: issueNumber,
});
- return;
- } else {
- extractIssueNumbersFromPrBody.forEach(async (issueNumber: number) => {
- //assumption: taking only first 100 comments because first rejection will happen in first 100 comments.If comments are more than 100 then such heavy discussed issue mostly would be given to a core team member.Even if it is given to a non core team member, our requirements would fulfill within 100 comments.
- const allCommentsInTheIssue = await octokit.issues.listComments({
- owner,
- repo,
- issue_number: issueNumber,
- per_page: 100,
- });
- const issue = await octokit.issues.get({
+ const issueAssignee = issue.data.assignees ? issue.data.assignees[0]?.login : "";
+
+ if (issueAssignee !== prAuthor) {
+ return;
+ }
+
+ const { hasCommentWithAttemptedUserNames } =
+ checkFirstOccurenceForAttemptedComment(allCommentsInTheIssue);
+
+ if (!hasCommentWithAttemptedUserNames) {
+ await octokit.issues.createComment({
owner,
repo,
issue_number: issueNumber,
+ body: `Attempted:${issueAssignee}`,
});
+ } else {
+ const { extractedUserNames, commentId } =
+ await extractUserNamesFromCommentsForRejectCommand(allCommentsInTheIssue);
- const issueAssignee = issue.data.assignees ? issue.data.assignees[0]?.login : "";
-
- if (issueAssignee !== prAuthor) {
- return;
- }
-
- const { hasCommentWithAttemptedUserNames } =
- checkFirstOccurenceForAttemptedComment(allCommentsInTheIssue);
+ extractedUserNames.push(issueAssignee);
- if (!hasCommentWithAttemptedUserNames) {
- await octokit.issues.createComment({
+ commentId &&
+ (await octokit.issues.updateComment({
owner,
repo,
issue_number: issueNumber,
- body: `Attempted:${issueAssignee}`,
- });
- } else {
- const { extractedUserNames, commentId } =
- await extractUserNamesFromCommentsForRejectCommand(allCommentsInTheIssue);
-
- extractedUserNames.push(issueAssignee);
-
- commentId &&
- (await octokit.issues.updateComment({
- owner,
- repo,
- issue_number: issueNumber,
- comment_id: commentId,
- body: `Attempted:${extractedUserNames}`,
- }));
- }
+ comment_id: commentId,
+ body: `Attempted:${extractedUserNames}`,
+ }));
+ }
- await octokit.issues.createComment({
- owner,
- repo,
- issue_number: issueNumber,
- body: "The issue is up for grabs again! Feel free to assign yourself using /assign.",
- });
+ await octokit.issues.createComment({
+ owner,
+ repo,
+ issue_number: issueNumber,
+ body: "The issue is up for grabs again! Feel free to assign yourself using /assign.",
+ });
- await octokit.issues.removeAssignees({
- owner,
- repo,
- issue_number: issueNumber,
- assignees: [issueAssignee],
- });
+ await octokit.issues.removeAssignees({
+ owner,
+ repo,
+ issue_number: issueNumber,
+ assignees: [issueAssignee],
});
- }
+ });
}
- } catch (err) {
- console.error(err);
}
- });
+ } catch (err) {
+ console.error(err);
+ }
};
const extractUserNamesFromCommentsForRejectCommand = async (allCommentsInTheIssue) => {
const { indexCommentWithAttemptedUserNames, hasCommentWithAttemptedUserNames } =
diff --git a/lib/github/index.ts b/lib/github/index.ts
index 7ae5e60..fac3ed7 100644
--- a/lib/github/index.ts
+++ b/lib/github/index.ts
@@ -1,7 +1,14 @@
import { onBountyCreated, onBountyPullRequestMerged } from "@/lib/github/hooks/bounty";
-import { Webhooks, createNodeMiddleware } from "@octokit/webhooks";
+import { EmitterWebhookEvent, EmitterWebhookEventName } from "@octokit/webhooks";
-import { GITHUB_APP_WEBHOOK_SECRET } from "../constants";
+import {
+ ASSIGN_IDENTIFIER,
+ AWARD_POINTS_IDENTIFIER,
+ BOUNTY_IDENTIFIER,
+ REJECT_IDENTIFIER,
+ UNASSIGN_IDENTIFIER,
+} from "../constants";
+import { githubCache } from "./hooks/cache";
import { onInstallationCreated } from "./hooks/installation";
import {
onAssignCommented,
@@ -12,22 +19,55 @@ import {
onUnassignCommented,
} from "./hooks/issue";
-const webhooks = new Webhooks({
- secret: GITHUB_APP_WEBHOOK_SECRET,
-});
+export const registerHooks = async (
+ event: EmitterWebhookEventName,
+ body: EmitterWebhookEvent<"issue_comment.created" | "pull_request" | "installation" | "issues">["payload"]
+) => {
+ switch (event) {
+ case "issues": {
+ githubCache.revalidate({
+ repositoryId: (body as EmitterWebhookEvent<"issues.opened">["payload"]).repository.id,
+ });
+ if (body.action === "opened") {
+ await onIssueOpened(body as EmitterWebhookEvent<"issues.opened">["payload"]);
+ }
+ }
+
+ case "issue_comment": {
+ if (body.action === "created") {
+ const payload = body as EmitterWebhookEvent<"issue_comment.created">["payload"];
+ const commentBody = payload.comment.body;
+ const handlers = {
+ [ASSIGN_IDENTIFIER]: onAssignCommented,
+ [UNASSIGN_IDENTIFIER]: onUnassignCommented,
+ [AWARD_POINTS_IDENTIFIER]: onAwardPoints,
+ [REJECT_IDENTIFIER]: onRejectCommented,
+ [BOUNTY_IDENTIFIER]: onBountyCreated,
+ };
+
+ for (const [identifier, handler] of Object.entries(handlers)) {
+ if (commentBody.startsWith(identifier)) {
+ await handler(payload);
+ break;
+ }
+ }
+ }
+ break;
+ }
-export const webhookMiddleware = createNodeMiddleware(webhooks, {
- path: "/api/github-webhook",
-});
+ case "installation": {
+ if (body.action === "created") {
+ await onInstallationCreated(body as EmitterWebhookEvent<"installation">["payload"]);
+ }
+ }
+ case "pull_request": {
+ if (body.action === "closed") {
+ await onPullRequestMerged(body as EmitterWebhookEvent<"pull_request">["payload"]);
+ }
-export const registerHooks = async () => {
- onIssueOpened(webhooks);
- onInstallationCreated(webhooks);
- onAssignCommented(webhooks);
- onUnassignCommented(webhooks);
- onAwardPoints(webhooks);
- onRejectCommented(webhooks);
- onBountyCreated(webhooks);
- onBountyPullRequestMerged(webhooks);
- onPullRequestMerged(webhooks);
+ if (body.action === "closed") {
+ await onBountyPullRequestMerged(body as EmitterWebhookEvent<"pull_request.closed">["payload"]);
+ }
+ }
+ }
};
diff --git a/lib/github/service.ts b/lib/github/service.ts
index 53c0ba7..5a12f5d 100644
--- a/lib/github/service.ts
+++ b/lib/github/service.ts
@@ -6,6 +6,7 @@ import { Octokit } from "@octokit/rest";
import { unstable_cache } from "next/cache";
import { GITHUB_APP_ACCESS_TOKEN, OSS_GG_LABEL } from "../constants";
+import { githubCache } from "./hooks/cache";
import { extractPointsFromLabels } from "./utils";
type PullRequestStatus = "open" | "merged" | "closed" | undefined;
@@ -85,19 +86,15 @@ export const getPullRequestsByGithubLogin = unstable_cache(
{ revalidate: 60 }
);
-const fetchAllOssGgIssuesOfRepos = async (
- repos: { id: number; fullName: string }[]
-): Promise => {
- const githubHeaders = {
- Authorization: `Bearer ${GITHUB_APP_ACCESS_TOKEN}`,
- Accept: "application/vnd.github.v3+json",
- };
-
- console.log(`Fetching issues for ${repos.length} repositories`);
-
- const allIssues = await Promise.all(
- repos.map(async (repo) => {
- const issuesUrl = `https://api.github.com/search/issues?q=repo:${repo.fullName}+is:issue+is:open+label:"${OSS_GG_LABEL}"+no:assignee&sort=created&order=desc`;
+export const getOssIssuesForRepo = (repositoryId: number, fullName: string): Promise =>
+ unstable_cache(
+ async () => {
+ console.log(`Fetching issues for repo: ${fullName}`);
+ const githubHeaders = {
+ Authorization: `Bearer ${GITHUB_APP_ACCESS_TOKEN}`,
+ Accept: "application/vnd.github.v3+json",
+ };
+ const issuesUrl = `https://api.github.com/search/issues?q=repo:${fullName}+is:issue+is:open+label:"${OSS_GG_LABEL}"+no:assignee&sort=created&order=desc`;
console.log(`Fetching issues from: ${issuesUrl}`);
const issuesResponse = await fetch(issuesUrl, { headers: githubHeaders });
@@ -111,10 +108,7 @@ const fetchAllOssGgIssuesOfRepos = async (
);
const issuesData = await issuesResponse.json();
- console.log(`Fetched ${issuesData.total_count} issues for ${repo.fullName}`);
-
const validatedData = ZGithubApiResponseSchema.parse(issuesData);
- console.log(`Validated ${validatedData.items.length} issues`);
return validatedData.items.map((issue) => {
console.log(`Processing issue: ${issue.title}`);
@@ -122,7 +116,7 @@ const fetchAllOssGgIssuesOfRepos = async (
title: issue.title,
href: issue.html_url,
author: issue.user.login,
- repositoryFullName: repo.fullName,
+ repositoryFullName: fullName,
dateOpened: issue.created_at,
dateMerged: null,
dateClosed: issue.closed_at,
@@ -130,26 +124,7 @@ const fetchAllOssGgIssuesOfRepos = async (
points: extractPointsFromLabels(issue.labels),
});
});
- })
- );
-
- const flattenedIssues = allIssues.flat();
- console.log(`Total issues fetched and processed: ${flattenedIssues.length}`);
-
- return flattenedIssues;
-};
-
-export const getAllOssGgIssuesOfRepos = (repos: { id: number; fullName: string }[]) =>
- unstable_cache(
- async () => {
- console.log(`Cache MISS for getAllOssGgIssuesOfRepos`);
- return await fetchAllOssGgIssuesOfRepos(repos);
},
- [
- `getAllOssGgIssuesOfRepos-${repos
- .map((r) => r.id)
- .sort((a, b) => a - b)
- .join("-")}`,
- ],
- { revalidate: 120 }
+ [`getOssIssuesForRepo-${repositoryId}-${fullName}`],
+ { tags: [githubCache.tag.byRepositoryId(repositoryId)] }
)();
diff --git a/lib/github/utils.ts b/lib/github/utils.ts
index a189994..5d1ca1b 100644
--- a/lib/github/utils.ts
+++ b/lib/github/utils.ts
@@ -78,7 +78,7 @@ export const extractPointsFromLabels = (labels: { name: string }[]): number | nu
// Helper to process user points and post a comment
export const processAndComment = async ({
- context,
+ payload,
pullRequest,
repo,
owner,
@@ -86,7 +86,7 @@ export const processAndComment = async ({
issueNumber,
ossGgRepoId,
}: {
- context: any;
+ payload: any;
pullRequest: any;
repo: string;
owner: string;
@@ -95,7 +95,7 @@ export const processAndComment = async ({
ossGgRepoId: string;
}) => {
const user = await processUserPoints({
- installationId: context.payload.installation?.id!,
+ installationId: payload.installation?.id!,
prAuthorGithubId: pullRequest.user.id,
prAuthorUsername: pullRequest.user.login,
avatarUrl: pullRequest.user.avatar_url,
@@ -109,7 +109,7 @@ export const processAndComment = async ({
// Post comment on the issue or pull request
postComment({
- installationId: context.payload.installation?.id!,
+ installationId: payload.installation?.id!,
body: comment,
issueNumber: issueNumber,
repo,
diff --git a/pages/api/github-webhook.ts b/pages/api/github-webhook.ts
deleted file mode 100644
index fca9c2e..0000000
--- a/pages/api/github-webhook.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import { registerHooks, webhookMiddleware } from "@/lib/github";
-import { NextApiRequest, NextApiResponse } from "next";
-
-export const config = {
- api: {
- bodyParser: false,
- },
-};
-
-// Set to store processed event IDs
-const processedEvents = new Set();
-
-// Flag to ensure hooks are registered only once
-let hooksRegistered = false;
-
-export default async function handler(req: NextApiRequest, res: NextApiResponse) {
- if (req.method === "POST") {
- const eventId = req.headers["x-github-delivery"] as string;
-
- if (!eventId) {
- res.status(400).json({ error: "Missing X-GitHub-Delivery header" });
- return;
- }
-
- if (processedEvents.has(eventId)) {
- res.status(200).end();
- return;
- }
-
- if (!hooksRegistered) {
- registerHooks();
- hooksRegistered = true;
- }
-
- webhookMiddleware(req, res, () => {
- processedEvents.add(eventId);
-
- // Optionally, remove the event ID after some time to prevent the set from growing indefinitely
- setTimeout(
- () => {
- processedEvents.delete(eventId);
- },
- 24 * 60 * 60 * 1000
- ); // 24 hours
-
- res.status(200).end();
- });
- } else {
- res.setHeader("Allow", "POST");
- res.status(405).end("Method Not Allowed");
- }
-}
diff --git a/types/issue.ts b/types/issue.ts
index ff3e9ef..d770401 100644
--- a/types/issue.ts
+++ b/types/issue.ts
@@ -14,7 +14,7 @@ export const ZGithubLabelSchema = z.object({
description: z.string().nullable(),
});
-export const ZGithubPrIssueSchema = z.object({
+export const ZGithubIssue = z.object({
html_url: z.string(),
title: z.string(),
user: ZGithubUserSchema,
@@ -29,7 +29,7 @@ export const ZGithubPrIssueSchema = z.object({
});
export const ZGithubApiResponseSchema = z.object({
- items: z.array(ZGithubPrIssueSchema),
+ items: z.array(ZGithubIssue),
});
export const ZMappedDataSchema = z.object({
@@ -49,3 +49,4 @@ export const ZMappedDataSchema = z.object({
export type TGithubApiResponse = z.infer;
export type TMappedData = z.infer;
+export type TGithubIssue = z.infer;