Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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: 3 additions & 1 deletion .github/workflows/todo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ jobs:
issue-title-template: src/templates/issueTitle.txt
issue-body-template: src/templates/issueBody.md
report: true
llm: true
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}

- name: Upload TODO report
uses: actions/upload-artifact@v4
Expand All @@ -46,5 +49,4 @@ jobs:
git add TODO_REPORT.md CHANGELOG.md
git diff --cached --quiet && echo "No changes to commit." || git commit -m "chore(report): update TODO report and changelog [skip ci]"
git push
5 changes: 1 addition & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
node_modules/
dist/
# dist/
.env


TODO_REPORT.md
10 changes: 9 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,19 @@ inputs:
required: false
description: Optional path to custom issue body template

llm:
required: false
description: Use LLM to generate issue titles and bodies
default: 'false'

openai-api-key:
required: false
description: OpenAI API key used when `llm` is true

runs:
using: 'node20'
main: 'dist/index.js'

branding:
icon: 'check-circle'
color: 'blue'

1 change: 1 addition & 0 deletions dist/ActionMain.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
75 changes: 75 additions & 0 deletions dist/ActionMain.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
const core = __importStar(require("@actions/core"));
const github = __importStar(require("@actions/github"));
const extractTodosFromDir_1 = require("./parser/extractTodosFromDir");
const issueManager_1 = require("./core/issueManager");
const report_1 = require("./core/report");
const todoUtils_1 = require("./core/todoUtils");
async function run() {
try {
const token = core.getInput('repo-token', { required: true });
const generateReport = core.getInput('report') === 'true';
const titleTemplatePath = core.getInput('issue-title-template');
const bodyTemplatePath = core.getInput('issue-body-template');
const workspace = process.env.GITHUB_WORKSPACE || '.';
const todos = (0, extractTodosFromDir_1.extractTodosFromDir)(workspace);
const octokit = github.getOctokit(token);
const { owner, repo } = github.context.repo;
core.info(`🔍 Found ${todos.length} TODOs`);
const existingTitles = await (0, issueManager_1.getExistingIssueTitles)(octokit, owner, repo);
const seenKeys = new Set();
const uniqueTodos = todos.filter(todo => {
const key = (0, todoUtils_1.todoKey)(todo);
if (seenKeys.has(key))
return false;
seenKeys.add(key);
return true;
});
const todosToCreate = (0, todoUtils_1.limitTodos)(uniqueTodos, 5);
for (const todo of todosToCreate) {
await (0, issueManager_1.createIssueIfNeeded)(octokit, owner, repo, todo, existingTitles, titleTemplatePath, bodyTemplatePath);
}
if (generateReport) {
(0, report_1.generateMarkdownReport)(todos);
core.info('📝 Generated TODO_REPORT.md');
}
}
catch (error) {
core.setFailed(`Action failed: ${error.message}`);
}
}
run();
1 change: 1 addition & 0 deletions dist/core/__tests__._/applyTemplate.test.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
1 change: 1 addition & 0 deletions dist/core/__tests__._/classifier.test.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
1 change: 1 addition & 0 deletions dist/core/__tests__._/commentPatterns.test.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
1 change: 1 addition & 0 deletions dist/core/__tests__._/extractTodos.test.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
1 change: 1 addition & 0 deletions dist/core/__tests__._/extractTodosFromContent.test.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
1 change: 1 addition & 0 deletions dist/core/__tests__._/extractTodosFromDir.test.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
1 change: 1 addition & 0 deletions dist/core/__tests__._/fixtures/one-file.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export declare const a = 42;
1 change: 1 addition & 0 deletions dist/core/__tests__._/issueManager.test.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
1 change: 1 addition & 0 deletions dist/core/__tests__._/labelManager.test.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
1 change: 1 addition & 0 deletions dist/core/__tests__/applyTemplate.test.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
32 changes: 32 additions & 0 deletions dist/core/__tests__/applyTemplate.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../../templates/utils");
const vitest_1 = require("vitest");
(0, vitest_1.describe)('applyTemplate', () => {
(0, vitest_1.it)('replaces simple variables', () => {
const template = '[{{tag}}] {{text}}';
const data = {
tag: 'TODO',
text: 'Implement login flow'
};
const result = (0, utils_1.applyTemplate)(template, data);
(0, vitest_1.expect)(result).toBe('[TODO] Implement login flow');
});
(0, vitest_1.it)('ignores missing variables', () => {
const template = 'Priority: {{priority}}';
const data = {
tag: 'TODO'
};
const result = (0, utils_1.applyTemplate)(template, data);
(0, vitest_1.expect)(result).toBe('Priority: ');
});
(0, vitest_1.it)('handles numeric values', () => {
const template = 'Line {{line}}: {{text}}';
const data = {
line: 42,
text: 'Optimize loop'
};
const result = (0, utils_1.applyTemplate)(template, data);
(0, vitest_1.expect)(result).toBe('Line 42: Optimize loop');
});
});
1 change: 1 addition & 0 deletions dist/core/__tests__/classifier.test.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
40 changes: 40 additions & 0 deletions dist/core/__tests__/classifier.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const vitest_1 = require("vitest");
const classifier_1 = require("../classifier");
(0, vitest_1.describe)('classifyTodoText', () => {
(0, vitest_1.it)('classifies refactor-related TODOs', () => {
const text = 'Refactor this function to improve readability';
(0, vitest_1.expect)((0, classifier_1.classifyTodoText)(text)).toContain('refactor');
});
(0, vitest_1.it)('classifies test-related TODOs', () => {
const text = 'Add unit tests for edge cases';
(0, vitest_1.expect)((0, classifier_1.classifyTodoText)(text)).toContain('test');
});
(0, vitest_1.it)('classifies doc-related TODOs', () => {
const text = 'Document this method and its arguments';
(0, vitest_1.expect)((0, classifier_1.classifyTodoText)(text)).toContain('doc');
});
(0, vitest_1.it)('classifies performance-related TODOs', () => {
const text = 'Optimize this query to reduce latency';
(0, vitest_1.expect)((0, classifier_1.classifyTodoText)(text)).toContain('performance');
});
(0, vitest_1.it)('classifies security-related TODOs', () => {
const text = 'Check for injection vulnerabilities';
(0, vitest_1.expect)((0, classifier_1.classifyTodoText)(text)).toContain('security');
});
(0, vitest_1.it)('classifies maintenance-related TODOs', () => {
const text = 'Deprecate old API and migrate to v2';
(0, vitest_1.expect)((0, classifier_1.classifyTodoText)(text)).toContain('maintenance');
});
(0, vitest_1.it)('returns empty array for unrelated TODOs', () => {
const text = 'Ask John about next steps';
(0, vitest_1.expect)((0, classifier_1.classifyTodoText)(text)).toEqual([]);
});
(0, vitest_1.it)('handles mixed case and accents', () => {
const text = 'Rewrite legacy logic and add docs';
const labels = (0, classifier_1.classifyTodoText)(text);
(0, vitest_1.expect)(labels).toContain('refactor');
(0, vitest_1.expect)(labels).toContain('doc');
});
});
1 change: 1 addition & 0 deletions dist/core/__tests__/commentPatterns.test.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
41 changes: 41 additions & 0 deletions dist/core/__tests__/commentPatterns.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const vitest_1 = require("vitest");
const extractTodosFromContent_1 = require("../../parser/extractTodosFromContent");
(0, vitest_1.describe)('extractTodosFromString - comment support by extension', () => {
(0, vitest_1.it)('extracts from JS-style (//) for .js/.ts/.go/.java', () => {
const code = `// TODO: js comment\n// BUG: broken`;
const extensions = ['.js', '.ts', '.go', '.java'];
for (const ext of extensions) {
const todos = (0, extractTodosFromContent_1.extractTodosFromString)(code, ext);
(0, vitest_1.expect)(todos.length).toBe(2);
(0, vitest_1.expect)(todos[0].tag).toBe('TODO');
(0, vitest_1.expect)(todos[1].tag).toBe('BUG');
}
});
(0, vitest_1.it)('extracts from Python-style (#) for .py/.sh/.rb', () => {
const code = `# TODO: python comment\n# FIXME: fix me`;
const extensions = ['.py', '.sh', '.rb'];
for (const ext of extensions) {
const todos = (0, extractTodosFromContent_1.extractTodosFromString)(code, ext);
(0, vitest_1.expect)(todos.length).toBe(2);
(0, vitest_1.expect)(todos[0].tag).toBe('TODO');
(0, vitest_1.expect)(todos[1].tag).toBe('FIXME');
}
});
(0, vitest_1.it)('extracts from HTML-style (<!-- -->) for .html/.xml', () => {
const code = `<!-- TODO: html fix -->\n<!-- HACK: temp hack -->`;
const extensions = ['.html', '.xml'];
for (const ext of extensions) {
const todos = (0, extractTodosFromContent_1.extractTodosFromString)(code, ext);
(0, vitest_1.expect)(todos.length).toBe(2);
(0, vitest_1.expect)(todos[0].tag).toBe('TODO');
(0, vitest_1.expect)(todos[1].tag).toBe('HACK');
}
});
(0, vitest_1.it)('returns [] for unsupported extensions', () => {
const code = `// TODO: will not be parsed`;
const todos = (0, extractTodosFromContent_1.extractTodosFromString)(code, '.txt');
(0, vitest_1.expect)(todos).toEqual([]);
});
});
1 change: 1 addition & 0 deletions dist/core/__tests__/extractTodos.test.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
40 changes: 40 additions & 0 deletions dist/core/__tests__/extractTodos.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const vitest_1 = require("vitest");
const extractTodosFromContent_1 = require("../../parser/extractTodosFromContent");
(0, vitest_1.describe)('extractTodos', () => {
(0, vitest_1.it)('extracts simple TODOs with //', () => {
const content = `// TODO: clean this up\nconst a = 1;`;
const todos = (0, extractTodosFromContent_1.extractTodosFromString)(content, '.js');
(0, vitest_1.expect)(todos.length).toBe(1);
(0, vitest_1.expect)(todos[0].text).toBe('clean this up');
(0, vitest_1.expect)(todos[0].tag).toBe('TODO');
(0, vitest_1.expect)(todos[0].line).toBe(1);
});
(0, vitest_1.it)('extracts multiple tags', () => {
const content = `# BUG: crashes\n# FIXME: something wrong`;
const todos = (0, extractTodosFromContent_1.extractTodosFromString)(content, '.py');
(0, vitest_1.expect)(todos.length).toBe(2);
(0, vitest_1.expect)(todos.map(t => t.tag)).toEqual(['BUG', 'FIXME']);
});
(0, vitest_1.it)('extracts metadata key=value pairs', () => {
const content = `// TODO(priority=high, due=2025-06-01): fix it`;
const todos = (0, extractTodosFromContent_1.extractTodosFromString)(content, '.js');
(0, vitest_1.expect)(todos.length).toBe(1);
(0, vitest_1.expect)(todos[0].metadata).toEqual({
priority: 'high',
due: '2025-06-01'
});
});
(0, vitest_1.it)('supports HTML comments', () => {
const content = `<!-- TODO: fix layout -->`;
const todos = (0, extractTodosFromContent_1.extractTodosFromString)(content, '.html');
(0, vitest_1.expect)(todos.length).toBe(1);
(0, vitest_1.expect)(todos[0].tag).toBe('TODO');
});
(0, vitest_1.it)('returns empty list if no TODOs are found', () => {
const content = `const x = 5; // just a comment`;
const todos = (0, extractTodosFromContent_1.extractTodosFromString)(content, '.js');
(0, vitest_1.expect)(todos.length).toBe(0);
});
});
1 change: 1 addition & 0 deletions dist/core/__tests__/extractTodosFromContent.test.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
27 changes: 27 additions & 0 deletions dist/core/__tests__/extractTodosFromContent.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const extractTodosFromContent_1 = require("../../parser/extractTodosFromContent");
const vitest_1 = require("vitest");
(0, vitest_1.describe)('extractTodosFromString', () => {
(0, vitest_1.it)('extracts multiple TODO-style tags', () => {
const content = `// BUG: crash here\n# FIXME: wrong\n<!-- TODO: layout -->`;
const jsTodos = (0, extractTodosFromContent_1.extractTodosFromString)(content, '.js');
const pyTodos = (0, extractTodosFromContent_1.extractTodosFromString)(content, '.py');
const htmlTodos = (0, extractTodosFromContent_1.extractTodosFromString)(content, '.html');
(0, vitest_1.expect)(jsTodos.length).toBe(1);
(0, vitest_1.expect)(jsTodos[0].tag).toBe('BUG');
(0, vitest_1.expect)(pyTodos.length).toBe(1);
(0, vitest_1.expect)(pyTodos[0].tag).toBe('FIXME');
(0, vitest_1.expect)(htmlTodos.length).toBe(1);
(0, vitest_1.expect)(htmlTodos[0].tag).toBe('TODO');
});
(0, vitest_1.it)('extracts metadata key=value pairs', () => {
const content = `// TODO(priority=high, due=2025-06-01): fix it`;
const todos = (0, extractTodosFromContent_1.extractTodosFromString)(content, '.js');
(0, vitest_1.expect)(todos.length).toBe(1);
(0, vitest_1.expect)(todos[0].metadata).toEqual({
priority: 'high',
due: '2025-06-01'
});
});
});
1 change: 1 addition & 0 deletions dist/core/__tests__/extractTodosFromDir.test.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
28 changes: 28 additions & 0 deletions dist/core/__tests__/extractTodosFromDir.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const vitest_1 = require("vitest");
const path_1 = __importDefault(require("path"));
const extractTodosFromDir_1 = require("../../parser/extractTodosFromDir");
(0, vitest_1.describe)('extractTodosFromDir', () => {
const base = path_1.default.join(__dirname, 'fixtures');
(0, vitest_1.it)('should extract TODOs from supported files recursively', () => {
const todos = (0, extractTodosFromDir_1.extractTodosFromDir)(base);
(0, vitest_1.expect)(todos.length).toBe(2);
const texts = todos.map(t => t.text);
(0, vitest_1.expect)(texts).toContain('Refactor this module');
(0, vitest_1.expect)(texts).toContain('Handle edge case');
const tags = todos.map(t => t.tag);
(0, vitest_1.expect)(tags).toContain('TODO');
(0, vitest_1.expect)(tags).toContain('FIXME');
});
(0, vitest_1.it)('should include correct file and line information', () => {
const todos = (0, extractTodosFromDir_1.extractTodosFromDir)(base);
const one = todos.find(t => t.text.includes('Refactor'));
(0, vitest_1.expect)(one?.file.endsWith('one-file.ts')).toBe(true);
(0, vitest_1.expect)(typeof one?.line).toBe('number');
(0, vitest_1.expect)(one?.line).toBeGreaterThan(0);
});
});
1 change: 1 addition & 0 deletions dist/core/__tests__/fixtures/one-file.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export declare const a = 42;
5 changes: 5 additions & 0 deletions dist/core/__tests__/fixtures/one-file.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.a = void 0;
// TODO: Refactor this module
exports.a = 42;
1 change: 1 addition & 0 deletions dist/core/__tests__/issueManager.test.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
Loading