Skip to content

Commit 7d46c35

Browse files
Merge pull request #189 from DiogoRibeiro7/codex/melhorar-o-projeto
Support Gemini LLM provider
2 parents 9842768 + 40a6914 commit 7d46c35

18 files changed

+457
-52
lines changed

.github/workflows/run_tests.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ jobs:
1313
steps:
1414
- name: Checkout code
1515
uses: actions/checkout@v4
16+
with:
17+
fetch-depth: 0
1618

1719
- name: Set up Node.js
1820
uses: actions/setup-node@v4
@@ -24,6 +26,9 @@ jobs:
2426

2527
- name: Run tests with coverage
2628
run: yarn vitest run --coverage
29+
30+
- name: Check package version matches tag
31+
run: yarn check-version
2732

2833

2934
# - name: Upload coverage to Codecov

README.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ A GitHub Action that scans your codebase for inline TODOs, FIXMEs, and BUG comme
77
## 🚀 Features
88

99
- ✅ Detects `TODO`, `FIXME`, `BUG`, and `HACK` comments
10-
- ✅ Supports multiple languages: `.ts`, `.js`, `.py`, `.go`, `.html`, etc.
10+
- ✅ Supports many languages: `.ts`, `.js`, `.py`, `.go`, `.c`, `.cpp`, `.rs`, `.html`, `.yaml`, etc.
1111
- ✅ Skips common directories like `node_modules`, `dist`, and `coverage`
1212
- ✅ Extracts metadata like `priority`, `due`, etc.
1313
- ✅ Parses structured tags (`@assignee`, `#module`, `key=value`)
@@ -17,6 +17,8 @@ A GitHub Action that scans your codebase for inline TODOs, FIXMEs, and BUG comme
1717
- ✅ Supports custom label colors and descriptions via JSON config
1818
- ✅ Custom templates for issue titles and bodies
1919
- ✅ LLM-powered issue title and body generation
20+
- ✅ Automatic retry logic for OpenAI API calls
21+
- ✅ Supports multiple LLM providers: OpenAI or Gemini
2022
- ✅ Command-line interface for local usage
2123
- ✅ Optional Jira synchronization
2224

@@ -55,8 +57,12 @@ jobs:
5557
with:
5658
repo-token: ${{ secrets.GITHUB_TOKEN }}
5759
limit: 5
60+
llm: true
61+
llm-provider: openai # or 'gemini'
5862
```
5963
64+
Set `OPENAI_API_KEY` or `GEMINI_API_KEY` secrets based on your chosen provider.
65+
6066
### 2. Run the CLI locally
6167

6268
Use the bundled command-line interface to scan a directory on your machine and
@@ -159,3 +165,12 @@ smart-todo-action/
159165
├── tsconfig.json
160166
└── README.md
161167
```
168+
169+
## 🔖 Versioning
170+
171+
The `check-version` script ensures the `package.json` version matches the
172+
current Git tag. It runs in CI and can be invoked locally with:
173+
174+
```bash
175+
yarn check-version
176+
```

action.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ inputs:
3939
description: Use LLM to generate issue titles and bodies
4040
default: 'false'
4141

42+
llm-provider:
43+
required: false
44+
description: LLM provider to use (`openai` or `gemini`)
45+
default: openai
46+
4247
openai-api-key:
4348
required: false
4449
description: 'OpenAI API key used when `llm` is true'
@@ -48,6 +53,15 @@ inputs:
4853
description: OpenAI model to use (e.g., `gpt-3.5-turbo`, `gpt-4`)
4954
default: gpt-3.5-turbo
5055

