Skip to content
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: Issue reminder job #19

Merged
merged 9 commits into from
Apr 1, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
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
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,7 @@ GITHUB_APP_PRIVATE_KEY=""

# GitHub App Name in your App's Installation URL (https://github.com/apps/${GITHUB_APP_SLUG}/installations/new)
GITHUB_APP_SLUG=

#Trigger API credential URL(https://trigger.dev/docs/documentation/introduction)
TRIGGER_API_KEY=
TRIGGER_API_URL=
10 changes: 10 additions & 0 deletions app/api/trigger/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createAppRoute } from "@trigger.dev/nextjs";
import { triggerDotDevClient } from "@/trigger";

import "@/jobs";

//this route is used to send and receive data with Trigger.dev
export const { POST, dynamic } = createAppRoute(triggerDotDevClient);

//uncomment this to set a higher max duration (it must be inside your plan limits). Full docs: https://vercel.com/docs/functions/serverless-functions/runtimes#max-duration
//export const maxDuration = 60;
4 changes: 4 additions & 0 deletions env.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export const env = createEnv({
GITHUB_APP_PRIVATE_KEY: z.string().min(1).optional(),
GITHUB_APP_SLUG: z.string().min(1),
GITHUB_APP_ACCESS_TOKEN: z.string().min(1),
TRIGGER_API_KEY: z.string().min(1),
TRIGGER_API_URL: z.string().min(1),
},
client: {
NEXT_PUBLIC_APP_URL: z.string().min(1),
Expand All @@ -43,5 +45,7 @@ export const env = createEnv({
GITHUB_APP_PRIVATE_KEY: process.env.GITHUB_APP_PRIVATE_KEY,
GITHUB_APP_SLUG: process.env.GITHUB_APP_SLUG,
GITHUB_APP_ACCESS_TOKEN: process.env.GITHUB_APP_ACCESS_TOKEN,
TRIGGER_API_KEY: process.env.TRIGGER_API_KEY,
TRIGGER_API_URL: process.env.TRIGGER_API_URL,
},
});
3 changes: 3 additions & 0 deletions jobs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// export all your job files here

export * from "./issueReminder"
115 changes: 115 additions & 0 deletions jobs/issueReminder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { extractIssueNumbers, getOctokitInstance } from "@/lib/github/utils";
import { triggerDotDevClient } from "@/trigger";
import { Octokit } from "@octokit/rest";
import { eventTrigger } from "@trigger.dev/sdk";
import { z } from "zod";

const findPullRequestByIssueAndCommenter = async (
octokit: Octokit,
owner: string,
repo: string,
issueNumber: number,
commenter: string
) => {
const { data: pullRequests } = await octokit.pulls.list({
owner,
repo,
state: "open",
});
const pullRequest = pullRequests.find(async (pr) => {
const openedBy = pr.user?.login;
const prBody = pr.body;
const prIssueNumber = extractIssueNumbers(prBody!);

// Return the PR that matches the issue number and belongs to the commenter
return prIssueNumber.includes(issueNumber) && openedBy === commenter;
});

return pullRequest;
};

triggerDotDevClient.defineJob({
// This is the unique identifier for your Job, it must be unique across all Jobs in your project.
id: "issue-reminder-job",
name: "issue reminder job",
version: "0.0.1",
// This is triggered by an event using eventTrigger. You can also trigger Jobs with webhooks, on schedules, and more: https://trigger.dev/docs/documentation/concepts/triggers/introduction
trigger: eventTrigger({
name: "issue.reminder",
schema: z.object({
issueNumber: z.number(),
repo: z.string(),
owner: z.string(),
commenter: z.string(),
installationId: z.number(),
}),
}),
run: async (payload, io, ctx) => {
const { issueNumber, repo, owner, commenter, installationId } = payload;
const octokit = getOctokitInstance(installationId);

//wait for 36hrs
await io.wait("waiting for 36hrs", 36 * 60 * 60);

//made this a task so it doesn't get replayed
const taskValue = await io.runTask("36-hrs", async () => {
const pullRequest = await findPullRequestByIssueAndCommenter(
octokit,
owner,
repo,
issueNumber,
commenter
);

if (!!pullRequest) {
io.logger.info("pull request has been created for the issue after 36hrs");
return { completed: true };
} else {
//send a comment reminder to the issue
io.logger.info("pull request has not been created for the issue after 36hrs, sending a reminder");
await octokit.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: ` @${commenter}, You have 12hrs left to create a pull request for this issue. `,
});
return { completed: false };
}
});

if (taskValue?.completed) {
return;
} else {
await io.wait("waiting for 12hrs", 12 * 60 * 60);

await io.runTask("48-hrs", async () => {
const pullRequest = await findPullRequestByIssueAndCommenter(
octokit,
owner,
repo,
issueNumber,
commenter
);

if (!!pullRequest) {
io.logger.info("pull request has been created for the issue within 48hrs");
return;
} else {
io.logger.info("pull request has not been created for the issue after 12hrs");
await octokit.issues.createComment({
owner: owner,
repo: repo,
issue_number: issueNumber,
body: `@${commenter} has been unassigned from the issue, anyone can now take it up.`,
});
await octokit.issues.removeAssignees({
owner: owner,
repo: repo,
issue_number: issueNumber,
assignees: [commenter],
});
}
});
}
},
});
9 changes: 8 additions & 1 deletion lib/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { env } from "../env.mjs";


export const DEFAULT_CACHE_REVALIDATION_INTERVAL = 60 * 30; // 30 minutes

