diff --git a/.github/scripts/README.md b/.github/scripts/README.md new file mode 100644 index 0000000000..fd58e4569f --- /dev/null +++ b/.github/scripts/README.md @@ -0,0 +1,135 @@ +# RooCode Agent - GitHub Actions Bot + +This directory contains the RooCode Agent bot that runs in GitHub Actions to automatically handle issues and pull requests. + +## Features + +The RooCode Agent bot supports the following commands: + +### Issue Commands + +- `/roo` - General command to interact with the bot +- `/roo plan` - Create a detailed implementation plan for an issue +- `/roo approve` - Approve a plan and start implementation +- `/roo fix` - Directly implement a fix for an issue +- `/roo triage` - Analyze and triage an issue (priority, labels, complexity) +- `/roo label` - Automatically add appropriate labels to an issue + +### Pull Request Commands + +- `/roo review` - Perform a code review on a pull request +- `/roo` - General interaction with the bot on PRs + +## Setup Instructions + +### 1. Repository Secrets + +Configure the following secrets in your repository settings (`Settings > Secrets and variables > Actions`): + +**Required (at least one):** + +- `OPENAI_API_KEY` - OpenAI API key for GPT models +- `ANTHROPIC_API_KEY` - Anthropic API key for Claude models +- `OPENROUTER_API_KEY` - OpenRouter API key for various models + +**Automatic:** + +- `GITHUB_TOKEN` - Automatically provided by GitHub Actions + +### 2. Repository Variables + +Configure these variables in your repository settings (`Settings > Secrets and variables > Actions > Variables`): + +- `MODEL_PROVIDER` - Choose: `openai`, `anthropic`, or `openrouter` (default: `anthropic`) +- `MODEL_NAME` - Model to use (default: `claude-3-5-sonnet-20241022`) +- `MAX_TOKENS` - Maximum tokens for responses (default: `8192`) +- `TEMPERATURE` - Model temperature 0-1 (default: `0.2`) + +### 3. Enable GitHub Actions + +1. Go to `Settings > Actions > General` +2. Under "Workflow permissions", select "Read and write permissions" +3. Check "Allow GitHub Actions to create and approve pull requests" +4. Save the settings + +### 4. Test the Bot + +Create an issue or comment with `/roo` to trigger the bot: + +``` +/roo plan + +Please help me implement a new feature for user authentication. +``` + +## Workflow Triggers + +The bot is triggered by: + +- **Issue events**: When issues are opened or edited containing `/roo` +- **Issue comments**: When comments are created or edited containing `/roo` +- **Pull request events**: When PRs are opened, edited, or synchronized containing `/roo` +- **PR review comments**: When review comments are created or edited containing `/roo` +- **Manual dispatch**: Can be triggered manually from the Actions tab + +## Architecture + +``` +.github/ +├── workflows/ +│ └── roocode-bot.yml # GitHub Actions workflow definition +└── scripts/ + ├── roocode-agent.ts # Main bot logic + ├── package.json # Dependencies + └── README.md # This file +``` + +## Development + +To test locally: + +```bash +cd .github/scripts +npm install +npm start +``` + +Set environment variables: + +```bash +export GITHUB_TOKEN=your_token +export ANTHROPIC_API_KEY=your_key +export GITHUB_EVENT_PATH=path/to/event.json +export GITHUB_REPOSITORY=owner/repo +export GITHUB_EVENT_NAME=issues +``` + +## Security Considerations + +- API keys are stored as encrypted secrets +- The bot only has permissions granted by the `GITHUB_TOKEN` +- All actions are logged in GitHub Actions +- The bot identifies itself in all comments + +## Extending the Bot + +To add new commands: + +1. Add the command detection in `extractCommands()` +2. Implement the handler function +3. Add the case in `processIssue()` or `processPullRequest()` +4. Update this README with the new command + +## Troubleshooting + +Check the GitHub Actions logs: + +1. Go to the "Actions" tab in your repository +2. Click on the "RooCode Agent Bot" workflow +3. Select a run to view detailed logs + +Common issues: + +- Missing API keys: Check repository secrets +- Insufficient permissions: Check workflow permissions +- Model errors: Verify MODEL_PROVIDER and MODEL_NAME variables diff --git a/.github/scripts/package.json b/.github/scripts/package.json new file mode 100644 index 0000000000..487fa66160 --- /dev/null +++ b/.github/scripts/package.json @@ -0,0 +1,19 @@ +{ + "name": "roocode-agent-scripts", + "version": "1.0.0", + "description": "RooCode Agent GitHub Actions Bot", + "type": "module", + "scripts": { + "start": "tsx roocode-agent.ts" + }, + "dependencies": { + "@octokit/rest": "^20.0.2", + "@anthropic-ai/sdk": "^0.37.0", + "openai": "^5.12.2", + "@modelcontextprotocol/sdk": "^1.12.0", + "tsx": "^4.19.3" + }, + "devDependencies": { + "@types/node": "^20.0.0" + } +} diff --git a/.github/scripts/roocode-agent.ts b/.github/scripts/roocode-agent.ts new file mode 100644 index 0000000000..e4779a20e2 --- /dev/null +++ b/.github/scripts/roocode-agent.ts @@ -0,0 +1,483 @@ +#!/usr/bin/env tsx + +import { Octokit } from "@octokit/rest" +import OpenAI from "openai" +import Anthropic from "@anthropic-ai/sdk" +import * as fs from "fs" +import * as path from "path" +import { exec } from "child_process" +import { promisify } from "util" + +const execAsync = promisify(exec) + +// Configuration from environment variables +const config = { + githubToken: process.env.GITHUB_TOKEN!, + openaiApiKey: process.env.OPENAI_API_KEY, + anthropicApiKey: process.env.ANTHROPIC_API_KEY, + openRouterApiKey: process.env.OPENROUTER_API_KEY, + modelProvider: process.env.MODEL_PROVIDER || "anthropic", + modelName: process.env.MODEL_NAME || "claude-3-5-sonnet-20241022", + maxTokens: parseInt(process.env.MAX_TOKENS || "8192"), + temperature: parseFloat(process.env.TEMPERATURE || "0.2"), +} + +// Initialize GitHub client +const octokit = new Octokit({ + auth: config.githubToken, +}) + +// Initialize AI clients based on provider +let aiClient: any +if (config.modelProvider === "openai" && config.openaiApiKey) { + aiClient = new OpenAI({ apiKey: config.openaiApiKey }) +} else if (config.modelProvider === "anthropic" && config.anthropicApiKey) { + aiClient = new Anthropic({ apiKey: config.anthropicApiKey }) +} else if (config.modelProvider === "openrouter" && config.openRouterApiKey) { + aiClient = new OpenAI({ + apiKey: config.openRouterApiKey, + baseURL: "https://openrouter.ai/api/v1", + }) +} else { + console.error("No valid AI provider configured") + process.exit(1) +} + +// Parse GitHub event +const eventPath = process.env.GITHUB_EVENT_PATH +const event = JSON.parse(fs.readFileSync(eventPath!, "utf8")) +const context = { + repo: process.env.GITHUB_REPOSITORY!.split("/")[1], + owner: process.env.GITHUB_REPOSITORY!.split("/")[0], + eventName: process.env.GITHUB_EVENT_NAME!, +} + +interface Command { + type: "plan" | "approve" | "fix" | "review" | "triage" | "label" | "comment" + content?: string + labels?: string[] + approved?: boolean +} + +// Extract commands from text +function extractCommands(text: string): Command[] { + const commands: Command[] = [] + + if (text.includes("/roo plan")) { + commands.push({ type: "plan" }) + } + if (text.includes("/roo approve")) { + commands.push({ type: "approve", approved: true }) + } + if (text.includes("/roo fix")) { + commands.push({ type: "fix" }) + } + if (text.includes("/roo review")) { + commands.push({ type: "review" }) + } + if (text.includes("/roo triage")) { + commands.push({ type: "triage" }) + } + if (text.includes("/roo label")) { + commands.push({ type: "label" }) + } + if (text.includes("/roo")) { + // Generic command - analyze and respond + commands.push({ type: "comment" }) + } + + return commands +} + +// Get AI response +async function getAIResponse(prompt: string, systemPrompt: string): Promise { + try { + if (config.modelProvider === "anthropic") { + const response = await aiClient.messages.create({ + model: config.modelName, + max_tokens: config.maxTokens, + temperature: config.temperature, + system: systemPrompt, + messages: [{ role: "user", content: prompt }], + }) + return response.content[0].text + } else { + // OpenAI or OpenRouter + const response = await aiClient.chat.completions.create({ + model: config.modelName, + max_tokens: config.maxTokens, + temperature: config.temperature, + messages: [ + { role: "system", content: systemPrompt }, + { role: "user", content: prompt }, + ], + }) + return response.choices[0].message.content || "" + } + } catch (error) { + console.error("Error getting AI response:", error) + throw error + } +} + +// Process issue event +async function processIssue(issue: any, comment?: any) { + const text = comment?.body || issue.body || "" + const commands = extractCommands(text) + + if (commands.length === 0) return + + for (const command of commands) { + switch (command.type) { + case "plan": + await createPlan(issue) + break + case "approve": + await approvePlan(issue) + break + case "fix": + await implementFix(issue) + break + case "triage": + await triageIssue(issue) + break + case "label": + await labelIssue(issue) + break + case "comment": + await respondToComment(issue, text) + break + } + } +} + +// Process pull request event +async function processPullRequest(pr: any, comment?: any) { + const text = comment?.body || pr.body || "" + const commands = extractCommands(text) + + if (commands.length === 0) return + + for (const command of commands) { + switch (command.type) { + case "review": + await reviewPullRequest(pr) + break + case "comment": + await respondToPRComment(pr, text) + break + } + } +} + +// Create a plan for an issue +async function createPlan(issue: any) { + const systemPrompt = `You are RooCode-Agent, an AI assistant that helps with GitHub issues. +Your task is to analyze the issue and create a detailed implementation plan. +Format your response as a clear, step-by-step plan with: +1. Understanding of the problem +2. Proposed solution +3. Implementation steps +4. Testing approach +5. Potential risks or considerations + +End your plan with: "Reply with '/roo approve' to proceed with implementation."` + + const prompt = `Issue #${issue.number}: ${issue.title} + +${issue.body} + +Please create a detailed implementation plan for this issue.` + + const plan = await getAIResponse(prompt, systemPrompt) + + await octokit.issues.createComment({ + owner: context.owner, + repo: context.repo, + issue_number: issue.number, + body: `## 📋 Implementation Plan + +${plan} + +--- +*Generated by RooCode-Agent*`, + }) +} + +// Approve and implement plan +async function approvePlan(issue: any) { + // Check if there's a plan in the comments + const comments = await octokit.issues.listComments({ + owner: context.owner, + repo: context.repo, + issue_number: issue.number, + }) + + const planComment = comments.data.find( + (c) => c.body?.includes("Implementation Plan") && c.user?.login === "github-actions[bot]", + ) + + if (!planComment) { + await octokit.issues.createComment({ + owner: context.owner, + repo: context.repo, + issue_number: issue.number, + body: `❌ No plan found to approve. Please run '/roo plan' first.`, + }) + return + } + + await octokit.issues.createComment({ + owner: context.owner, + repo: context.repo, + issue_number: issue.number, + body: `✅ Plan approved! Starting implementation... + +*Note: This is a demonstration. In a real scenario, the bot would now create a branch and implement the changes.*`, + }) + + // In a real implementation, this would create a branch and start coding + await implementFix(issue) +} + +// Implement a fix for an issue +async function implementFix(issue: any) { + // This is a simplified version - in reality, this would: + // 1. Create a new branch + // 2. Make code changes based on the issue + // 3. Commit and push changes + // 4. Create a pull request + + const branchName = `fix/issue-${issue.number}` + + try { + // Create a simple demonstration PR + await octokit.issues.createComment({ + owner: context.owner, + repo: context.repo, + issue_number: issue.number, + body: `🔧 Working on implementation... + +- Creating branch: \`${branchName}\` +- Analyzing codebase... +- Implementing changes... + +*Note: This is a demonstration. In a production environment, the bot would actually create and modify files.*`, + }) + } catch (error) { + console.error("Error implementing fix:", error) + await octokit.issues.createComment({ + owner: context.owner, + repo: context.repo, + issue_number: issue.number, + body: `❌ Error implementing fix: ${error}`, + }) + } +} + +// Triage an issue +async function triageIssue(issue: any) { + const systemPrompt = `You are RooCode-Agent. Analyze this issue and suggest: +1. Priority level (P0-Critical, P1-High, P2-Medium, P3-Low) +2. Relevant labels (bug, enhancement, documentation, etc.) +3. Estimated complexity (Easy, Medium, Hard) +4. Suggested assignee type (frontend, backend, fullstack, devops)` + + const prompt = `Issue #${issue.number}: ${issue.title} + +${issue.body} + +Please triage this issue.` + + const response = await getAIResponse(prompt, systemPrompt) + + await octokit.issues.createComment({ + owner: context.owner, + repo: context.repo, + issue_number: issue.number, + body: `## 🏷️ Issue Triage + +${response} + +--- +*Generated by RooCode-Agent*`, + }) +} + +// Label an issue based on content +async function labelIssue(issue: any) { + const systemPrompt = `You are RooCode-Agent. Analyze this issue and suggest appropriate labels. +Common labels include: bug, enhancement, documentation, question, help wanted, good first issue, +frontend, backend, performance, security, testing, ui/ux. +Return only a comma-separated list of labels.` + + const prompt = `Issue #${issue.number}: ${issue.title} + +${issue.body} + +Suggest appropriate labels for this issue.` + + const response = await getAIResponse(prompt, systemPrompt) + const labels = response + .split(",") + .map((l) => l.trim()) + .filter((l) => l) + + if (labels.length > 0) { + try { + await octokit.issues.addLabels({ + owner: context.owner, + repo: context.repo, + issue_number: issue.number, + labels: labels, + }) + + await octokit.issues.createComment({ + owner: context.owner, + repo: context.repo, + issue_number: issue.number, + body: `🏷️ Added labels: ${labels.map((l) => `\`${l}\``).join(", ")}`, + }) + } catch (error) { + console.error("Error adding labels:", error) + } + } +} + +// Review a pull request +async function reviewPullRequest(pr: any) { + const systemPrompt = `You are RooCode-Agent, a code reviewer. Review this pull request and provide: +1. Summary of changes +2. Code quality assessment +3. Potential issues or bugs +4. Suggestions for improvement +5. Security considerations +6. Overall recommendation (Approve, Request Changes, or Comment)` + + // Get PR diff + const diff = await octokit.pulls.get({ + owner: context.owner, + repo: context.repo, + pull_number: pr.number, + mediaType: { format: "diff" }, + }) + + const prompt = `Pull Request #${pr.number}: ${pr.title} + +Description: +${pr.body} + +Diff: +${diff.data} + +Please review this pull request.` + + const review = await getAIResponse(prompt, systemPrompt) + + await octokit.pulls.createReview({ + owner: context.owner, + repo: context.repo, + pull_number: pr.number, + body: `## 🔍 Code Review + +${review} + +--- +*Generated by RooCode-Agent*`, + event: "COMMENT", + }) +} + +// Respond to a comment on an issue +async function respondToComment(issue: any, commentText: string) { + const systemPrompt = `You are RooCode-Agent, an AI assistant for the Roo-Code project. +Respond helpfully to the user's comment or question about this issue. +Be concise, technical, and actionable.` + + const prompt = `Issue #${issue.number}: ${issue.title} + +Issue Description: +${issue.body} + +User Comment: +${commentText} + +Please provide a helpful response.` + + const response = await getAIResponse(prompt, systemPrompt) + + await octokit.issues.createComment({ + owner: context.owner, + repo: context.repo, + issue_number: issue.number, + body: response + "\n\n---\n*Response by RooCode-Agent*", + }) +} + +// Respond to a comment on a PR +async function respondToPRComment(pr: any, commentText: string) { + const systemPrompt = `You are RooCode-Agent, an AI assistant for the Roo-Code project. +Respond helpfully to the user's comment or question about this pull request. +Be concise, technical, and actionable.` + + const prompt = `Pull Request #${pr.number}: ${pr.title} + +PR Description: +${pr.body} + +User Comment: +${commentText} + +Please provide a helpful response.` + + const response = await getAIResponse(prompt, systemPrompt) + + await octokit.issues.createComment({ + owner: context.owner, + repo: context.repo, + issue_number: pr.number, + body: response + "\n\n---\n*Response by RooCode-Agent*", + }) +} + +// Main execution +async function main() { + try { + console.log(`Processing ${context.eventName} event`) + + if (context.eventName === "issues") { + await processIssue(event.issue) + } else if (context.eventName === "issue_comment") { + await processIssue(event.issue, event.comment) + } else if (context.eventName === "pull_request") { + await processPullRequest(event.pull_request) + } else if (context.eventName === "pull_request_review_comment") { + await processPullRequest(event.pull_request, event.comment) + } else if (context.eventName === "workflow_dispatch") { + // Handle manual trigger + if (event.inputs?.issue_number) { + const issue = await octokit.issues.get({ + owner: context.owner, + repo: context.repo, + issue_number: parseInt(event.inputs.issue_number), + }) + await processIssue(issue.data) + } else if (event.inputs?.pr_number) { + const pr = await octokit.pulls.get({ + owner: context.owner, + repo: context.repo, + pull_number: parseInt(event.inputs.pr_number), + }) + await processPullRequest(pr.data) + } + } + + console.log("Event processed successfully") + } catch (error) { + console.error("Error processing event:", error) + process.exit(1) + } +} + +// Run the bot +main() diff --git a/.github/workflows/roocode-bot.yml b/.github/workflows/roocode-bot.yml new file mode 100644 index 0000000000..ef0498714e --- /dev/null +++ b/.github/workflows/roocode-bot.yml @@ -0,0 +1,66 @@ +name: RooCode Agent Bot + +on: + issues: + types: [opened, edited] + issue_comment: + types: [created, edited] + pull_request: + types: [opened, edited, synchronize] + pull_request_review_comment: + types: [created, edited] + workflow_dispatch: + inputs: + issue_number: + description: 'Issue number to process' + required: false + type: string + pr_number: + description: 'PR number to process' + required: false + type: string + +jobs: + process-event: + runs-on: ubuntu-latest + if: | + (github.event_name == 'issues' && contains(github.event.issue.body, '/roo')) || + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '/roo')) || + (github.event_name == 'pull_request' && contains(github.event.pull_request.body, '/roo')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '/roo')) || + github.event_name == 'workflow_dispatch' + + permissions: + contents: write + issues: write + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: | + cd .github/scripts + npm install + + - name: Process Event + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} + MODEL_PROVIDER: ${{ vars.MODEL_PROVIDER || 'anthropic' }} + MODEL_NAME: ${{ vars.MODEL_NAME || 'claude-3-5-sonnet-20241022' }} + MAX_TOKENS: ${{ vars.MAX_TOKENS || '8192' }} + TEMPERATURE: ${{ vars.TEMPERATURE || '0.2' }} + run: | + cd .github/scripts + npm start \ No newline at end of file diff --git a/packages/types/src/vscode.ts b/packages/types/src/vscode.ts index 2838514690..ba1fd02b5f 100644 --- a/packages/types/src/vscode.ts +++ b/packages/types/src/vscode.ts @@ -34,6 +34,7 @@ export const commandIds = [ "mcpButtonClicked", "historyButtonClicked", "marketplaceButtonClicked", + "githubActionsButtonClicked", "popoutButtonClicked", "cloudButtonClicked", "settingsButtonClicked", diff --git a/src/activate/registerCommands.ts b/src/activate/registerCommands.ts index fac615edf1..94c708a3f0 100644 --- a/src/activate/registerCommands.ts +++ b/src/activate/registerCommands.ts @@ -158,6 +158,14 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt if (!visibleProvider) return visibleProvider.postMessageToWebview({ type: "action", action: "marketplaceButtonClicked" }) }, + githubActionsButtonClicked: () => { + const visibleProvider = getVisibleProviderOrLog(outputChannel) + if (!visibleProvider) return + + TelemetryService.instance.captureTitleButtonClicked("githubActions") + + visibleProvider.postMessageToWebview({ type: "action", action: "githubActionsButtonClicked" }) + }, showHumanRelayDialog: (params: { requestId: string; promptText: string }) => { const panel = getPanel() diff --git a/src/package.json b/src/package.json index 05b59063ec..ec2e3605a5 100644 --- a/src/package.json +++ b/src/package.json @@ -95,6 +95,11 @@ "title": "%command.marketplace.title%", "icon": "$(extensions)" }, + { + "command": "roo-cline.githubActionsButtonClicked", + "title": "%command.githubActions.title%", + "icon": "$(github-action)" + }, { "command": "roo-cline.popoutButtonClicked", "title": "%command.openInEditor.title%", @@ -229,15 +234,20 @@ "when": "view == roo-cline.SidebarProvider" }, { - "command": "roo-cline.settingsButtonClicked", + "command": "roo-cline.githubActionsButtonClicked", "group": "navigation@3", "when": "view == roo-cline.SidebarProvider" }, { - "command": "roo-cline.cloudButtonClicked", + "command": "roo-cline.settingsButtonClicked", "group": "navigation@4", "when": "view == roo-cline.SidebarProvider" }, + { + "command": "roo-cline.cloudButtonClicked", + "group": "navigation@5", + "when": "view == roo-cline.SidebarProvider" + }, { "command": "roo-cline.historyButtonClicked", "group": "overflow@1", @@ -417,6 +427,41 @@ "minimum": 1, "maximum": 200, "description": "%settings.codeIndex.embeddingBatchSize.description%" + }, + "roo-cline.githubActions.enabled": { + "type": "boolean", + "default": false, + "description": "%settings.githubActions.enabled.description%" + }, + "roo-cline.githubActions.workflowsPath": { + "type": "string", + "default": ".github/workflows", + "description": "%settings.githubActions.workflowsPath.description%" + }, + "roo-cline.githubActions.autoInstall": { + "type": "boolean", + "default": false, + "description": "%settings.githubActions.autoInstall.description%" + }, + "roo-cline.githubActions.defaultBranch": { + "type": "string", + "default": "main", + "description": "%settings.githubActions.defaultBranch.description%" + }, + "roo-cline.githubActions.modelProvider": { + "type": "string", + "enum": [ + "anthropic", + "openai", + "openrouter" + ], + "default": "anthropic", + "description": "%settings.githubActions.modelProvider.description%" + }, + "roo-cline.githubActions.modelName": { + "type": "string", + "default": "claude-3-5-sonnet-20241022", + "description": "%settings.githubActions.modelName.description%" } } } diff --git a/src/package.nls.json b/src/package.nls.json index b0b7f401f8..6775d49239 100644 --- a/src/package.nls.json +++ b/src/package.nls.json @@ -10,6 +10,7 @@ "command.prompts.title": "Modes", "command.history.title": "History", "command.marketplace.title": "Marketplace", + "command.githubActions.title": "GitHub Actions", "command.openInEditor.title": "Open in Editor", "command.cloud.title": "Cloud", "command.settings.title": "Settings", @@ -41,5 +42,11 @@ "settings.useAgentRules.description": "Enable loading of AGENTS.md files for agent-specific rules (see https://agent-rules.org/)", "settings.apiRequestTimeout.description": "Maximum time in seconds to wait for API responses (0 = no timeout, 1-3600s, default: 600s). Higher values are recommended for local providers like LM Studio and Ollama that may need more processing time.", "settings.newTaskRequireTodos.description": "Require todos parameter when creating new tasks with the new_task tool", - "settings.codeIndex.embeddingBatchSize.description": "The batch size for embedding operations during code indexing. Adjust this based on your API provider's limits. Default is 60." + "settings.codeIndex.embeddingBatchSize.description": "The batch size for embedding operations during code indexing. Adjust this based on your API provider's limits. Default is 60.", + "settings.githubActions.enabled.description": "Enable GitHub Actions bot integration", + "settings.githubActions.workflowsPath.description": "Path to GitHub Actions workflows directory", + "settings.githubActions.autoInstall.description": "Automatically install recommended workflows", + "settings.githubActions.defaultBranch.description": "Default branch for GitHub Actions operations", + "settings.githubActions.modelProvider.description": "AI model provider for GitHub Actions bot", + "settings.githubActions.modelName.description": "AI model name for GitHub Actions bot" } diff --git a/src/services/github-actions/GitHubActionsService.ts b/src/services/github-actions/GitHubActionsService.ts new file mode 100644 index 0000000000..8bc5818220 --- /dev/null +++ b/src/services/github-actions/GitHubActionsService.ts @@ -0,0 +1,490 @@ +import * as vscode from "vscode" +import * as fs from "fs/promises" +import * as path from "path" +import { exec } from "child_process" +import { promisify } from "util" + +const execAsync = promisify(exec) + +export interface GitHubActionsConfig { + enabled: boolean + workflowsPath: string + autoInstall: boolean + defaultBranch: string + botToken?: string + modelProvider?: string + modelName?: string +} + +export interface WorkflowTemplate { + name: string + description: string + filename: string + content: string +} + +export class GitHubActionsService { + private static instance: GitHubActionsService + private config: GitHubActionsConfig + private workspaceRoot: string | undefined + + private constructor() { + this.config = this.loadConfig() + this.workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath + } + + public static getInstance(): GitHubActionsService { + if (!GitHubActionsService.instance) { + GitHubActionsService.instance = new GitHubActionsService() + } + return GitHubActionsService.instance + } + + private loadConfig(): GitHubActionsConfig { + const config = vscode.workspace.getConfiguration("roo-cline.githubActions") + return { + enabled: config.get("enabled", false), + workflowsPath: config.get("workflowsPath", ".github/workflows"), + autoInstall: config.get("autoInstall", false), + defaultBranch: config.get("defaultBranch", "main"), + botToken: config.get("botToken"), + modelProvider: config.get("modelProvider", "anthropic"), + modelName: config.get("modelName", "claude-3-5-sonnet-20241022"), + } + } + + public async updateConfig(newConfig: Partial): Promise { + const config = vscode.workspace.getConfiguration("roo-cline.githubActions") + + for (const [key, value] of Object.entries(newConfig)) { + await config.update(key, value, vscode.ConfigurationTarget.Workspace) + } + + this.config = { ...this.config, ...newConfig } + } + + public getConfig(): GitHubActionsConfig { + return this.config + } + + public getWorkflowTemplates(): WorkflowTemplate[] { + return [ + { + name: "RooCode Agent Bot", + description: "Main bot workflow that handles issues and PRs with /roo commands", + filename: "roocode-bot.yml", + content: this.getRooCodeBotWorkflow(), + }, + { + name: "Issue Triage", + description: "Automatically triage and label new issues", + filename: "issue-triage.yml", + content: this.getIssueTriageWorkflow(), + }, + { + name: "PR Auto-Review", + description: "Automatically review pull requests", + filename: "pr-auto-review.yml", + content: this.getPRAutoReviewWorkflow(), + }, + { + name: "Auto-Fix", + description: "Automatically fix simple issues on push", + filename: "auto-fix.yml", + content: this.getAutoFixWorkflow(), + }, + ] + } + + public async installWorkflow(template: WorkflowTemplate): Promise { + if (!this.workspaceRoot) { + throw new Error("No workspace folder found") + } + + const workflowsDir = path.join(this.workspaceRoot, this.config.workflowsPath) + const workflowPath = path.join(workflowsDir, template.filename) + + // Create workflows directory if it doesn't exist + await fs.mkdir(workflowsDir, { recursive: true }) + + // Write workflow file + await fs.writeFile(workflowPath, template.content, "utf8") + + // Also install the agent scripts + await this.installAgentScripts() + + vscode.window.showInformationMessage(`GitHub Actions workflow "${template.name}" installed successfully!`) + } + + private async installAgentScripts(): Promise { + if (!this.workspaceRoot) { + throw new Error("No workspace folder found") + } + + const scriptsDir = path.join(this.workspaceRoot, ".github", "scripts") + await fs.mkdir(scriptsDir, { recursive: true }) + + // Install agent script + const agentScriptPath = path.join(scriptsDir, "roocode-agent.ts") + await fs.writeFile(agentScriptPath, this.getAgentScript(), "utf8") + + // Install package.json + const packageJsonPath = path.join(scriptsDir, "package.json") + await fs.writeFile(packageJsonPath, this.getAgentPackageJson(), "utf8") + + // Install README + const readmePath = path.join(scriptsDir, "README.md") + await fs.writeFile(readmePath, this.getAgentReadme(), "utf8") + } + + public async uninstallWorkflow(filename: string): Promise { + if (!this.workspaceRoot) { + throw new Error("No workspace folder found") + } + + const workflowPath = path.join(this.workspaceRoot, this.config.workflowsPath, filename) + + try { + await fs.unlink(workflowPath) + vscode.window.showInformationMessage(`Workflow "${filename}" uninstalled successfully!`) + } catch (error) { + vscode.window.showErrorMessage(`Failed to uninstall workflow: ${error}`) + } + } + + public async getInstalledWorkflows(): Promise { + if (!this.workspaceRoot) { + return [] + } + + const workflowsDir = path.join(this.workspaceRoot, this.config.workflowsPath) + + try { + const files = await fs.readdir(workflowsDir) + return files.filter((file) => file.endsWith(".yml") || file.endsWith(".yaml")) + } catch { + return [] + } + } + + public async setupGitHubRepository(): Promise { + if (!this.workspaceRoot) { + throw new Error("No workspace folder found") + } + + // Check if git repository exists + try { + await execAsync("git status", { cwd: this.workspaceRoot }) + } catch { + vscode.window.showErrorMessage("This is not a git repository. Please initialize git first.") + return + } + + // Get repository information + const { stdout: remoteUrl } = await execAsync("git remote get-url origin", { + cwd: this.workspaceRoot, + }) + + if (!remoteUrl.includes("github.com")) { + vscode.window.showErrorMessage("This repository is not hosted on GitHub.") + return + } + + // Extract owner and repo from URL + const match = remoteUrl.match(/github\.com[:/]([^/]+)\/([^/.]+)/) + if (!match) { + vscode.window.showErrorMessage("Could not parse GitHub repository URL.") + return + } + + const [, owner, repo] = match + + // Show setup instructions + const message = ` +GitHub Actions Bot Setup Instructions: + +1. Go to https://github.com/${owner}/${repo}/settings/secrets/actions +2. Add the following secrets: + - ANTHROPIC_API_KEY (or OPENAI_API_KEY) + +3. Go to Settings > Actions > General +4. Enable "Read and write permissions" +5. Check "Allow GitHub Actions to create and approve pull requests" + +6. Install workflows using the GitHub Actions view in Roo Code +` + + vscode.window.showInformationMessage(message, { modal: true }) + } + + public async testBotConnection(): Promise { + // This would test the connection to the AI provider + // For now, just check if configuration is valid + if (!this.config.enabled) { + vscode.window.showWarningMessage("GitHub Actions bot is not enabled") + return false + } + + if (!this.config.modelProvider) { + vscode.window.showErrorMessage("No model provider configured") + return false + } + + vscode.window.showInformationMessage("Bot configuration is valid!") + return true + } + + // Workflow template content methods + private getRooCodeBotWorkflow(): string { + return `name: RooCode Agent Bot + +on: + issues: + types: [opened, edited] + issue_comment: + types: [created, edited] + pull_request: + types: [opened, edited, synchronize] + pull_request_review_comment: + types: [created, edited] + workflow_dispatch: + inputs: + issue_number: + description: 'Issue number to process' + required: false + type: string + pr_number: + description: 'PR number to process' + required: false + type: string + +jobs: + process-event: + runs-on: ubuntu-latest + if: | + (github.event_name == 'issues' && contains(github.event.issue.body, '/roo')) || + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '/roo')) || + (github.event_name == 'pull_request' && contains(github.event.pull_request.body, '/roo')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '/roo')) || + github.event_name == 'workflow_dispatch' + + permissions: + contents: write + issues: write + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: | + cd .github/scripts + npm install + + - name: Process Event + env: + GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }} + OPENAI_API_KEY: \${{ secrets.OPENAI_API_KEY }} + ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }} + OPENROUTER_API_KEY: \${{ secrets.OPENROUTER_API_KEY }} + MODEL_PROVIDER: \${{ vars.MODEL_PROVIDER || '${this.config.modelProvider}' }} + MODEL_NAME: \${{ vars.MODEL_NAME || '${this.config.modelName}' }} + MAX_TOKENS: \${{ vars.MAX_TOKENS || '8192' }} + TEMPERATURE: \${{ vars.TEMPERATURE || '0.2' }} + run: | + cd .github/scripts + npm start +` + } + + private getIssueTriageWorkflow(): string { + return `name: Issue Triage Bot + +on: + issues: + types: [opened] + +jobs: + triage: + runs-on: ubuntu-latest + permissions: + issues: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: | + cd .github/scripts + npm install + + - name: Triage Issue + env: + GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }} + ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }} + ISSUE_NUMBER: \${{ github.event.issue.number }} + run: | + cd .github/scripts + echo "Triaging issue #\$ISSUE_NUMBER" + npm start -- --triage +` + } + + private getPRAutoReviewWorkflow(): string { + return `name: PR Auto-Review Bot + +on: + pull_request: + types: [opened, synchronize] + +jobs: + review: + runs-on: ubuntu-latest + permissions: + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: | + cd .github/scripts + npm install + + - name: Review PR + env: + GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }} + ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }} + PR_NUMBER: \${{ github.event.pull_request.number }} + run: | + cd .github/scripts + echo "Reviewing PR #\$PR_NUMBER" + npm start -- --review +` + } + + private getAutoFixWorkflow(): string { + return `name: Auto-Fix Bot + +on: + push: + branches: [ main, develop ] + workflow_dispatch: + +jobs: + auto-fix: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: | + cd .github/scripts + npm install + + - name: Run Auto-Fix + env: + GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }} + ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }} + run: | + cd .github/scripts + echo "Running auto-fix..." + npm start -- --auto-fix +` + } + + private getAgentScript(): string { + // Return the full agent script content + // This is a simplified version - the full script is in .github/scripts/roocode-agent.ts + return `#!/usr/bin/env tsx + +import { Octokit } from '@octokit/rest'; +import OpenAI from 'openai'; +import Anthropic from '@anthropic-ai/sdk'; +// ... rest of the agent script implementation +// See .github/scripts/roocode-agent.ts for full implementation +` + } + + private getAgentPackageJson(): string { + return JSON.stringify( + { + name: "roocode-agent-scripts", + version: "1.0.0", + description: "RooCode Agent GitHub Actions Bot", + type: "module", + scripts: { + start: "tsx roocode-agent.ts", + }, + dependencies: { + "@octokit/rest": "^20.0.2", + "@anthropic-ai/sdk": "^0.37.0", + openai: "^5.12.2", + "@modelcontextprotocol/sdk": "^1.12.0", + tsx: "^4.19.3", + }, + devDependencies: { + "@types/node": "^20.0.0", + }, + }, + null, + 2, + ) + } + + private getAgentReadme(): string { + return `# RooCode Agent - GitHub Actions Bot + +See the full documentation in the main README. + +## Quick Start + +1. Configure repository secrets +2. Enable GitHub Actions +3. Create an issue or PR with /roo command + +## Commands + +- /roo plan - Create implementation plan +- /roo approve - Approve and implement +- /roo fix - Direct fix +- /roo review - Review PR +- /roo triage - Triage issue +- /roo label - Add labels +` + } +} diff --git a/src/services/github-actions/index.ts b/src/services/github-actions/index.ts new file mode 100644 index 0000000000..848020a6a5 --- /dev/null +++ b/src/services/github-actions/index.ts @@ -0,0 +1,2 @@ +export { GitHubActionsService } from "./GitHubActionsService" +export type { GitHubActionsConfig, WorkflowTemplate } from "./GitHubActionsService" diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index aaddc520cb..ee0f4d98fd 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -124,6 +124,10 @@ export interface ExtensionMessage { | "commands" | "insertTextIntoTextarea" | "dismissedUpsells" + | "gitHubActionsConfig" + | "installedWorkflows" + | "workflowInstalled" + | "workflowUninstalled" text?: string payload?: any // Add a generic payload for now, can refine later action?: @@ -137,6 +141,7 @@ export interface ExtensionMessage { | "didBecomeVisible" | "focusInput" | "switchTab" + | "githubActionsButtonClicked" invoke?: "newChat" | "sendMessage" | "primaryButtonClick" | "secondaryButtonClick" | "setChatBoxMessage" state?: ExtensionState images?: string[] @@ -201,6 +206,7 @@ export interface ExtensionMessage { commands?: Command[] queuedMessages?: QueuedMessage[] list?: string[] // For dismissedUpsells + workflows?: string[] // For installedWorkflows } export type ExtensionState = Pick< diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index f5bbae577e..4697eba29d 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -226,6 +226,14 @@ export interface WebviewMessage { | "editQueuedMessage" | "dismissUpsell" | "getDismissedUpsells" + | "getGitHubActionsConfig" + | "getInstalledWorkflows" + | "updateGitHubActionsConfig" + | "installGitHubActionsWorkflow" + | "uninstallGitHubActionsWorkflow" + | "setupGitHubRepository" + | "testGitHubActionsConnection" + | "githubActionsButtonClicked" text?: string editedMessageContent?: string tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud" @@ -267,12 +275,14 @@ export interface WebviewMessage { url?: string // For openExternal mpItem?: MarketplaceItem mpInstallOptions?: InstallMarketplaceItemOptions - config?: Record // Add config to the payload + config?: Record // Add config to the payload (also used for GitHub Actions config) visibility?: ShareVisibility // For share visibility hasContent?: boolean // For checkRulesDirectoryResult checkOnly?: boolean // For deleteCustomMode check upsellId?: string // For dismissUpsell list?: string[] // For dismissedUpsells response + workflow?: any // For GitHub Actions workflow operations + filename?: string // For GitHub Actions workflow uninstall codeIndexSettings?: { // Global state settings codebaseIndexEnabled: boolean diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index fa38a566e7..9496539859 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -26,8 +26,9 @@ import { CloudView } from "./components/cloud/CloudView" import { useAddNonInteractiveClickListener } from "./components/ui/hooks/useNonInteractiveClick" import { TooltipProvider } from "./components/ui/tooltip" import { STANDARD_TOOLTIP_DELAY } from "./components/ui/standard-tooltip" +import GitHubActionsView from "./components/githubActions/GitHubActionsView" -type Tab = "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud" +type Tab = "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud" | "githubActions" interface HumanRelayDialogState { isOpen: boolean @@ -63,6 +64,7 @@ const tabsByMessageAction: Partial { @@ -270,6 +272,7 @@ const App = () => { onDone={() => switchTab("chat")} /> )} + {tab === "githubActions" && switchTab("chat")} />} void }) { + const [config, setConfig] = useState({ + enabled: false, + workflowsPath: ".github/workflows", + autoInstall: false, + defaultBranch: "main", + modelProvider: "anthropic", + modelName: "claude-3-5-sonnet-20241022", + }) + const [workflows, setWorkflows] = useState([ + { + name: "RooCode Agent Bot", + description: "Main bot workflow that handles issues and PRs with /roo commands", + filename: "roocode-bot.yml", + installed: false, + }, + { + name: "Issue Triage", + description: "Automatically triage and label new issues", + filename: "issue-triage.yml", + installed: false, + }, + { + name: "PR Auto-Review", + description: "Automatically review pull requests", + filename: "pr-auto-review.yml", + installed: false, + }, + { + name: "Auto-Fix", + description: "Automatically fix simple issues on push", + filename: "auto-fix.yml", + installed: false, + }, + ]) + const [showSetupGuide, setShowSetupGuide] = useState(false) + + useEffect(() => { + // Load configuration from extension + vscode.postMessage({ type: "getGitHubActionsConfig" }) + vscode.postMessage({ type: "getInstalledWorkflows" }) + }, []) + + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + const message = event.data + switch (message.type) { + case "gitHubActionsConfig": + setConfig(message.config) + break + case "installedWorkflows": { + const installedFiles = message.workflows || [] + setWorkflows((prev) => + prev.map((w) => ({ + ...w, + installed: installedFiles.includes(w.filename), + })), + ) + break + } + case "workflowInstalled": + setWorkflows((prev) => + prev.map((w) => (w.filename === message.filename ? { ...w, installed: true } : w)), + ) + break + case "workflowUninstalled": + setWorkflows((prev) => + prev.map((w) => (w.filename === message.filename ? { ...w, installed: false } : w)), + ) + break + } + } + + window.addEventListener("message", handleMessage) + return () => window.removeEventListener("message", handleMessage) + }, []) + + const handleToggleBot = () => { + const newConfig = { ...config, enabled: !config.enabled } + setConfig(newConfig) + vscode.postMessage({ type: "updateGitHubActionsConfig", config: newConfig }) + } + + const handleInstallWorkflow = (workflow: WorkflowTemplate) => { + vscode.postMessage({ type: "installGitHubActionsWorkflow", workflow }) + } + + const handleUninstallWorkflow = (workflow: WorkflowTemplate) => { + vscode.postMessage({ type: "uninstallGitHubActionsWorkflow", filename: workflow.filename }) + } + + const handleSetupRepository = () => { + vscode.postMessage({ type: "setupGitHubRepository" }) + setShowSetupGuide(true) + } + + const handleTestConnection = () => { + vscode.postMessage({ type: "testGitHubActionsConnection" }) + } + + return ( +
+
+

