diff --git a/.github/workflows/compute.yml b/.github/workflows/compute.yml index 66c0313..9ebcc7e 100644 --- a/.github/workflows/compute.yml +++ b/.github/workflows/compute.yml @@ -27,6 +27,7 @@ jobs: SUPABASE_URL: ${{ secrets.SUPABASE_URL }} SUPABASE_KEY: ${{ secrets.SUPABASE_KEY }} VOYAGEAI_API_KEY: ${{secrets.VOYAGEAI_API_KEY}} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v4 @@ -46,3 +47,4 @@ jobs: SUPABASE_URL: ${{ secrets.SUPABASE_URL }} SUPABASE_KEY: ${{ secrets.SUPABASE_KEY }} VOYAGEAI_API_KEY: ${{secrets.VOYAGEAI_API_KEY}} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/src/adapters/supabase/helpers/comment.ts b/src/adapters/supabase/helpers/comment.ts index 295f8ae..3d94fc5 100644 --- a/src/adapters/supabase/helpers/comment.ts +++ b/src/adapters/supabase/helpers/comment.ts @@ -28,11 +28,10 @@ export class Comment extends SuperSupabase { //First Check if the comment already exists const { data, error } = await this.supabase.from("issue_comments").select("*").eq("id", commentNodeId); if (error) { - this.context.logger.error("Error creating comment", error); - return; + throw new Error("Error creating comment"); } if (data && data.length > 0) { - this.context.logger.info("Comment already exists"); + throw new Error("Comment already exists"); return; } else { //Create the embedding for this comment @@ -47,8 +46,7 @@ export class Comment extends SuperSupabase { .from("issue_comments") .insert([{ id: commentNodeId, markdown, plaintext, author_id: authorId, payload, embedding: embedding, issue_id: issueId }]); if (error) { - this.context.logger.error("Error creating comment", error); - return; + throw new Error("Error creating comment: " + error.message); } } this.context.logger.info("Comment created successfully"); @@ -80,7 +78,7 @@ export class Comment extends SuperSupabase { .update({ markdown, plaintext, embedding: embedding, payload, modified_at: new Date() }) .eq("id", commentNodeId); if (error) { - this.context.logger.error("Error updating comment", error); + throw new Error("Error updating comment: " + error.message); } } } @@ -96,7 +94,7 @@ export class Comment extends SuperSupabase { async deleteComment(commentNodeId: string) { const { error } = await this.supabase.from("issue_comments").delete().eq("id", commentNodeId); if (error) { - this.context.logger.error("Error deleting comment", error); + throw new Error("Error deleting comment: " + error.message); } } } diff --git a/src/adapters/supabase/helpers/issues.ts b/src/adapters/supabase/helpers/issues.ts index 4e4f7e5..d00781c 100644 --- a/src/adapters/supabase/helpers/issues.ts +++ b/src/adapters/supabase/helpers/issues.ts @@ -29,14 +29,13 @@ export class Issues extends SuperSupabase { //First Check if the issue already exists const { data, error } = await this.supabase.from("issues").select("*").eq("id", issueNodeId); if (error) { - this.context.logger.error("Error creating issue", error); - return; + throw new Error("Error creating issue: " + error.message); } if (data && data.length > 0) { this.context.logger.info("Issue already exists"); return; } else { - const embedding = await this.context.adapters.voyage.embedding.createEmbedding(markdown); + const embedding = await this.context.adapters.voyage.embedding.createEmbedding(markdown, "document"); let plaintext: string | null = markdownToPlainText(markdown); if (isPrivate) { payload = null; @@ -45,8 +44,7 @@ export class Issues extends SuperSupabase { } const { error } = await this.supabase.from("issues").insert([{ id: issueNodeId, payload, markdown, plaintext, author_id: authorId, embedding }]); if (error) { - this.context.logger.error("Error creating issue", error); - return; + throw new Error("Error creating issue: " + error.message); } } this.context.logger.info("Issue created successfully"); @@ -61,14 +59,13 @@ export class Issues extends SuperSupabase { plaintext = null; } const issues = await this.getIssue(issueNodeId); - if (issues && issues.length == 0) { + if (!issues) { this.context.logger.info("Issue does not exist, creating a new one"); await this.createIssue(issueNodeId, payload, isPrivate, markdown, authorId); } else { const { error } = await this.supabase.from("issues").update({ markdown, plaintext, embedding, payload, modified_at: new Date() }).eq("id", issueNodeId); - if (error) { - this.context.logger.error("Error updating comment", error); + throw new Error("Error updating issue: " + error.message); } } } @@ -76,7 +73,7 @@ export class Issues extends SuperSupabase { async deleteIssue(issueNodeId: string) { const { error } = await this.supabase.from("issues").delete().eq("id", issueNodeId); if (error) { - this.context.logger.error("Error deleting comment", error); + throw new Error("Error deleting issue: " + error.message); } } @@ -86,8 +83,12 @@ export class Issues extends SuperSupabase { .select("*") .eq("id", issueNodeId) .returns(); - if (error) { - this.context.logger.error("Error getting issue", error); + if (error || !data || data.length === 0) { + if (error) { + this.context.logger.error("Error getting issue", error); + } else { + this.context.logger.error("Error getting issue: No data found"); + } return null; } return data; @@ -107,11 +108,4 @@ export class Issues extends SuperSupabase { } return data; } - - async updatePayload(issueNodeId: string, payload: Record) { - const { error } = await this.supabase.from("issues").update({ payload }).eq("id", issueNodeId); - if (error) { - this.context.logger.error("Error updating issue payload", error); - } - } } diff --git a/src/adapters/supabase/helpers/supabase.ts b/src/adapters/supabase/helpers/supabase.ts index 34e845c..3f8268d 100644 --- a/src/adapters/supabase/helpers/supabase.ts +++ b/src/adapters/supabase/helpers/supabase.ts @@ -9,4 +9,15 @@ export class SuperSupabase { this.supabase = supabase; this.context = context; } + + async checkConnection(): Promise { + const { error } = await this.supabase.from("issues").select("*").limit(1); + // If there's no error, the connection is working + if (!error) { + return true; + } else { + console.log(error); + throw new Error("Error connecting to Supabase or Schema has not been migrated/created"); + } + } } diff --git a/src/handlers/add-comments.ts b/src/handlers/add-comments.ts index 54745a1..94ac384 100644 --- a/src/handlers/add-comments.ts +++ b/src/handlers/add-comments.ts @@ -1,5 +1,6 @@ import { Context } from "../types"; import { CommentPayload } from "../types/payload"; +import { addIssue } from "./add-issue"; export async function addComments(context: Context) { const { @@ -17,7 +18,15 @@ export async function addComments(context: Context) { if (!markdown) { throw new Error("Comment body is empty"); } + if (context.payload.issue.pull_request) { + throw new Error("Comment is on a pull request"); + } + if ((await supabase.issue.getIssue(issueId)) === null) { + await addIssue(context); + } await supabase.comment.createComment(markdown, nodeId, authorId, payload, isPrivate, issueId); + logger.ok(`Created Comment with id: ${nodeId}`); + logger.ok(`Successfully created comment!`); } catch (error) { if (error instanceof Error) { logger.error(`Error creating comment:`, { error: error, stack: error.stack }); @@ -27,7 +36,5 @@ export async function addComments(context: Context) { throw error; } } - - logger.ok(`Successfully created comment!`); logger.debug(`Exiting addComments`); } diff --git a/src/handlers/issue-deduplication.ts b/src/handlers/issue-deduplication.ts index f32c1ba..b667a7b 100644 --- a/src/handlers/issue-deduplication.ts +++ b/src/handlers/issue-deduplication.ts @@ -41,8 +41,11 @@ export async function issueChecker(context: Context): Promise { issueBody = removeFootnotes(issueBody); const similarIssues = await supabase.issue.findSimilarIssues(issue.title + removeFootnotes(issueBody), 0.7, issue.node_id); if (similarIssues && similarIssues.length > 0) { - const matchIssues = similarIssues.filter((issue) => issue.similarity >= context.config.matchThreshold); - const processedIssues = await processSimilarIssues(similarIssues, context, issueBody); + let processedIssues = await processSimilarIssues(similarIssues, context, issueBody); + processedIssues = processedIssues.filter((issue) => + matchRepoOrgToSimilarIssueRepoOrg(payload.repository.owner.login, issue.node.repository.owner.login, payload.repository.name, issue.node.repository.name) + ); + const matchIssues = processedIssues.filter((issue) => parseFloat(issue.similarity) >= context.config.matchThreshold); if (matchIssues.length > 0) { logger.info(`Similar issue which matches more than ${context.config.matchThreshold} already exists`); //To the issue body, add a footnote with the link to the similar issue @@ -58,7 +61,7 @@ export async function issueChecker(context: Context): Promise { }); return true; } - if (similarIssues.length > 0) { + if (processedIssues.length > 0) { logger.info(`Similar issue which matches more than ${context.config.warningThreshold} already exists`); await handleSimilarIssuesComment(context, payload, issueBody, issue.number, processedIssues); return true; @@ -130,15 +133,13 @@ function findMostSimilarSentence(issueContent: string, similarIssueContent: stri return { sentence: mostSimilarSentence, similarity: maxSimilarity, index: mostSimilarIndex }; } -async function handleSimilarIssuesComment(context: Context, payload: IssuePayload, issueBody: string, issueNumber: number, issueList: IssueGraphqlResponse[]) { - const relevantIssues = issueList.filter((issue) => - matchRepoOrgToSimilarIssueRepoOrg(payload.repository.owner.login, issue.node.repository.owner.login, payload.repository.name, issue.node.repository.name) - ); - - if (relevantIssues.length === 0) { - context.logger.info("No relevant issues found with the same repository and organization"); - } - +async function handleSimilarIssuesComment( + context: Context, + payload: IssuePayload, + issueBody: string, + issueNumber: number, + relevantIssues: IssueGraphqlResponse[] +) { if (!issueBody) { return; } @@ -183,16 +184,8 @@ async function handleMatchIssuesComment( context: Context, payload: IssuePayload, issueBody: string, - issueList: IssueGraphqlResponse[] + relevantIssues: IssueGraphqlResponse[] ): Promise { - const relevantIssues = issueList.filter((issue) => - matchRepoOrgToSimilarIssueRepoOrg(payload.repository.owner.login, issue.node.repository.owner.login, payload.repository.name, issue.node.repository.name) - ); - - if (relevantIssues.length === 0) { - context.logger.info("No relevant issues found with the same repository and organization"); - } - if (!issueBody) { return; } @@ -292,9 +285,15 @@ export function removeFootnotes(content: string): string { contentWithoutFootnotes = contentWithoutFootnotes.replace(new RegExp(`\\[\\^${footnoteNumber}\\^\\]`, "g"), ""); }); } + contentWithoutFootnotes = removeCautionMessages(contentWithoutFootnotes); return contentWithoutFootnotes; } +export function removeCautionMessages(content: string): string { + const cautionRegex = />[!CAUTION]\n> This issue may be a duplicate of the following issues:\n((> - \[[^\]]+\]\([^)]+\)\n)+)/g; + return content.replace(cautionRegex, ""); +} + function checkIfDuplicateFootNoteExists(content: string): boolean { const footnoteDefRegex = /\[\^(\d+)\^\]: ⚠ \d+% possible duplicate - [^\n]+(\n|$)/g; const footnotes = content.match(footnoteDefRegex); diff --git a/src/handlers/update-comments.ts b/src/handlers/update-comments.ts index 6cc9545..cd585ed 100644 --- a/src/handlers/update-comments.ts +++ b/src/handlers/update-comments.ts @@ -1,5 +1,6 @@ import { Context } from "../types"; import { CommentPayload } from "../types/payload"; +import { addIssue } from "./add-issue"; export async function updateComment(context: Context) { const { @@ -18,6 +19,12 @@ export async function updateComment(context: Context) { if (!markdown) { throw new Error("Comment body is empty"); } + if (context.payload.issue.pull_request) { + throw new Error("Comment is on a pull request"); + } + if ((await supabase.issue.getIssue(issueId)) === null) { + await addIssue(context); + } await supabase.comment.updateComment(markdown, nodeId, authorId, payload, isPrivate, issueId); } catch (error) { if (error instanceof Error) { diff --git a/src/main.ts b/src/main.ts index 0b9b9a4..9524eea 100644 --- a/src/main.ts +++ b/src/main.ts @@ -27,7 +27,6 @@ export async function run() { }; await plugin(inputs, env); - return returnDataToKernel(process.env.GITHUB_TOKEN, inputs.stateId, {}); } diff --git a/src/plugin.ts b/src/plugin.ts index 5cdf193..3e2f220 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -37,8 +37,8 @@ export async function runPlugin(context: Context) { await issueChecker(context); return await issueMatching(context); case "issues.edited": - await issueChecker(context); await updateIssue(context); + await issueChecker(context); return await issueMatching(context); case "issues.deleted": return await deleteIssues(context); @@ -72,5 +72,7 @@ export async function plugin(inputs: PluginInputs, env: Env) { adapters: {} as ReturnType, }; context.adapters = createAdapters(supabase, voyageClient, context); + //Check the supabase adapter + await context.adapters.supabase.super.checkConnection(); return await runPlugin(context); } diff --git a/tests/__mocks__/adapter.ts b/tests/__mocks__/adapter.ts index abb271f..dc95cb1 100644 --- a/tests/__mocks__/adapter.ts +++ b/tests/__mocks__/adapter.ts @@ -12,6 +12,13 @@ export interface CommentMock { embedding: number[]; } +export interface IssueMock { + id: string; + plaintext: string; + author_id: number; + payload: Record; +} + export function createMockAdapters(context: Context) { const commentMap: Map = new Map(); return { @@ -72,6 +79,18 @@ export function createMockAdapters(context: Context) { return commentMap.get(commentNodeId); }), } as unknown as Comment, + issue: { + getIssue: jest.fn(async (issueNodeId: string) => { + return [ + { + id: issueNodeId, + plaintext: STRINGS.HELLO_WORLD, + author_id: 1, + payload: {}, + } as IssueMock, + ]; + }), + }, }, voyage: { embedding: {