Skip to content

Commit 7c88973

Browse files
committed
merge: sync with upstream main
2 parents bce4311 + b251324 commit 7c88973

253 files changed

Lines changed: 48324 additions & 3536 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.local.example

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@
2020
# Generate new token (classic)
2121
# Required scope: read:user
2222
# ------------------------------------------------------------
23-
GITHUB_TOKEN=ghp_your_personal_access_token_here
24-
23+
GITHUB_TOKEN=
2524

2625
# ------------------------------------------------------------
2726
# REQUIRED — Public site URL

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* text=auto eol=lf
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
name: 🐛 Bug Report
2+
description: Create a report to help us improve CommitPulse
3+
title: 'bug: <short description of the issue>'
4+
labels: ['bug', 'triage']
5+
body:
6+
- type: markdown
7+
attributes:
8+
value: |
9+
Thank you for reporting an issue! Please fill out this form as completely as possible.
10+
11+
💡 **Pro-Tip:** Once you submit this issue, you can claim it for yourself by commenting `/claim`! Only you, as the author of the issue, have permission to claim it.
12+
- type: textarea
13+
id: description
14+
attributes:
15+
label: Description
16+
description: A clear and concise description of what the bug is.
17+
validations:
18+
required: true
19+
- type: textarea
20+
id: steps-to-reproduce
21+
attributes:
22+
label: Steps to Reproduce
23+
description: How can we reproduce this bug? Please be specific.
24+
placeholder: |
25+
1. Go to '...'
26+
2. Click on '....'
27+
3. Scroll down to '....'
28+
4. See error
29+
validations:
30+
required: true
31+
- type: textarea
32+
id: expected-behavior
33+
attributes:
34+
label: Expected Behavior
35+
description: What did you expect to happen?
36+
validations:
37+
required: true
38+
- type: textarea
39+
id: screenshots
40+
attributes:
41+
label: Screenshots / Logs
42+
description: If applicable, drag and drop screenshots or paste error logs here to help explain your problem.
43+
validations:
44+
required: false
45+
- type: input
46+
id: username
47+
attributes:
48+
label: GitHub Username (If applicable)
49+
description: The GitHub username you were trying to generate a badge for.
50+
validations:
51+
required: false
52+
- type: dropdown
53+
id: browser
54+
attributes:
55+
label: Environment
56+
description: What browser/environment are you experiencing this in?
57+
options:
58+
- Chrome
59+
- Firefox
60+
- Safari
61+
- Edge
62+
- GitHub Mobile App
63+
- Other
64+
validations:
65+
required: true

.github/ISSUE_TEMPLATE/config.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
blank_issues_enabled: false
2+
contact_links:
3+
- name: Ask a question or get support
4+
url: https://discord.gg/Cb73bS79j
5+
about: Please join our Discord server for general questions instead of opening an issue.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: ✨ Feature Request
2+
description: Suggest an idea for this project
3+
title: 'feat: <short description of the feature>'
4+
labels: ['enhancement', 'triage']
5+
body:
6+
- type: markdown
7+
attributes:
8+
value: |
9+
Thank you for suggesting an idea! Please explain your request in detail.
10+
11+
💡 **Pro-Tip:** Once you submit this issue, you can claim it for yourself by commenting `/claim`! Only you, as the author of the issue, have permission to claim it.
12+
- type: textarea
13+
id: problem
14+
attributes:
15+
label: Is your feature request related to a problem? Please describe.
16+
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
17+
validations:
18+
required: true
19+
- type: textarea
20+
id: solution
21+
attributes:
22+
label: Describe the solution you'd like
23+
description: A clear and concise description of what you want to happen.
24+
validations:
25+
required: true
26+
- type: textarea
27+
id: alternatives
28+
attributes:
29+
label: Describe alternatives you've considered
30+
description: A clear and concise description of any alternative solutions or features you've considered.
31+
validations:
32+
required: false