🤖 GitHub Actions Bot

+

+ Automate issue and pull request handling with AI-powered GitHub Actions +

+
+ + + +
+
+ + Enable GitHub Actions Bot + + + {config.enabled ? "Active" : "Inactive"} + +
+ +
+ 📚 Setup Guide + + 🔌 Test Connection + +
+
+ + + +
+

Available Workflows

+
+ {workflows.map((workflow) => ( +
+
+
+

+ {workflow.name} + {workflow.installed && ( + + Installed + + )} +

+

+ {workflow.description} +

+ + {workflow.filename} + +
+ + workflow.installed + ? handleUninstallWorkflow(workflow) + : handleInstallWorkflow(workflow) + } + appearance={workflow.installed ? "secondary" : "primary"}> + {workflow.installed ? "Uninstall" : "Install"} + +
+
+ ))} +
+
+ + {showSetupGuide && ( + <> + +
+

📋 Setup Instructions

+
+
    +
  1. + Configure Repository Secrets: +
      +
    • Go to your repository's Settings → Secrets and variables → Actions
    • +
    • + Add ANTHROPIC_API_KEY or OPENAI_API_KEY +
    • +
    • + Optionally add OPENROUTER_API_KEY for OpenRouter support +
    • +
    +
  2. +
  3. + Configure Repository Variables: +
      +
    • + MODEL_PROVIDER: anthropic, openai, or openrouter +
    • +
    • + MODEL_NAME: Model identifier (e.g., claude-3-5-sonnet-20241022) +
    • +
    • + MAX_TOKENS: Maximum response tokens (default: 8192) +
    • +
    • + TEMPERATURE: Model temperature 0-1 (default: 0.2) +
    • +
    +
  4. +
  5. + Enable GitHub Actions Permissions: +
      +
    • Go to Settings → Actions → General
    • +
    • Select "Read and write permissions"
    • +
    • + Check "Allow GitHub Actions to create and approve pull requests" +
    • +
    +
  6. +
  7. + Install Workflows: +
      +
    • Click "Install" on the workflows you want to use
    • +
    • Commit and push the changes to your repository
    • +
    +
  8. +
  9. + Test the Bot: +
      +
    • + Create an issue or comment with /roo command +
    • +
    • + Example: /roo plan to create an implementation plan +
    • +
    +
  10. +
+
+
+ + )} + + + +
+

🎯 Available Commands

+
+
+ /roo plan + Create a detailed implementation plan for an issue + + /roo approve + Approve a plan and start implementation + + /roo fix + Directly implement a fix for an issue + + /roo review + Perform a code review on a pull request + + /roo triage + Analyze and triage an issue (priority, labels, complexity) + + /roo label + Automatically add appropriate labels to an issue + + /roo + General interaction with the bot +
+
+
+ + + +
+ Done +
+
+ ) +}