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..2209fd4bbb 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%", @@ -254,9 +259,14 @@ "when": "view == roo-cline.SidebarProvider" }, { - "command": "roo-cline.popoutButtonClicked", + "command": "roo-cline.githubActionsButtonClicked", "group": "overflow@4", "when": "view == roo-cline.SidebarProvider" + }, + { + "command": "roo-cline.popoutButtonClicked", + "group": "overflow@5", + "when": "view == roo-cline.SidebarProvider" } ], "editor/title": [ @@ -296,9 +306,14 @@ "when": "activeWebviewPanelId == roo-cline.TabPanelProvider" }, { - "command": "roo-cline.popoutButtonClicked", + "command": "roo-cline.githubActionsButtonClicked", "group": "overflow@4", "when": "activeWebviewPanelId == roo-cline.TabPanelProvider" + }, + { + "command": "roo-cline.popoutButtonClicked", + "group": "overflow@5", + "when": "activeWebviewPanelId == roo-cline.TabPanelProvider" } ] }, @@ -417,6 +432,31 @@ "minimum": 1, "maximum": 200, "description": "%settings.codeIndex.embeddingBatchSize.description%" + }, + "rooCode.githubActions.enabled": { + "type": "boolean", + "default": false, + "description": "%settings.githubActions.enabled.description%" + }, + "rooCode.githubActions.workflowsPath": { + "type": "string", + "default": ".github/workflows", + "description": "%settings.githubActions.workflowsPath.description%" + }, + "rooCode.githubActions.autoInstall": { + "type": "boolean", + "default": false, + "description": "%settings.githubActions.autoInstall.description%" + }, + "rooCode.githubActions.defaultBranch": { + "type": "string", + "default": "main", + "description": "%settings.githubActions.defaultBranch.description%" + }, + "rooCode.githubActions.botToken": { + "type": "string", + "default": "", + "description": "%settings.githubActions.botToken.description%" } } } diff --git a/src/package.nls.json b/src/package.nls.json index b0b7f401f8..9d2a7003f4 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,10 @@ "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 (relative to workspace root)", + "settings.githubActions.autoInstall.description": "Automatically install GitHub Actions workflows when opening a repository", + "settings.githubActions.defaultBranch.description": "Default branch name for GitHub Actions workflows", + "settings.githubActions.botToken.description": "GitHub token for the Actions bot (optional, for advanced features)" } diff --git a/src/services/github-actions/GitHubActionsService.ts b/src/services/github-actions/GitHubActionsService.ts new file mode 100644 index 0000000000..ceea9b4e0d --- /dev/null +++ b/src/services/github-actions/GitHubActionsService.ts @@ -0,0 +1,394 @@ +import * as vscode from "vscode" +import * as path from "path" +import * as fs from "fs/promises" +import { safeWriteJson } from "../../utils/safeWriteJson" + +export interface GitHubActionsConfig { + enabled: boolean + workflowsPath: string + autoInstall: boolean + defaultBranch: string + botToken?: string +} + +export interface WorkflowTemplate { + name: string + description: string + fileName: string + content: string +} + +export class GitHubActionsService { + private static instance: GitHubActionsService | undefined + private config: GitHubActionsConfig + private outputChannel: vscode.OutputChannel + private workspaceRoot: string | undefined + + private constructor(outputChannel: vscode.OutputChannel, workspaceRoot: string | undefined) { + this.outputChannel = outputChannel + this.workspaceRoot = workspaceRoot + this.config = this.loadConfig() + } + + public static getInstance(outputChannel?: vscode.OutputChannel, workspaceRoot?: string): GitHubActionsService { + if (!GitHubActionsService.instance) { + if (!outputChannel) { + throw new Error("OutputChannel is required for first initialization") + } + GitHubActionsService.instance = new GitHubActionsService(outputChannel, workspaceRoot) + } + return GitHubActionsService.instance + } + + private loadConfig(): GitHubActionsConfig { + const config = vscode.workspace.getConfiguration("rooCode.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"), + } + } + + public async updateConfig(newConfig: Partial): Promise { + this.config = { ...this.config, ...newConfig } + const config = vscode.workspace.getConfiguration("rooCode.githubActions") + + for (const [key, value] of Object.entries(newConfig)) { + await config.update(key, value, vscode.ConfigurationTarget.Global) + } + } + + public getConfig(): GitHubActionsConfig { + return { ...this.config } + } + + public isEnabled(): boolean { + return this.config.enabled + } + + public async enable(): Promise { + await this.updateConfig({ enabled: true }) + this.outputChannel.appendLine("GitHub Actions bot enabled") + } + + public async disable(): Promise { + await this.updateConfig({ enabled: false }) + this.outputChannel.appendLine("GitHub Actions bot disabled") + } + + private getWorkflowTemplates(): WorkflowTemplate[] { + return [ + { + name: "Roo Code Issue Handler", + description: "Automatically handle GitHub issues with Roo Code", + fileName: "roo-code-issues.yml", + content: `name: Roo Code Issue Handler + +on: + issues: + types: [opened, edited] + issue_comment: + types: [created] + +jobs: + handle-issue: + runs-on: ubuntu-latest + permissions: + issues: write + contents: read + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Process issue with Roo Code + env: + GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }} + ROO_CODE_API_KEY: \${{ secrets.ROO_CODE_API_KEY }} + run: | + # This is a placeholder for the actual Roo Code CLI tool + # that would process the issue + echo "Processing issue #\${{ github.event.issue.number }}" + echo "Title: \${{ github.event.issue.title }}" + echo "Body: \${{ github.event.issue.body }}" + + - name: Create pull request if needed + if: success() + uses: peter-evans/create-pull-request@v5 + with: + token: \${{ secrets.GITHUB_TOKEN }} + commit-message: "fix: Automated fix for issue #\${{ github.event.issue.number }}" + title: "Fix for issue #\${{ github.event.issue.number }}" + body: | + This PR was automatically created by Roo Code to address issue #\${{ github.event.issue.number }}. + + ## Changes + - Automated fix implementation + + Closes #\${{ github.event.issue.number }} + branch: roo-code/issue-\${{ github.event.issue.number }} +`, + }, + { + name: "Roo Code PR Review", + description: "Automatically review pull requests with Roo Code", + fileName: "roo-code-pr-review.yml", + content: `name: Roo Code PR Review + +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + review-pr: + runs-on: ubuntu-latest + permissions: + pull-requests: write + contents: read + + 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: Review PR with Roo Code + env: + GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }} + ROO_CODE_API_KEY: \${{ secrets.ROO_CODE_API_KEY }} + run: | + # This is a placeholder for the actual Roo Code CLI tool + # that would review the PR + echo "Reviewing PR #\${{ github.event.pull_request.number }}" + echo "Title: \${{ github.event.pull_request.title }}" + + - name: Post review comment + if: success() + uses: actions/github-script@v7 + with: + github-token: \${{ secrets.GITHUB_TOKEN }} + script: | + await github.rest.pulls.createReview({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + body: '## Roo Code Review\\n\\nThis PR has been automatically reviewed by Roo Code.\\n\\nāœ… No issues found.', + event: 'COMMENT' + }); +`, + }, + { + name: "Roo Code Auto-Fix", + description: "Automatically fix code issues on push", + fileName: "roo-code-auto-fix.yml", + content: `name: Roo Code Auto-Fix + +on: + push: + branches: [ main, develop ] + paths: + - '**.ts' + - '**.tsx' + - '**.js' + - '**.jsx' + - '**.py' + - '**.java' + +jobs: + auto-fix: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: \${{ secrets.GITHUB_TOKEN }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Run Roo Code auto-fix + env: + GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }} + ROO_CODE_API_KEY: \${{ secrets.ROO_CODE_API_KEY }} + run: | + # This is a placeholder for the actual Roo Code CLI tool + # that would auto-fix issues + echo "Running Roo Code auto-fix on changed files" + + - name: Commit and push if changed + run: | + git config --global user.name 'Roo Code Bot' + git config --global user.email 'bot@roo-code.com' + git add -A + if git diff --staged --quiet; then + echo "No changes to commit" + else + git commit -m "fix: Auto-fix by Roo Code bot" + git push + fi +`, + }, + ] + } + + public async installWorkflows(selectedTemplates?: string[]): Promise { + if (!this.workspaceRoot) { + throw new Error("No workspace folder open") + } + + const templates = this.getWorkflowTemplates() + const templatesToInstall = selectedTemplates + ? templates.filter((t) => selectedTemplates.includes(t.fileName)) + : templates + + const workflowsDir = path.join(this.workspaceRoot, this.config.workflowsPath) + + // Create workflows directory if it doesn't exist + await fs.mkdir(workflowsDir, { recursive: true }) + + for (const template of templatesToInstall) { + const filePath = path.join(workflowsDir, template.fileName) + + // Check if file already exists + try { + await fs.access(filePath) + const overwrite = await vscode.window.showWarningMessage( + `Workflow ${template.fileName} already exists. Overwrite?`, + "Yes", + "No", + ) + if (overwrite !== "Yes") { + continue + } + } catch { + // File doesn't exist, proceed with creation + } + + await fs.writeFile(filePath, template.content, "utf8") + this.outputChannel.appendLine(`Installed workflow: ${template.fileName}`) + } + + vscode.window.showInformationMessage( + `Successfully installed ${templatesToInstall.length} GitHub Actions workflow(s)`, + ) + } + + public async setupBot(): Promise { + // Guide user through bot setup + const steps = [ + "1. Go to your repository settings on GitHub", + "2. Navigate to 'Secrets and variables' > 'Actions'", + "3. Add a new secret named 'ROO_CODE_API_KEY'", + "4. Generate an API key from Roo Code settings", + "5. Paste the API key as the secret value", + ] + + const message = `To complete GitHub Actions bot setup:\n\n${steps.join("\n")}` + + const result = await vscode.window.showInformationMessage( + message, + "Open GitHub Settings", + "Copy Instructions", + "Close", + ) + + if (result === "Open GitHub Settings") { + const repoUrl = await this.getRepositoryUrl() + if (repoUrl) { + vscode.env.openExternal(vscode.Uri.parse(`${repoUrl}/settings/secrets/actions`)) + } + } else if (result === "Copy Instructions") { + await vscode.env.clipboard.writeText(steps.join("\n")) + vscode.window.showInformationMessage("Setup instructions copied to clipboard") + } + } + + private async getRepositoryUrl(): Promise { + if (!this.workspaceRoot) { + return undefined + } + + try { + const gitConfigPath = path.join(this.workspaceRoot, ".git", "config") + const gitConfig = await fs.readFile(gitConfigPath, "utf8") + + // Extract remote origin URL + const match = gitConfig.match(/url = (.+)/) + if (match) { + let url = match[1] + // Convert SSH URL to HTTPS if needed + if (url.startsWith("git@github.com:")) { + url = url.replace("git@github.com:", "https://github.com/") + } + // Remove .git suffix if present + if (url.endsWith(".git")) { + url = url.slice(0, -4) + } + return url + } + } catch (error) { + this.outputChannel.appendLine(`Failed to get repository URL: ${error}`) + } + + return undefined + } + + public getAvailableTemplates(): WorkflowTemplate[] { + return this.getWorkflowTemplates() + } + + public async checkWorkflowsInstalled(): Promise { + if (!this.workspaceRoot) { + return false + } + + const workflowsDir = path.join(this.workspaceRoot, this.config.workflowsPath) + + try { + await fs.access(workflowsDir) + const files = await fs.readdir(workflowsDir) + return files.some((file) => file.startsWith("roo-code-")) + } catch { + return false + } + } + + 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.startsWith("roo-code-") && file.endsWith(".yml")) + } catch { + return [] + } + } + + public dispose(): void { + // Clean up resources if needed + GitHubActionsService.instance = undefined + } +} 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..7c328675a0 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -133,6 +133,7 @@ export interface ExtensionMessage { | "historyButtonClicked" | "promptsButtonClicked" | "marketplaceButtonClicked" + | "githubActionsButtonClicked" | "cloudButtonClicked" | "didBecomeVisible" | "focusInput" diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index fa38a566e7..15a6a490a0 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 @@ -62,6 +63,7 @@ const tabsByMessageAction: Partial { onDone={() => switchTab("chat")} /> )} + {tab === "githubActions" && switchTab("chat")} />} void +} + +interface WorkflowTemplate { + name: string + description: string + fileName: string + selected: boolean +} + +const GitHubActionsView: React.FC = ({ onDone }) => { + const [isEnabled, setIsEnabled] = useState(false) + const [workflowsInstalled, setWorkflowsInstalled] = useState(false) + const [templates, setTemplates] = useState([ + { + name: "Roo Code Issue Handler", + description: "Automatically handle GitHub issues with Roo Code", + fileName: "roo-code-issues.yml", + selected: true, + }, + { + name: "Roo Code PR Review", + description: "Automatically review pull requests with Roo Code", + fileName: "roo-code-pr-review.yml", + selected: true, + }, + { + name: "Roo Code Auto-Fix", + description: "Automatically fix code issues on push", + fileName: "roo-code-auto-fix.yml", + selected: false, + }, + ]) + const [installing, setInstalling] = useState(false) + const [setupComplete, setSetupComplete] = useState(false) + + useEffect(() => { + // Check if GitHub Actions is enabled + vscode.postMessage({ type: "githubActionsStatus" }) + }, []) + + const handleEnable = () => { + setIsEnabled(true) + vscode.postMessage({ type: "githubActionsEnable" }) + } + + const handleDisable = () => { + setIsEnabled(false) + vscode.postMessage({ type: "githubActionsDisable" }) + } + + const handleTemplateToggle = (index: number) => { + const newTemplates = [...templates] + newTemplates[index].selected = !newTemplates[index].selected + setTemplates(newTemplates) + } + + const handleInstallWorkflows = async () => { + setInstalling(true) + const selectedTemplates = templates.filter((t) => t.selected).map((t) => t.fileName) + vscode.postMessage({ + type: "githubActionsInstallWorkflows", + templates: selectedTemplates, + }) + + // Simulate installation delay + setTimeout(() => { + setInstalling(false) + setWorkflowsInstalled(true) + }, 2000) + } + + const handleSetupBot = () => { + vscode.postMessage({ type: "githubActionsSetupBot" }) + setSetupComplete(true) + } + + return ( +
+ {/* Header */} +
+
+ +