.github/scripts/README.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# 🤖 Gemini Semantic Duplicate Issue Detector
2+
3+
A production-ready custom GitHub Action that performs semantic search across open issues in your repository to detect and flag potential duplicates. Powered by the Google Gemini API `gemini-embedding-001` model.
4+
5+
---
6+
7+
## 📋 Features
8+
9+
- 🔍 **Semantic Search**: Understands the meaning of titles and descriptions, finding matches that simple keyword searches would miss.
10+
-**Rate-Limit Guard**: Automatically enforces a strict **4.1-second delay** between API requests to strictly respect the Gemini free tier limit (15 Requests Per Minute).
11+
- 🛡️ **Anti-Spam / Idempotency**: Inspects previous bot comments to avoid duplicate warnings on the same issue.
12+
- 🏷️ **Auto-Labeling**: Applies a `possible-duplicate` label to flagged issues.
13+
-**Manual Trigger**: Runs on-demand via the Actions tab using `workflow_dispatch` to control when resources and API keys are consumed.
14+
15+
---
16+
17+
## 🛠️ Setup Instructions
18+
19+
Follow these steps to integrate the duplicate detector into your repository.
20+
21+
### Step 1: Add Repository Secrets
22+
23+
To communicate with Google Gemini, you need a free Gemini API Key:
24+
25+
1. Go to [Google AI Studio](https://aistudio.google.com/) and generate a free API Key.
26+
2. Navigate to your GitHub Repository: **Settings****Secrets and variables****Actions****New repository secret**.
27+
3. Create a secret named **`GEMINI_API_KEY`** and paste your API key.
28+
29+
### Step 2: Configure Permissions
30+
31+
GitHub Actions need permission to write comments and add labels to issues:
32+
33+
1. Navigate to: **Settings****Actions****General**.
34+
2. Scroll to **Workflow permissions**.
35+
3. Select **Read and write permissions**.
36+
4. Click **Save**.
37+
38+
### Step 3: Run the Workflow
39+
40+
1. Navigate to the **Actions** tab of your repository.
41+
2. Select **Find Semantic Duplicates** from the left-hand sidebar.
42+
3. Click the **Run workflow** dropdown on the right and click the green button.
43+
44+
---
45+
46+
## 🤖 Example Bot Comment Format
47+
48+
When a duplicate is successfully detected (similarity score $\ge 0.85$), the bot will post a friendly, structured comment on the **newer issue**:
49+
50+
> Hey @jhasourav07! 🤖
51+
>
52+
> My semantic scan detected that this issue might be a duplicate of #42 (Similarity: **89.5%**).
53+
>
54+
> Please check between these issues and close this one if it is a duplicate.
55+
56+
---
57+
58+
## 🛡️ Robust Error Handling
59+
60+
The script includes several levels of robust, production-grade error handling:
61+
62+
- **Missing Keys**: Throws a explicit error if `GEMINI_API_KEY` or `GITHUB_TOKEN` is missing, immediately failing the step with clean diagnostic logs.
63+
- **Failures in Embeddings API**: Catch blocks log precise error diagnostics if a request to Gemini fails.
64+
- **Label Creation Safety**: Gracefully catches errors if the `possible-duplicate` label does not exist yet (or if the token lacks labeling permissions) to prevent the entire run from failing.
65+
66+
---
67+
68+
## ⚙️ Technical Details
69+
70+
### Cosine Similarity Engine
71+
72+
The cosine similarity calculation computes the dot product of normalized embedding vectors generated from your issue titles and descriptions:
73+
74+
$$\text{similarity} = \frac{A \cdot B}{\|A\| \|B\|}$$
75+
76+
This ensures mathematical precision across semantic features regardless of text length.
77+
78+
### Text Truncation
79+
80+
To conserve API tokens and prevent payload overflow, issue text is combined and truncated:
81+
82+
```javascript
83+
const textToEmbed = `Title: ${issue.title}\nBody: ${issue.body || ''}`.slice(0, 3000);
84+
```
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
const github = require('@actions/github');
2+
const { GoogleGenerativeAI } = require('@google/generative-ai');
3+
4+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
5+
6+
function cosineSimilarity(vecA, vecB) {
7+
let dotProduct = 0.0;
8+
let normA = 0.0;
9+
let normB = 0.0;
10+
for (let i = 0; i < vecA.length; i++) {
11+
dotProduct += vecA[i] * vecB[i];
12+
normA += vecA[i] * vecA[i];
13+
normB += vecB[i] * vecB[i];
14+
}
15+
if (normA === 0 || normB === 0) return 0;
16+
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
17+
}
18+
19+
async function run() {
20+
const geminiApiKey = process.env.GEMINI_API_KEY;
21+
const githubToken = process.env.GITHUB_TOKEN;
22+
23+
if (!geminiApiKey) {
24+
throw new Error('❌ Missing GEMINI_API_KEY environment variable.');
25+
}
26+
if (!githubToken) {
27+
throw new Error('❌ Missing GITHUB_TOKEN environment variable.');
28+
}
29+
30+
const octokit = github.getOctokit(githubToken);
31+
const { owner, repo } = github.context.repo;
32+
console.log(`🤖 Starting semantic scan for duplicates in ${owner}/${repo}...`);
33+
34+
// 1. Fetch all open issues in the repository
35+
console.log('Fetching all open issues...');
36+
const allIssues = await octokit.paginate(octokit.rest.issues.listForRepo, {
37+
owner,
38+
repo,
39+
state: 'open',
40+
per_page: 100,
41+
});
42+
43+
// Filter out Pull Requests
44+
const openIssues = allIssues.filter((issue) => !issue.pull_request);
45+
console.log(`Found ${openIssues.length} open issues (excluding Pull Requests).`);
46+
47+
if (openIssues.length < 2) {
48+
console.log('ℹ️ Not enough issues to compare. Ending scan.');
49+
return;
50+
}
51+
52+
// 2. Initialize Gemini API
53+
const genAI = new GoogleGenerativeAI(geminiApiKey);
54+
const model = genAI.getGenerativeModel({ model: 'gemini-embedding-001' });
55+
56+
// 3. Generate embeddings with 4.1s rate limit delay
57+
console.log('Generating semantic embeddings using gemini-embedding-001...');
58+
for (let i = 0; i < openIssues.length; i++) {
59+
const issue = openIssues[i];
60+
const textToEmbed = `Title: ${issue.title}\nBody: ${issue.body || ''}`.slice(0, 3000);
61+
62+
console.log(
63+
`[${i + 1}/${openIssues.length}] Embedding Issue #${issue.number}: "${issue.title.substring(0, 50)}..."`
64+
);
65+
66+
try {
67+
const result = await model.embedContent(textToEmbed);
68+
issue.embedding = result.embedding.values;
69+
} catch (err) {
70+
console.error(`❌ Failed to generate embedding for Issue #${issue.number}:`, err.message);
71+
throw err;
72+
}
73+
74+
if (i < openIssues.length - 1) {
75+
console.log('Sleeping for 4.1 seconds to avoid rate limits...');
76+
await delay(4100);
77+
}
78+
}
79+
80+
// 4. Perform pairwise comparisons
81+
console.log('\nComparing issues for semantic similarity...');
82+
let duplicatesCount = 0;
83+
84+
for (let i = 0; i < openIssues.length; i++) {
85+
for (let j = i + 1; j < openIssues.length; j++) {
86+
const issueA = openIssues[i];
87+
const issueB = openIssues[j];
88+
89+
const similarity = cosineSimilarity(issueA.embedding, issueB.embedding);
90+
91+
// Flag if similarity is 85% or higher
92+
if (similarity >= 0.85) {
93+
const newerIssue = issueA.number > issueB.number ? issueA : issueB;
94+
const olderIssue = issueA.number < issueB.number ? issueA : issueB;
95+
const similarityPercent = (similarity * 100).toFixed(1);
96+
97+
console.log(
98+
`⚠️ Possible Duplicate: #${newerIssue.number} and #${olderIssue.number} similarity: ${similarityPercent}%`
99+
);
100+
101+
// Check if bot already commented on the newer issue
102+
console.log(`Checking comments on newer issue #${newerIssue.number}...`);
103+
const { data: comments } = await octokit.rest.issues.listComments({
104+
owner,
105+
repo,
106+
issue_number: newerIssue.number,
107+
});
108+
109+
const alreadyFlagged = comments.some(
110+
(comment) =>
111+
comment.body &&
112+
comment.body.includes(
113+
`My semantic scan detected that this issue might be a duplicate of #${olderIssue.number}`
114+
)
115+
);
116+
117+
if (alreadyFlagged) {
118+
console.log(
119+
`ℹ️ Already flagged Issue #${newerIssue.number} as duplicate of #${olderIssue.number}. Skipping comment.`
120+
);
121+
continue;
122+
}
123+
124+
// Post duplicate warning comment
125+
const author = newerIssue.user.login;
126+
const commentBody = `Hey @${author}! 🤖\n\nMy semantic scan detected that this issue might be a duplicate of #${olderIssue.number} (Similarity: **${similarityPercent}%**).\n\nPlease check between these issues and close this one if it is a duplicate.`;
127+
128+
console.log(`Posting duplicate warning comment on newer issue #${newerIssue.number}...`);
129+
await octokit.rest.issues.createComment({
130+
owner,
131+
repo,
132+
issue_number: newerIssue.number,
133+
body: commentBody,
134+
});
135+
136+
// Add 'possible-duplicate' label to the newer issue
137+
try {
138+
console.log(`Adding 'possible-duplicate' label to issue #${newerIssue.number}...`);
139+
await octokit.rest.issues.addLabels({
140+
owner,
141+
repo,
142+
issue_number: newerIssue.number,
143+
labels: ['possible-duplicate'],
144+
});
145+
} catch (labelErr) {
146+
console.warn(
147+
`⚠️ Warning: Could not add label 'possible-duplicate' to Issue #${newerIssue.number}:`,
148+
labelErr.message
149+
);
150+
}
151+
152+
duplicatesCount++;
153+
}
154+
}
155+
}
156+
157+
console.log(`\n🎉 Semantic duplicate scan complete! Flagged ${duplicatesCount} new duplicates.`);
158+
}
159+
160+
run().catch((error) => {
161+
console.error('❌ Execution failed:', error.message);
162+
process.exit(1);
163+
});

0 commit comments

Comments
 (0)