export const GITHUB_CACHE_REVALIDATION_INTERVAL = 60 * 60 * 24; // 24 hours
Expand All @@ -15,6 +16,7 @@ export enum EVENT_TRIGGERS {
ISSUE_OPENED = "issues.opened",
INSTALLATION_CREATED = "installation.created",
ISSUE_COMMENTED = "issue_comment.created",
PULL_REQUEST_OPENED = "pull_request.opened"
}
export const AWARD_POINTS_IDENTIFIER = "/award" as const;

Expand All @@ -28,4 +30,9 @@ export const GITHUB_APP_WEBHOOK_SECRET = env.GITHUB_APP_WEBHOOK_SECRET as string
export const GITHUB_APP_SLUG = env.GITHUB_APP_SLUG as string;
export const GITHUB_APP_ACCESS_TOKEN = env.GITHUB_APP_ACCESS_TOKEN as string;

export const OSS_GG_LABEL = "🕹️ oss.gg" as const;
// export const OSS_GG_LABEL = "🕹️ oss.gg" as const;
export const OSS_GG_LABEL = ":joystick: oss.gg" as const;

// Trigger.dev
export const TRIGGER_API_KEY = env.TRIGGER_API_KEY as string;
export const TRIGGER_API_URL = env.TRIGGER_API_URL as string;
29 changes: 28 additions & 1 deletion lib/github/hooks/issue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,19 @@ import {
import { assignUserPoints } from "@/lib/points/service";
import { getRepositoryByGithubId } from "@/lib/repository/service";
import { createUser, getUser, getUserByGithubId } from "@/lib/user/service";
import { triggerDotDevClient } from "@/trigger";
import { Webhooks } from "@octokit/webhooks";

import { getOctokitInstance } from "../utils";
import { extractIssueNumbers, getOctokitInstance } from "../utils";

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(
Expand Down Expand Up @@ -102,6 +107,19 @@ export const onAssignCommented = async (webhooks: Webhooks) => {
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
await triggerDotDevClient.sendEvent({
name: "issue.reminder",
payload: {
issueNumber,
repo,
owner,
commenter,
installationId: context.payload.installation?.id,
},
});

await octokit.issues.createComment({
owner,
repo,
Expand Down Expand Up @@ -280,3 +298,12 @@ export const onAwardPoints = async (webhooks: Webhooks) => {
}
});
};

export const onPullRequestOpened = async (webhooks: Webhooks) => {
webhooks.on(EVENT_TRIGGERS.PULL_REQUEST_OPENED, async (context) => {
const pullRequestUser = context.payload.pull_request.user;
const body = context.payload.pull_request.body;
const issueNumber = extractIssueNumbers(body!);
// create a comment on the issue that a PR has been opened
});
};
9 changes: 8 additions & 1 deletion lib/github/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import { Webhooks, createNodeMiddleware } from "@octokit/webhooks";

import { GITHUB_APP_WEBHOOK_SECRET } from "../constants";
import { onInstallationCreated } from "./hooks/installation";
import { onAssignCommented, onAwardPoints, onIssueOpened, onUnassignCommented } from "./hooks/issue";
import {
onAssignCommented,
onAwardPoints,
onIssueOpened,
onPullRequestOpened,
onUnassignCommented,
} from "./hooks/issue";

const webhooks = new Webhooks({
secret: GITHUB_APP_WEBHOOK_SECRET,
Expand All @@ -18,4 +24,5 @@ export const registerHooks = async () => {
onAssignCommented(webhooks);
onUnassignCommented(webhooks);
onAwardPoints(webhooks);
onPullRequestOpened(webhooks);
};
21 changes: 20 additions & 1 deletion lib/github/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createAppAuth } from "@octokit/auth-app";
import { Octokit } from "@octokit/rest";
import crypto from "node:crypto";

import {
GITHUB_APP_CLIENT_ID,
Expand All @@ -13,14 +14,32 @@ export const getOctokitInstance = (installationId: number) => {
throw new Error("No installation id provided");
}

//Converting PKCS#1 to PKCS#8
//For it to work trigger it has to be in PKCS#8 format

const privateKeyPkcs8 = crypto.createPrivateKey(GITHUB_APP_PRIVATE_KEY).export({
type: "pkcs8",
format: "pem",
});

Comment on lines +17 to +24
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we should include this in this PR as things work just fine for now. Can you revert this change please?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I revert this, it'll throw an error when the trigger job runs. I think that was why you weren't able to see the comment, I had the same issue while I was testing.

const octokit = new Octokit({
authStrategy: createAppAuth,
auth: {
appId: GITHUB_APP_ID!,
privateKey: GITHUB_APP_PRIVATE_KEY!,
privateKey: privateKeyPkcs8!,
installationId,
},
});

return octokit;
};

export const extractIssueNumbers = (body: string): number[] => {
const regex = /#(\d+)/g;
const matches = body.match(regex);
if (matches) {
return matches.map((match) => parseInt(match.substring(1)));
} else {
return [];
}
};
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toast": "^1.1.5",
"@t3-oss/env-nextjs": "^0.8.0",
"@trigger.dev/nextjs": "^2.3.18",
"@trigger.dev/react": "^2.3.18",
"@trigger.dev/sdk": "^2.3.18",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"jsonwebtoken": "^9.0.2",
Expand Down Expand Up @@ -68,5 +71,8 @@
"rimraf": "^5.0.5",
"tailwindcss": "^3.4.1",
"typescript": "^5"
},
"trigger.dev": {
"endpointId": "testing-bHhV"
}
}
Loading
Loading