diff --git a/.github/workflows/todo-tracking.yml b/.github/workflows/todo-tracking.yml new file mode 100644 index 000000000..69e47320f --- /dev/null +++ b/.github/workflows/todo-tracking.yml @@ -0,0 +1,100 @@ +name: TODOs Tracking + +on: + schedule: + # Runs at 00:00 UTC every day + - cron: "0 0 * * *" + workflow_dispatch: + +jobs: + update-todos: + name: Update TODOs and FIXMEs + runs-on: ubuntu-latest + permissions: + issues: write + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.x" + + - name: Generate TODOs list + run: python scripts/todos.py + + - name: Update issue and timestamp + uses: actions/github-script@v6 + with: + script: | + const fs = require('fs'); + const todoContent = fs.readFileSync('TODO.md', 'utf8'); + + // Find or create the tracking issue + const issues = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['todo-tracking'], + state: 'open' + }); + + let issueNumber; + if (issues.data.length === 0) { + const newIssue = await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: 'code: TODOs and FIXMES', + body: todoContent, + labels: ['todo-tracking'] + }); + issueNumber = newIssue.data.number; + + // Create initial timestamp comment + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: `Last updated: ${new Date().toISOString()}` + }); + } else { + issueNumber = issues.data[0].number; + + // Update issue content + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: todoContent + }); + + // Find and update the timestamp comment + const comments = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber + }); + + const timestampComment = comments.data.find(comment => + comment.body.startsWith('Last updated:') + ); + + if (timestampComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: timestampComment.id, + body: `Last updated: ${new Date().toISOString()}` + }); + } else { + // Create timestamp comment if it doesn't exist + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: `Last updated: ${new Date().toISOString()}` + }); + } + } diff --git a/scripts/todos.py b/scripts/todos.py new file mode 100644 index 000000000..83a546981 --- /dev/null +++ b/scripts/todos.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 + +import os +import re +import subprocess +from datetime import datetime + +def get_git_root(): + """Get the root directory of the git repository.""" + try: + root = subprocess.check_output(['git', 'rev-parse', '--show-toplevel']) + return root.strip().decode('utf-8') + except subprocess.CalledProcessError: + print("Error: Not a git repository") + exit(1) + +def get_remote_url(): + """Get the GitHub remote URL of the repository.""" + try: + remote = subprocess.check_output(['git', 'remote', 'get-url', 'origin']) + remote = remote.strip().decode('utf-8') + # Convert SSH URL to HTTPS if necessary + if remote.startswith('git@github.com:'): + remote = remote.replace('git@github.com:', 'https://github.com/') + if remote.endswith('.git'): + remote = remote[:-4] + return remote + except subprocess.CalledProcessError: + print("Error: Cannot get remote URL") + exit(1) + +def get_current_commit(): + """Get the current commit hash.""" + try: + commit = subprocess.check_output(['git', 'rev-parse', 'HEAD']) + return commit.strip().decode('utf-8') + except subprocess.CalledProcessError: + print("Error: Cannot get current commit hash") + exit(1) + +def find_todos(root_dir): + """Find all TODO and FIXME comments in the repository.""" + todos = [] + + # File extensions to search + extensions = ('.rs') + + for root, _, files in os.walk(root_dir): + if '.git' in root: + continue + + for file in files: + if file.endswith(extensions): + file_path = os.path.join(root, file) + relative_path = os.path.relpath(file_path, root_dir) + + try: + with open(file_path, 'r', encoding='utf-8') as f: + for line_num, line in enumerate(f, 1): + # Search for TODO or FIXME + if re.search(r'\b(TODO|FIXME)\b', line, re.IGNORECASE): + todos.append({ + 'file': relative_path, + 'line_num': line_num, + 'content': line.strip(), + 'type': 'TODO' if 'TODO' in line.upper() else 'FIXME' + }) + except UnicodeDecodeError: + continue + + return todos + +def generate_markdown(todos, remote_url, commit_hash): + """Generate markdown document from found TODOs and FIXMEs.""" + markdown = f"# TODOs and FIXMEs\n\n" + markdown += f"Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n" + markdown += f"Commit: [{commit_hash[:7]}]({remote_url}/commit/{commit_hash})\n\n" + + if not todos: + markdown += "No TODO or FIXME items found.\n" + return markdown + + # Separate TODOs and FIXMEs + todos_list = [t for t in todos if t['type'] == 'TODO'] + fixmes_list = [t for t in todos if t['type'] == 'FIXME'] + + if fixmes_list: + markdown += "## FIXMEs\n\n" + for fixme in fixmes_list: + file_link = f"{remote_url}/blob/{commit_hash}/{fixme['file']}#L{fixme['line_num']}" + markdown += f"- [{fixme['file']}:{fixme['line_num']}]({file_link})\n" + markdown += f" ```\n {fixme['content']}\n ```\n\n" + + if todos_list: + markdown += "## TODOs\n\n" + for todo in todos_list: + file_link = f"{remote_url}/blob/{commit_hash}/{todo['file']}#L{todo['line_num']}" + markdown += f"- [{todo['file']}:{todo['line_num']}]({file_link})\n" + markdown += f" ```\n {todo['content']}\n ```\n\n" + + return markdown + +def main(): + root_dir = get_git_root() + remote_url = get_remote_url() + commit_hash = get_current_commit() + + todos = find_todos(root_dir) + markdown = generate_markdown(todos, remote_url, commit_hash) + + # Write to file + output_file = os.path.join(root_dir, 'TODO.md') + with open(output_file, 'w', encoding='utf-8') as f: + f.write(markdown) + + print(f"Generated TODO.md with {len(todos)} items") + print(f"Current commit: {commit_hash[:7]}") + +if __name__ == '__main__': + main()