From da877f7aff6fbccd1ded91dfe61e9057e21679d6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 1 Sep 2024 03:50:45 +0000 Subject: [PATCH 1/8] chore: updated generated Supabase types --- src/types/database.ts | 340 ------------------------------------------ 1 file changed, 340 deletions(-) diff --git a/src/types/database.ts b/src/types/database.ts index 3622f52..f741570 100644 --- a/src/types/database.ts +++ b/src/types/database.ts @@ -1,31 +1,6 @@ export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[]; export type Database = { - graphql_public: { - Tables: { - [_ in never]: never; - }; - Views: { - [_ in never]: never; - }; - Functions: { - graphql: { - Args: { - operationName?: string; - query?: string; - variables?: Json; - extensions?: Json; - }; - Returns: Json; - }; - }; - Enums: { - [_ in never]: never; - }; - CompositeTypes: { - [_ in never]: never; - }; - }; public: { Tables: { issue_comments: { @@ -240,321 +215,6 @@ export type Database = { [_ in never]: never; }; }; - storage: { - Tables: { - buckets: { - Row: { - allowed_mime_types: string[] | null; - avif_autodetection: boolean | null; - created_at: string | null; - file_size_limit: number | null; - id: string; - name: string; - owner: string | null; - owner_id: string | null; - public: boolean | null; - updated_at: string | null; - }; - Insert: { - allowed_mime_types?: string[] | null; - avif_autodetection?: boolean | null; - created_at?: string | null; - file_size_limit?: number | null; - id: string; - name: string; - owner?: string | null; - owner_id?: string | null; - public?: boolean | null; - updated_at?: string | null; - }; - Update: { - allowed_mime_types?: string[] | null; - avif_autodetection?: boolean | null; - created_at?: string | null; - file_size_limit?: number | null; - id?: string; - name?: string; - owner?: string | null; - owner_id?: string | null; - public?: boolean | null; - updated_at?: string | null; - }; - Relationships: []; - }; - migrations: { - Row: { - executed_at: string | null; - hash: string; - id: number; - name: string; - }; - Insert: { - executed_at?: string | null; - hash: string; - id: number; - name: string; - }; - Update: { - executed_at?: string | null; - hash?: string; - id?: number; - name?: string; - }; - Relationships: []; - }; - objects: { - Row: { - bucket_id: string | null; - created_at: string | null; - id: string; - last_accessed_at: string | null; - metadata: Json | null; - name: string | null; - owner: string | null; - owner_id: string | null; - path_tokens: string[] | null; - updated_at: string | null; - user_metadata: Json | null; - version: string | null; - }; - Insert: { - bucket_id?: string | null; - created_at?: string | null; - id?: string; - last_accessed_at?: string | null; - metadata?: Json | null; - name?: string | null; - owner?: string | null; - owner_id?: string | null; - path_tokens?: string[] | null; - updated_at?: string | null; - user_metadata?: Json | null; - version?: string | null; - }; - Update: { - bucket_id?: string | null; - created_at?: string | null; - id?: string; - last_accessed_at?: string | null; - metadata?: Json | null; - name?: string | null; - owner?: string | null; - owner_id?: string | null; - path_tokens?: string[] | null; - updated_at?: string | null; - user_metadata?: Json | null; - version?: string | null; - }; - Relationships: [ - { - foreignKeyName: "objects_bucketId_fkey"; - columns: ["bucket_id"]; - isOneToOne: false; - referencedRelation: "buckets"; - referencedColumns: ["id"]; - }, - ]; - }; - s3_multipart_uploads: { - Row: { - bucket_id: string; - created_at: string; - id: string; - in_progress_size: number; - key: string; - owner_id: string | null; - upload_signature: string; - user_metadata: Json | null; - version: string; - }; - Insert: { - bucket_id: string; - created_at?: string; - id: string; - in_progress_size?: number; - key: string; - owner_id?: string | null; - upload_signature: string; - user_metadata?: Json | null; - version: string; - }; - Update: { - bucket_id?: string; - created_at?: string; - id?: string; - in_progress_size?: number; - key?: string; - owner_id?: string | null; - upload_signature?: string; - user_metadata?: Json | null; - version?: string; - }; - Relationships: [ - { - foreignKeyName: "s3_multipart_uploads_bucket_id_fkey"; - columns: ["bucket_id"]; - isOneToOne: false; - referencedRelation: "buckets"; - referencedColumns: ["id"]; - }, - ]; - }; - s3_multipart_uploads_parts: { - Row: { - bucket_id: string; - created_at: string; - etag: string; - id: string; - key: string; - owner_id: string | null; - part_number: number; - size: number; - upload_id: string; - version: string; - }; - Insert: { - bucket_id: string; - created_at?: string; - etag: string; - id?: string; - key: string; - owner_id?: string | null; - part_number: number; - size?: number; - upload_id: string; - version: string; - }; - Update: { - bucket_id?: string; - created_at?: string; - etag?: string; - id?: string; - key?: string; - owner_id?: string | null; - part_number?: number; - size?: number; - upload_id?: string; - version?: string; - }; - Relationships: [ - { - foreignKeyName: "s3_multipart_uploads_parts_bucket_id_fkey"; - columns: ["bucket_id"]; - isOneToOne: false; - referencedRelation: "buckets"; - referencedColumns: ["id"]; - }, - { - foreignKeyName: "s3_multipart_uploads_parts_upload_id_fkey"; - columns: ["upload_id"]; - isOneToOne: false; - referencedRelation: "s3_multipart_uploads"; - referencedColumns: ["id"]; - }, - ]; - }; - }; - Views: { - [_ in never]: never; - }; - Functions: { - can_insert_object: { - Args: { - bucketid: string; - name: string; - owner: string; - metadata: Json; - }; - Returns: undefined; - }; - extension: { - Args: { - name: string; - }; - Returns: string; - }; - filename: { - Args: { - name: string; - }; - Returns: string; - }; - foldername: { - Args: { - name: string; - }; - Returns: string[]; - }; - get_size_by_bucket: { - Args: Record; - Returns: { - size: number; - bucket_id: string; - }[]; - }; - list_multipart_uploads_with_delimiter: { - Args: { - bucket_id: string; - prefix_param: string; - delimiter_param: string; - max_keys?: number; - next_key_token?: string; - next_upload_token?: string; - }; - Returns: { - key: string; - id: string; - created_at: string; - }[]; - }; - list_objects_with_delimiter: { - Args: { - bucket_id: string; - prefix_param: string; - delimiter_param: string; - max_keys?: number; - start_after?: string; - next_token?: string; - }; - Returns: { - name: string; - id: string; - metadata: Json; - updated_at: string; - }[]; - }; - operation: { - Args: Record; - Returns: string; - }; - search: { - Args: { - prefix: string; - bucketname: string; - limits?: number; - levels?: number; - offsets?: number; - search?: string; - sortcolumn?: string; - sortorder?: string; - }; - Returns: { - name: string; - id: string; - updated_at: string; - created_at: string; - last_accessed_at: string; - metadata: Json; - }[]; - }; - }; - Enums: { - [_ in never]: never; - }; - CompositeTypes: { - [_ in never]: never; - }; - }; }; type PublicSchema = Database[Extract]; From 2a08b22a98526e65000d09119cbc8fa19737bced Mon Sep 17 00:00:00 2001 From: Shivaditya Shivganesh Date: Fri, 13 Sep 2024 16:37:12 -0400 Subject: [PATCH 2/8] feat: issue dedup --- src/adapters/supabase/helpers/issues.ts | 20 +-- src/handlers/issue-deduplication.ts | 136 ++++++++++++++++++ src/plugin.ts | 8 +- .../20240913145445_issue_comments.sql | 13 ++ 4 files changed, 166 insertions(+), 11 deletions(-) create mode 100644 src/handlers/issue-deduplication.ts create mode 100644 supabase/migrations/20240913145445_issue_comments.sql diff --git a/src/adapters/supabase/helpers/issues.ts b/src/adapters/supabase/helpers/issues.ts index 063d8a9..05fc62d 100644 --- a/src/adapters/supabase/helpers/issues.ts +++ b/src/adapters/supabase/helpers/issues.ts @@ -13,6 +13,12 @@ export interface IssueType { embedding: number[]; } +export interface IssueSimilaritySearchResult { + issue_id: string; + issue_plaintext: string; + similarity: number; +} + export class Issues extends SuperSupabase { constructor(supabase: SupabaseClient, context: Context) { super(supabase, context); @@ -70,15 +76,13 @@ export class Issues extends SuperSupabase { } } - async findSimilarIssues(markdown: string, threshold: number): Promise { + async findSimilarIssues(markdown: string, threshold: number, currentId: string): Promise { const embedding = await this.context.adapters.voyage.embedding.createEmbedding(markdown); - const { data, error } = await this.supabase - .from("issues") - .select("*") - .eq("type", "issue") - .textSearch("embedding", embedding.join(",")) - .order("embedding", { foreignTable: "issues", ascending: false }) - .lte("embedding", threshold); + const { data, error } = await this.supabase.rpc("find_similar_issues", { + current_id: currentId, + query_embedding: embedding, + threshold: threshold, + }); if (error) { this.context.logger.error("Error finding similar issues", error); return []; diff --git a/src/handlers/issue-deduplication.ts b/src/handlers/issue-deduplication.ts new file mode 100644 index 0000000..2ef8eb2 --- /dev/null +++ b/src/handlers/issue-deduplication.ts @@ -0,0 +1,136 @@ +import { IssueSimilaritySearchResult } from "../adapters/supabase/helpers/issues"; +import { Context } from "../types"; +const MATCH_THRESHOLD = 0.95; +const WARNING_THRESHOLD = 0.5; + +export interface IssueGraphqlResponse { + node: { + title: string; + url: string; + }; +} + +/** + * Check if an issue is similar to any existing issues in the database + * @param context + * @returns true if the issue is similar to an existing issue, false otherwise + */ +export async function issueChecker(context: Context): Promise { + const { + logger, + payload, + adapters: { supabase }, + } = context; + + const issue = payload.issue; + + //First Check if an issue with more than MATCH_THRESHOLD similarity exists (Very Similar) + const similarIssue = await supabase.issue.findSimilarIssues(issue.body + issue.title, MATCH_THRESHOLD, issue.node_id); + if (similarIssue && similarIssue?.length > 0) { + logger.info(`Similar issue which matches more than ${MATCH_THRESHOLD} already exists`); + //Close the issue as "unplanned" + await context.octokit.issues.update({ + owner: payload.repository.owner.login, + repo: payload.repository.name, + issue_number: issue.number, + state: "closed", + labels: ["unplanned"], + }); + return true; + } + + //Second Check if an issue with more than WARNING_THRESHOLD similarity exists (Warning) + const warningIssue = await supabase.issue.findSimilarIssues(issue.body + issue.title, WARNING_THRESHOLD, issue.node_id); + if (warningIssue && warningIssue?.length > 0) { + logger.info(`Similar issue which matches more than ${WARNING_THRESHOLD} already exists`); + //Add a comment immediately next to the issue + //Build a list of similar issues url + const issueList: IssueGraphqlResponse[] = await Promise.all( + warningIssue.map(async (issue: IssueSimilaritySearchResult) => { + //fetch the issue url and title using globaNodeId + const issueUrl: IssueGraphqlResponse = await context.octokit.graphql( + `query($issueNodeId: ID!) { + node(id: $issueNodeId) { + ... on Issue { + title + url + } + } + }`, + { + issueNodeId: issue.issue_id, + } + ); + return issueUrl; + }) + ); + + // Reopen the issue + await context.octokit.issues.update({ + owner: payload.repository.owner.login, + repo: payload.repository.name, + issue_number: issue.number, + state: "open", + }); + //Remove the "unplanned" label + await context.octokit.issues.removeLabel({ + owner: payload.repository.owner.login, + repo: payload.repository.name, + issue_number: issue.number, + name: "unplanned", + }); + // Check if there is already a comment on the issue + const existingComment = await context.octokit.issues.listComments({ + owner: payload.repository.owner.login, + repo: payload.repository.name, + issue_number: issue.number, + }); + if (existingComment.data.length > 0) { + // Find the comment that lists the similar issues + const commentToUpdate = existingComment.data.find( + (comment) => comment && comment.body && comment.body.includes("This issue seems to be similar to the following issue(s)") + ); + + if (commentToUpdate) { + // Update the comment with the latest list of similar issues + const body = issueList.map((issue) => `- [${issue.node.title}](${issue.node.url})`).join("\n"); + const updatedBody = `This issue seems to be similar to the following issue(s):\n\n${body}`; + await context.octokit.issues.updateComment({ + owner: payload.repository.owner.login, + repo: payload.repository.name, + comment_id: commentToUpdate.id, + body: updatedBody, + }); + } else { + // Add a new comment to the issue + await createNewComment(context, issueList); + } + } else { + // Add a new comment to the issue + await createNewComment(context, issueList); + } + return true; + } + + logger.info("No similar issue found"); + return false; +} + +/** + * Create a new comment on the issue with the list of similar issues + * @param context + * @param resolvedIssueList + */ +async function createNewComment(context: Context, resolvedIssueList: IssueGraphqlResponse[]) { + let body = "This issue seems to be similar to the following issue(s):\n\n"; + resolvedIssueList.forEach((issue) => { + const issueLine = `- [${issue.node.title}](${issue.node.url})\n`; + body += issueLine; + }); + await context.octokit.issues.createComment({ + owner: context.payload.repository.owner.login, + repo: context.payload.repository.name, + issue_number: context.payload.issue.number, + body: body, + }); +} diff --git a/src/plugin.ts b/src/plugin.ts index 2f7dad8..0af10f0 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -13,6 +13,7 @@ import { VoyageAIClient } from "voyageai"; import { deleteIssues } from "./handlers/delete-issue"; import { addIssue } from "./handlers/add-issue"; import { updateIssue } from "./handlers/update-issue"; +import { issueChecker } from "./handlers/issue-deduplication"; /** * The main plugin function. Split for easier testing. @@ -31,11 +32,12 @@ export async function runPlugin(context: Context) { } else if (isIssueEvent(context)) { switch (eventName) { case "issues.opened": - return await addIssue(context); - case "issues.deleted": - return await deleteIssues(context); + return (await issueChecker(context)) ? null : await addIssue(context); case "issues.edited": + await issueChecker(context); return await updateIssue(context); + case "issues.deleted": + return await deleteIssues(context); } } else { logger.error(`Unsupported event: ${eventName}`); diff --git a/supabase/migrations/20240913145445_issue_comments.sql b/supabase/migrations/20240913145445_issue_comments.sql new file mode 100644 index 0000000..1753ab1 --- /dev/null +++ b/supabase/migrations/20240913145445_issue_comments.sql @@ -0,0 +1,13 @@ +CREATE OR REPLACE FUNCTION find_similar_issues(current_id VARCHAR, query_embedding vector(1024), threshold float8) +RETURNS TABLE(issue_id VARCHAR, issue_plaintext TEXT, similarity float8) AS $$ +BEGIN + RETURN QUERY + SELECT id AS issue_id, + plaintext AS issue_plaintext, + 1 - (embedding <=> query_embedding) AS similarity + FROM issues + WHERE id <> current_id + AND 1 - (embedding <=> query_embedding) >= threshold + ORDER BY similarity DESC; +END; +$$ LANGUAGE plpgsql; From 736a0bf134b1b9cead19d487ff22644abf5ffad1 Mon Sep 17 00:00:00 2001 From: Shivaditya Shivganesh Date: Fri, 13 Sep 2024 16:38:46 -0400 Subject: [PATCH 3/8] fix: cspell --- .cspell.json | 3 ++- src/handlers/issue-deduplication.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.cspell.json b/.cspell.json index c78b966..43040b8 100644 --- a/.cspell.json +++ b/.cspell.json @@ -26,7 +26,8 @@ "voyageai", "vectordump", "payloadobject", - "markdownit" + "markdownit", + "plpgsql" ], "dictionaries": ["typescript", "node", "software-terms"], "import": ["@cspell/dict-typescript/cspell-ext.json", "@cspell/dict-node/cspell-ext.json", "@cspell/dict-software-terms"], diff --git a/src/handlers/issue-deduplication.ts b/src/handlers/issue-deduplication.ts index 2ef8eb2..07b1af9 100644 --- a/src/handlers/issue-deduplication.ts +++ b/src/handlers/issue-deduplication.ts @@ -47,7 +47,7 @@ export async function issueChecker(context: Context): Promise { //Build a list of similar issues url const issueList: IssueGraphqlResponse[] = await Promise.all( warningIssue.map(async (issue: IssueSimilaritySearchResult) => { - //fetch the issue url and title using globaNodeId + //fetch the issue url and title using globalNodeId const issueUrl: IssueGraphqlResponse = await context.octokit.graphql( `query($issueNodeId: ID!) { node(id: $issueNodeId) { From fe17b3e90ac2ce11c187d9212ee0697bbab7e25a Mon Sep 17 00:00:00 2001 From: Shivaditya Shivganesh Date: Fri, 13 Sep 2024 19:22:51 -0400 Subject: [PATCH 4/8] fix: knip : --- src/adapters/supabase/helpers/issues.ts | 10 ---------- src/handlers/issue-deduplication.ts | 16 +++++++++------- src/plugin.ts | 3 ++- 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/src/adapters/supabase/helpers/issues.ts b/src/adapters/supabase/helpers/issues.ts index 05fc62d..5f30821 100644 --- a/src/adapters/supabase/helpers/issues.ts +++ b/src/adapters/supabase/helpers/issues.ts @@ -3,16 +3,6 @@ import { SuperSupabase } from "./supabase"; import { Context } from "../../../types/context"; import { markdownToPlainText } from "../../utils/markdown-to-plaintext"; -export interface IssueType { - id: string; - markdown?: string; - author_id: number; - created_at: string; - modified_at: string; - payloadObject: Record | null; - embedding: number[]; -} - export interface IssueSimilaritySearchResult { issue_id: string; issue_plaintext: string; diff --git a/src/handlers/issue-deduplication.ts b/src/handlers/issue-deduplication.ts index 07b1af9..5c01f9e 100644 --- a/src/handlers/issue-deduplication.ts +++ b/src/handlers/issue-deduplication.ts @@ -72,13 +72,15 @@ export async function issueChecker(context: Context): Promise { issue_number: issue.number, state: "open", }); - //Remove the "unplanned" label - await context.octokit.issues.removeLabel({ - owner: payload.repository.owner.login, - repo: payload.repository.name, - issue_number: issue.number, - name: "unplanned", - }); + //Remove the "unplanned" label if it exists + if (issue.labels && issue.labels.find((label) => label.name === "unplanned")) { + await context.octokit.issues.removeLabel({ + owner: payload.repository.owner.login, + repo: payload.repository.name, + issue_number: issue.number, + name: "unplanned", + }); + } // Check if there is already a comment on the issue const existingComment = await context.octokit.issues.listComments({ owner: payload.repository.owner.login, diff --git a/src/plugin.ts b/src/plugin.ts index 0af10f0..0d0876c 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -32,7 +32,8 @@ export async function runPlugin(context: Context) { } else if (isIssueEvent(context)) { switch (eventName) { case "issues.opened": - return (await issueChecker(context)) ? null : await addIssue(context); + await issueChecker(context); + return await addIssue(context); case "issues.edited": await issueChecker(context); return await updateIssue(context); From 4d6491a2984351bfec798221f744e177fb4454f1 Mon Sep 17 00:00:00 2001 From: Shivaditya Shivganesh Date: Sat, 14 Sep 2024 12:25:50 -0400 Subject: [PATCH 5/8] fix: remove label controls and issue reopen controls, added similarity to issues in comment --- src/handlers/issue-deduplication.ts | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/src/handlers/issue-deduplication.ts b/src/handlers/issue-deduplication.ts index 5c01f9e..a5d0afa 100644 --- a/src/handlers/issue-deduplication.ts +++ b/src/handlers/issue-deduplication.ts @@ -8,6 +8,7 @@ export interface IssueGraphqlResponse { title: string; url: string; }; + similarity: number; } /** @@ -34,7 +35,7 @@ export async function issueChecker(context: Context): Promise { repo: payload.repository.name, issue_number: issue.number, state: "closed", - labels: ["unplanned"], + state_reason: "not_planned", }); return true; } @@ -61,26 +62,10 @@ export async function issueChecker(context: Context): Promise { issueNodeId: issue.issue_id, } ); + issueUrl.similarity = issue.similarity; return issueUrl; }) ); - - // Reopen the issue - await context.octokit.issues.update({ - owner: payload.repository.owner.login, - repo: payload.repository.name, - issue_number: issue.number, - state: "open", - }); - //Remove the "unplanned" label if it exists - if (issue.labels && issue.labels.find((label) => label.name === "unplanned")) { - await context.octokit.issues.removeLabel({ - owner: payload.repository.owner.login, - repo: payload.repository.name, - issue_number: issue.number, - name: "unplanned", - }); - } // Check if there is already a comment on the issue const existingComment = await context.octokit.issues.listComments({ owner: payload.repository.owner.login, @@ -95,7 +80,7 @@ export async function issueChecker(context: Context): Promise { if (commentToUpdate) { // Update the comment with the latest list of similar issues - const body = issueList.map((issue) => `- [${issue.node.title}](${issue.node.url})`).join("\n"); + const body = issueList.map((issue) => `- [${issue.node.title}](${issue.node.url}) Similarity: ${issue.similarity}`).join("\n"); const updatedBody = `This issue seems to be similar to the following issue(s):\n\n${body}`; await context.octokit.issues.updateComment({ owner: payload.repository.owner.login, @@ -126,7 +111,7 @@ export async function issueChecker(context: Context): Promise { async function createNewComment(context: Context, resolvedIssueList: IssueGraphqlResponse[]) { let body = "This issue seems to be similar to the following issue(s):\n\n"; resolvedIssueList.forEach((issue) => { - const issueLine = `- [${issue.node.title}](${issue.node.url})\n`; + const issueLine = `- [${issue.node.title}](${issue.node.url}) Similarity: ${issue.similarity}\n`; body += issueLine; }); await context.octokit.issues.createComment({ From 33a5f93dd7cde5b87d3b935e8cb4ad93527617c3 Mon Sep 17 00:00:00 2001 From: Shivaditya Shivganesh Date: Sat, 14 Sep 2024 12:28:37 -0400 Subject: [PATCH 6/8] fix: similarity in percents --- src/handlers/issue-deduplication.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/handlers/issue-deduplication.ts b/src/handlers/issue-deduplication.ts index a5d0afa..2f5ff8c 100644 --- a/src/handlers/issue-deduplication.ts +++ b/src/handlers/issue-deduplication.ts @@ -8,7 +8,7 @@ export interface IssueGraphqlResponse { title: string; url: string; }; - similarity: number; + similarity: string; } /** @@ -62,7 +62,7 @@ export async function issueChecker(context: Context): Promise { issueNodeId: issue.issue_id, } ); - issueUrl.similarity = issue.similarity; + issueUrl.similarity = (issue.similarity * 100).toFixed(2); return issueUrl; }) ); From 72d72a34d3300faa1134b35fab9627d758beb74a Mon Sep 17 00:00:00 2001 From: Shivaditya Shivganesh Date: Sat, 14 Sep 2024 15:31:29 -0400 Subject: [PATCH 7/8] fix: issue dedup warning threshold to 75 --- src/handlers/issue-deduplication.ts | 163 +++++++++++++--------------- 1 file changed, 77 insertions(+), 86 deletions(-) diff --git a/src/handlers/issue-deduplication.ts b/src/handlers/issue-deduplication.ts index 2f5ff8c..57b5d3e 100644 --- a/src/handlers/issue-deduplication.ts +++ b/src/handlers/issue-deduplication.ts @@ -1,7 +1,9 @@ import { IssueSimilaritySearchResult } from "../adapters/supabase/helpers/issues"; import { Context } from "../types"; +import { IssuePayload } from "../types/payload"; + const MATCH_THRESHOLD = 0.95; -const WARNING_THRESHOLD = 0.5; +const WARNING_THRESHOLD = 0.75; export interface IssueGraphqlResponse { node: { @@ -19,105 +21,94 @@ export interface IssueGraphqlResponse { export async function issueChecker(context: Context): Promise { const { logger, - payload, adapters: { supabase }, + octokit, } = context; - + const { payload } = context as { payload: IssuePayload }; const issue = payload.issue; + const issueContent = issue.body + issue.title; - //First Check if an issue with more than MATCH_THRESHOLD similarity exists (Very Similar) - const similarIssue = await supabase.issue.findSimilarIssues(issue.body + issue.title, MATCH_THRESHOLD, issue.node_id); - if (similarIssue && similarIssue?.length > 0) { - logger.info(`Similar issue which matches more than ${MATCH_THRESHOLD} already exists`); - //Close the issue as "unplanned" - await context.octokit.issues.update({ - owner: payload.repository.owner.login, - repo: payload.repository.name, - issue_number: issue.number, - state: "closed", - state_reason: "not_planned", - }); - return true; - } + // Fetch all similar issues based on WARNING_THRESHOLD + const similarIssues = await supabase.issue.findSimilarIssues(issueContent, WARNING_THRESHOLD, issue.node_id); + console.log(similarIssues); + if (similarIssues && similarIssues.length > 0) { + const matchIssues = similarIssues.filter((issue) => issue.similarity >= MATCH_THRESHOLD); - //Second Check if an issue with more than WARNING_THRESHOLD similarity exists (Warning) - const warningIssue = await supabase.issue.findSimilarIssues(issue.body + issue.title, WARNING_THRESHOLD, issue.node_id); - if (warningIssue && warningIssue?.length > 0) { - logger.info(`Similar issue which matches more than ${WARNING_THRESHOLD} already exists`); - //Add a comment immediately next to the issue - //Build a list of similar issues url - const issueList: IssueGraphqlResponse[] = await Promise.all( - warningIssue.map(async (issue: IssueSimilaritySearchResult) => { - //fetch the issue url and title using globalNodeId - const issueUrl: IssueGraphqlResponse = await context.octokit.graphql( - `query($issueNodeId: ID!) { - node(id: $issueNodeId) { - ... on Issue { - title - url - } - } - }`, - { - issueNodeId: issue.issue_id, - } - ); - issueUrl.similarity = (issue.similarity * 100).toFixed(2); - return issueUrl; - }) - ); - // Check if there is already a comment on the issue - const existingComment = await context.octokit.issues.listComments({ - owner: payload.repository.owner.login, - repo: payload.repository.name, - issue_number: issue.number, - }); - if (existingComment.data.length > 0) { - // Find the comment that lists the similar issues - const commentToUpdate = existingComment.data.find( - (comment) => comment && comment.body && comment.body.includes("This issue seems to be similar to the following issue(s)") - ); + // Handle issues that match the MATCH_THRESHOLD (Very Similar) + if (matchIssues.length > 0) { + logger.info(`Similar issue which matches more than ${MATCH_THRESHOLD} already exists`); + await octokit.issues.update({ + owner: payload.repository.owner.login, + repo: payload.repository.name, + issue_number: issue.number, + state: "closed", + state_reason: "not_planned", + }); + } - if (commentToUpdate) { - // Update the comment with the latest list of similar issues - const body = issueList.map((issue) => `- [${issue.node.title}](${issue.node.url}) Similarity: ${issue.similarity}`).join("\n"); - const updatedBody = `This issue seems to be similar to the following issue(s):\n\n${body}`; - await context.octokit.issues.updateComment({ - owner: payload.repository.owner.login, - repo: payload.repository.name, - comment_id: commentToUpdate.id, - body: updatedBody, - }); - } else { - // Add a new comment to the issue - await createNewComment(context, issueList); - } - } else { - // Add a new comment to the issue - await createNewComment(context, issueList); + // Handle issues that match the WARNING_THRESHOLD but not the MATCH_THRESHOLD + if (similarIssues.length > 0) { + logger.info(`Similar issue which matches more than ${WARNING_THRESHOLD} already exists`); + await handleSimilarIssuesComment(context, payload, issue.number, similarIssues); + return true; } - return true; } - logger.info("No similar issue found"); return false; } /** - * Create a new comment on the issue with the list of similar issues + * Handle commenting on an issue with similar issues information * @param context - * @param resolvedIssueList + * @param payload + * @param issueNumber + * @param similarIssues */ -async function createNewComment(context: Context, resolvedIssueList: IssueGraphqlResponse[]) { - let body = "This issue seems to be similar to the following issue(s):\n\n"; - resolvedIssueList.forEach((issue) => { - const issueLine = `- [${issue.node.title}](${issue.node.url}) Similarity: ${issue.similarity}\n`; - body += issueLine; - }); - await context.octokit.issues.createComment({ - owner: context.payload.repository.owner.login, - repo: context.payload.repository.name, - issue_number: context.payload.issue.number, - body: body, +async function handleSimilarIssuesComment(context: Context, payload: IssuePayload, issueNumber: number, similarIssues: IssueSimilaritySearchResult[]) { + const issueList: IssueGraphqlResponse[] = await Promise.all( + similarIssues.map(async (issue: IssueSimilaritySearchResult) => { + const issueUrl: IssueGraphqlResponse = await context.octokit.graphql( + `query($issueNodeId: ID!) { + node(id: $issueNodeId) { + ... on Issue { + title + url + } + } + }`, + { issueNodeId: issue.issue_id } + ); + issueUrl.similarity = (issue.similarity * 100).toFixed(2); + return issueUrl; + }) + ); + + const commentBody = issueList.map((issue) => `- [${issue.node.title}](${issue.node.url}) Similarity: ${issue.similarity}`).join("\n"); + const body = `This issue seems to be similar to the following issue(s):\n\n${commentBody}`; + + const existingComments = await context.octokit.issues.listComments({ + owner: payload.repository.owner.login, + repo: payload.repository.name, + issue_number: issueNumber, }); + + const existingComment = existingComments.data.find( + (comment) => comment.body && comment.body.includes("This issue seems to be similar to the following issue(s)") + ); + + if (existingComment) { + await context.octokit.issues.updateComment({ + owner: payload.repository.owner.login, + repo: payload.repository.name, + comment_id: existingComment.id, + body: body, + }); + } else { + await context.octokit.issues.createComment({ + owner: payload.repository.owner.login, + repo: payload.repository.name, + issue_number: issueNumber, + body: body, + }); + } } From 7609943c256dddc9fa1e05fbdb77eadd8e63b19e Mon Sep 17 00:00:00 2001 From: Shivaditya Shivganesh Date: Sat, 14 Sep 2024 17:10:11 -0400 Subject: [PATCH 8/8] feat: pass config through plugin settings --- src/handlers/issue-deduplication.ts | 15 ++++++--------- src/types/plugin-inputs.ts | 8 +++++++- tests/main.test.ts | 5 ++++- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/handlers/issue-deduplication.ts b/src/handlers/issue-deduplication.ts index 57b5d3e..2378a1e 100644 --- a/src/handlers/issue-deduplication.ts +++ b/src/handlers/issue-deduplication.ts @@ -2,9 +2,6 @@ import { IssueSimilaritySearchResult } from "../adapters/supabase/helpers/issues import { Context } from "../types"; import { IssuePayload } from "../types/payload"; -const MATCH_THRESHOLD = 0.95; -const WARNING_THRESHOLD = 0.75; - export interface IssueGraphqlResponse { node: { title: string; @@ -28,15 +25,15 @@ export async function issueChecker(context: Context): Promise { const issue = payload.issue; const issueContent = issue.body + issue.title; - // Fetch all similar issues based on WARNING_THRESHOLD - const similarIssues = await supabase.issue.findSimilarIssues(issueContent, WARNING_THRESHOLD, issue.node_id); + // Fetch all similar issues based on settings.warningThreshold + const similarIssues = await supabase.issue.findSimilarIssues(issueContent, context.config.warningThreshold, issue.node_id); console.log(similarIssues); if (similarIssues && similarIssues.length > 0) { - const matchIssues = similarIssues.filter((issue) => issue.similarity >= MATCH_THRESHOLD); + const matchIssues = similarIssues.filter((issue) => issue.similarity >= context.config.matchThreshold); // Handle issues that match the MATCH_THRESHOLD (Very Similar) if (matchIssues.length > 0) { - logger.info(`Similar issue which matches more than ${MATCH_THRESHOLD} already exists`); + logger.info(`Similar issue which matches more than ${context.config.matchThreshold} already exists`); await octokit.issues.update({ owner: payload.repository.owner.login, repo: payload.repository.name, @@ -46,9 +43,9 @@ export async function issueChecker(context: Context): Promise { }); } - // Handle issues that match the WARNING_THRESHOLD but not the MATCH_THRESHOLD + // Handle issues that match the settings.warningThreshold but not the MATCH_THRESHOLD if (similarIssues.length > 0) { - logger.info(`Similar issue which matches more than ${WARNING_THRESHOLD} already exists`); + logger.info(`Similar issue which matches more than ${context.config.warningThreshold} already exists`); await handleSimilarIssuesComment(context, payload, issue.number, similarIssues); return true; } diff --git a/src/types/plugin-inputs.ts b/src/types/plugin-inputs.ts index 77a40f9..a942db2 100644 --- a/src/types/plugin-inputs.ts +++ b/src/types/plugin-inputs.ts @@ -18,7 +18,13 @@ export interface PluginInputs