GitHub Actions Bot

+
+ +
+ + {/* Content */} +
+
+ {/* Enable/Disable Section */} +
+

Bot Status

+
+
+ {isEnabled ? ( + <> + + Enabled + + ) : ( + <> + + Disabled + + )} +
+ {isEnabled ? ( + + ) : ( + + )} +
+
+ + + + {/* Workflow Templates Section */} +
+

Workflow Templates

+

+ Select the GitHub Actions workflows you want to install: +

+ +
+ {templates.map((template, index) => ( +
+ handleTemplateToggle(index)} + /> +
+
{template.name}
+
+ {template.description} +
+
+ {template.fileName} +
+
+
+ ))} +
+ +
+ + {workflowsInstalled && ( +
+ + Workflows installed successfully! +
+ )} +
+
+ + + + {/* Setup Instructions Section */} +
+

Setup Instructions

+ +
+
+ +
+

To complete the setup:

+
    +
  1. Go to your repository settings on GitHub
  2. +
  3. Navigate to "Secrets and variables" → "Actions"
  4. +
  5. + Add a new secret named{" "} + + ROO_CODE_API_KEY + +
  6. +
  7. Generate an API key from Roo Code settings
  8. +
  9. Paste the API key as the secret value
  10. +
+
+
+
+ +
+ + {setupComplete && ( +
+ + Setup guide opened! +
+ )} +
+
+ + {/* Status Section */} + {isEnabled && workflowsInstalled && ( + <> + +
+

Bot Status

+
+
+ + GitHub Actions Bot is Active +
+

+ The bot will automatically process issues and pull requests based on your + installed workflows. +

+
+
+ + )} +
+
+ + {/* Footer */} +
+
+
+ GitHub Actions integration for automated issue and PR handling +
+ +
+
+
+ ) +} + +export default GitHubActionsView