56+
gemini-api-key:
57+
required: false
58+
description: Gemini API key used when `llm-provider` is `gemini`
59+
60+
gemini-model:
61+
required: false
62+
description: Gemini model to use (e.g., `gemini-1.5-pro`)
63+
default: gemini-1.5-pro
64+
5165
sync-to-jira:
5266
required: false
5367
default: 'false'

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "smart-todo-action",
3-
"version": "0.1.0",
3+
"version": "1.0.0",
44
"description": "GitHub Action inteligente para transformar TODOs em issues e tarefas rastreáveis.",
55
"main": "dist/index.js",
66
"bin": {
@@ -11,7 +11,8 @@
1111
"test": "vitest run",
1212
"prepare": "yarn ncc build src/ActionMain.ts -o dist",
1313
"changelog": "ts-node scripts/generateChangelog.ts",
14-
"build:dist": "ncc build src/ActionMain.ts -o dist"
14+
"build:dist": "ncc build src/ActionMain.ts -o dist",
15+
"check-version": "ts-node scripts/checkVersionTag.ts"
1516
},
1617
"keywords": [
1718
"github-action",
@@ -26,6 +27,7 @@
2627
"dependencies": {
2728
"@actions/core": "^1.10.0",
2829
"@actions/github": "^6.0.1",
30+
"@google/genai": "^1.4.0",
2931
"@jest/globals": "^29.7.0",
3032
"@octokit/rest": "^21.1.1",
3133
"@types/jest": "^29.5.14",

scripts/checkVersionTag.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { readFileSync } from 'fs';
2+
import { execSync } from 'child_process';
3+
4+
const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
5+
6+
function getGitTag(): string | null {
7+
const envTag = process.env.GITHUB_REF?.startsWith('refs/tags/')
8+
? process.env.GITHUB_REF.replace('refs/tags/', '')
9+
: undefined;
10+
if (envTag) return envTag;
11+
try {
12+
return execSync('git describe --tags --exact-match').toString().trim();
13+
} catch {
14+
return null;
15+
}
16+
}
17+
18+
const tag = getGitTag();
19+
if (!tag) {
20+
console.log('No git tag found; skipping version check');
21+
process.exit(0);
22+
}
23+
24+
const normalizedTag = tag.startsWith('v') ? tag.slice(1) : tag;
25+
if (pkg.version !== normalizedTag) {
26+
console.error(
27+
`Version mismatch: package.json is ${pkg.version} but tag is ${tag}`
28+
);
29+
process.exit(1);
30+
}
31+
32+
console.log(`\u2714\ufe0f package.json version ${pkg.version} matches tag ${tag}`);

src/ActionMain.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,20 @@ async function run(): Promise<void> {
2323
const workspace = process.env.GITHUB_WORKSPACE || '.';
2424

2525
// LLM support
26-
process.env.OPENAI_API_KEY = core.getInput('openai-api-key') || process.env.OPENAI_API_KEY;
26+
const llmProvider = core.getInput('llm-provider') || 'openai';
27+
process.env.LLM_PROVIDER = llmProvider;
28+
if (llmProvider === 'gemini') {
29+
process.env.GEMINI_API_KEY = core.getInput('gemini-api-key') || process.env.GEMINI_API_KEY;
30+
} else {
31+
process.env.OPENAI_API_KEY = core.getInput('openai-api-key') || process.env.OPENAI_API_KEY;
32+
}
2733
const useLLM = core.getInput('llm') === 'true';
28-
if (useLLM && !process.env.OPENAI_API_KEY) {
34+
if (useLLM && llmProvider === 'openai' && !process.env.OPENAI_API_KEY) {
2935
core.warning('⚠️ LLM is enabled, but OPENAI_API_KEY is not set.');
3036
}
37+
if (useLLM && llmProvider === 'gemini' && !process.env.GEMINI_API_KEY) {
38+
core.warning('⚠️ LLM is enabled, but GEMINI_API_KEY is not set.');
39+
}
3140

3241
const useStructured = core.getInput('structured') === 'true';
3342

src/core/llm/generateIssueContent.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
// src/core/llm/generateIssueContent.ts
22
import { TodoItem } from '../../parser/types';
3-
import OpenAI from 'openai';
43
import * as core from '@actions/core';
4+
import { chatCompletionWithRetry } from './llmClient';
55

6-
const openai = new OpenAI({
7-
apiKey: core.getInput('openai-api-key'), // correto agora
8-
});
9-
10-
const model = core.getInput('openai-model') || 'gpt-3.5-turbo';
6+
const provider = core.getInput('llm-provider') || 'openai';
7+
const model =
8+
provider === 'gemini'
9+
? core.getInput('gemini-model') || 'gemini-1.5-pro'
10+
: core.getInput('openai-model') || 'gpt-3.5-turbo';
1111

1212
export async function generateIssueTitleAndBodyLLM(todo: TodoItem): Promise<{ title: string; body: string }> {
1313
const prompt = `
@@ -27,17 +27,20 @@ TITLE: <title>
2727
BODY:
2828
<detailed body>
2929
`;
30-
// 👇 Adiciona aqui
31-
core.debug(`[DEBUG] OpenAI key starts with: ${process.env.OPENAI_API_KEY?.slice(0, 5)}`);
30+
core.debug(`[DEBUG] LLM provider: ${provider}`);
31+
if (provider === 'openai') {
32+
core.debug(`[DEBUG] OpenAI key starts with: ${process.env.OPENAI_API_KEY?.slice(0, 5)}`);
33+
} else {
34+
core.debug(`[DEBUG] Gemini key starts with: ${process.env.GEMINI_API_KEY?.slice(0, 5)}`);
35+
}
3236
core.debug(`[DEBUG] Using model: ${model}`);
33-
core.debug('[DEBUG] Sending prompt to OpenAI...');
37+
core.debug('[DEBUG] Sending prompt to LLM...');
3438
try {
35-
const response = await openai.chat.completions.create({
39+
const response = await chatCompletionWithRetry({
3640
model,
3741
messages: [{ role: 'user', content: prompt }],
3842
temperature: 0.4,
3943
});
40-
// TODO(priority=high): improve retry logic for API errors
4144
const result = response.choices[0].message?.content || '';
4245
const match = result.match(/TITLE:\s*(.+?)\s*BODY:\s*([\s\S]*)/i);
4346

@@ -48,7 +51,7 @@ try {
4851
const [, title, body] = match;
4952
return { title: title.trim(), body: body.trim() };
5053
} catch (err: any) {
51-
console.error('[ERROR] OpenAI call failed:', err);
54+
console.error('[ERROR] LLM call failed:', err);
5255
throw err;
5356
}
5457
}

src/core/llm/llmClient.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// src/core/llm/openaiClient.ts
2+
import OpenAI from 'openai';
3+
import { GoogleGenAI } from '@google/genai';
4+
import * as core from '@actions/core';
5+
6+
const provider = core.getInput('llm-provider') || 'openai';
7+
8+
export const openai = new OpenAI({
9+
apiKey: core.getInput('openai-api-key'),
10+
});
11+
12+
export const gemini = new GoogleGenAI({
13+
apiKey: core.getInput('gemini-api-key'),
14+
});
15+
16+
/**
17+
* Wraps `openai.chat.completions.create` with simple retry logic.
18+
* Retries on failure with exponential backoff.
19+
*
20+
* @param params Parameters forwarded to OpenAI
21+
* @param maxRetries Maximum number of attempts before throwing
22+
*/
23+
export interface ChatCompletionParams {
24+
model: string;
25+
messages: { role: string; content: string }[];
26+
temperature?: number;
27+
}
28+
29+
export async function chatCompletionWithRetry(
30+
params: ChatCompletionParams,
31+
maxRetries = 3
32+
): Promise<{ choices: { message: { content: string } }[] }> {
33+
let attempt = 0;
34+
for (;;) {
35+
try {
36+
if (provider === 'gemini') {
37+
const prompt = params.messages.map(m => m.content).join('\n');
38+
const response = await gemini.models.generateContent({
39+
model: params.model,
40+
contents: prompt,
41+
generationConfig: { temperature: params.temperature },
42+
} as any);
43+
return { choices: [{ message: { content: (response as any).text } }] } as any;
44+
}
45+
return (await openai.chat.completions.create(params as any)) as any;
46+
} catch (err) {
47+
attempt++;
48+
if (attempt > maxRetries) throw err;
49+
const delay = Math.min(1000 * 2 ** attempt, 5000);
50+
core.warning(`LLM request failed (attempt ${attempt}). Retrying in ${delay}ms...`);
51+
await new Promise(res => setTimeout(res, delay));
52+
}
53+
}
54+
}

src/core/llm/openaiClient.ts

Lines changed: 0 additions & 7 deletions
This file was deleted.

src/parser/extractTodos.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import { TodoItem } from './types';
44
import { normalizeTag } from '../utils/isTextFile';
55

66
const COMMENT_PATTERNS = [
7-
{ ext: ['.ts', '.js', '.java', '.go'], pattern: /^\s*\/\/\s*(.*)$/ },
8-
{ ext: ['.py', '.sh', '.rb'], pattern: /^\s*#\s*(.*)$/ },
7+
{ ext: ['.ts', '.js', '.java', '.go', '.c', '.cpp', '.cs', '.rs', '.php', '.h', '.hpp'], pattern: /^\s*\/\/\s*(.*)$/ },
8+
{ ext: ['.py', '.sh', '.rb', '.yaml', '.yml'], pattern: /^\s*#\s*(.*)$/ },
99
{ ext: ['.html', '.xml'], pattern: /<!--\s*(.*?)\s*-->/ }
1010
];
1111

0 commit comments

Comments
 (0)