summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/cli/src/services/BuiltinCommandLoader.ts3
-rw-r--r--packages/cli/src/ui/commands/setupGithubCommand.test.ts66
-rw-r--r--packages/cli/src/ui/commands/setupGithubCommand.ts60
-rw-r--r--packages/cli/src/utils/gitUtils.ts26
4 files changed, 155 insertions, 0 deletions
diff --git a/packages/cli/src/services/BuiltinCommandLoader.ts b/packages/cli/src/services/BuiltinCommandLoader.ts
index 3b54047c..46ecb37c 100644
--- a/packages/cli/src/services/BuiltinCommandLoader.ts
+++ b/packages/cli/src/services/BuiltinCommandLoader.ts
@@ -31,6 +31,8 @@ import { statsCommand } from '../ui/commands/statsCommand.js';
import { themeCommand } from '../ui/commands/themeCommand.js';
import { toolsCommand } from '../ui/commands/toolsCommand.js';
import { vimCommand } from '../ui/commands/vimCommand.js';
+import { setupGithubCommand } from '../ui/commands/setupGithubCommand.js';
+import { isGitHubRepository } from '../utils/gitUtils.js';
/**
* Loads the core, hard-coded slash commands that are an integral part
@@ -72,6 +74,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
themeCommand,
toolsCommand,
vimCommand,
+ ...(isGitHubRepository() ? [setupGithubCommand] : []),
];
return allDefinitions.filter((cmd): cmd is SlashCommand => cmd !== null);
diff --git a/packages/cli/src/ui/commands/setupGithubCommand.test.ts b/packages/cli/src/ui/commands/setupGithubCommand.test.ts
new file mode 100644
index 00000000..fe68be0c
--- /dev/null
+++ b/packages/cli/src/ui/commands/setupGithubCommand.test.ts
@@ -0,0 +1,66 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { vi, describe, expect, it, afterEach, beforeEach } from 'vitest';
+import * as child_process from 'child_process';
+import { setupGithubCommand } from './setupGithubCommand.js';
+import { CommandContext, ToolActionReturn } from './types.js';
+
+vi.mock('child_process');
+
+describe('setupGithubCommand', () => {
+ beforeEach(() => {
+ vi.resetAllMocks();
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ it('returns a tool action to download github workflows and handles paths', () => {
+ const fakeRepoRoot = '/github.com/fake/repo/root';
+ vi.mocked(child_process.execSync).mockReturnValue(fakeRepoRoot);
+
+ const result = setupGithubCommand.action?.(
+ {} as CommandContext,
+ '',
+ ) as ToolActionReturn;
+
+ expect(result.type).toBe('tool');
+ expect(result.toolName).toBe('run_shell_command');
+ expect(child_process.execSync).toHaveBeenCalledWith(
+ 'git rev-parse --show-toplevel',
+ {
+ encoding: 'utf-8',
+ },
+ );
+ expect(child_process.execSync).toHaveBeenCalledWith('git remote -v', {
+ encoding: 'utf-8',
+ });
+
+ const { command } = result.toolArgs;
+
+ const expectedSubstrings = [
+ `mkdir -p "${fakeRepoRoot}/.github/workflows"`,
+ `curl -fsSL -o "${fakeRepoRoot}/.github/workflows/gemini-cli.yml"`,
+ `curl -fsSL -o "${fakeRepoRoot}/.github/workflows/gemini-issue-automated-triage.yml"`,
+ `curl -fsSL -o "${fakeRepoRoot}/.github/workflows/gemini-issue-scheduled-triage.yml"`,
+ `curl -fsSL -o "${fakeRepoRoot}/.github/workflows/gemini-pr-review.yml"`,
+ 'https://raw.githubusercontent.com/google-github-actions/run-gemini-cli/refs/heads/main/workflows/',
+ ];
+
+ for (const substring of expectedSubstrings) {
+ expect(command).toContain(substring);
+ }
+ });
+
+ it('throws an error if git root cannot be determined', () => {
+ vi.mocked(child_process.execSync).mockReturnValue('');
+ expect(() => {
+ setupGithubCommand.action?.({} as CommandContext, '');
+ }).toThrow('Unable to determine the Git root directory.');
+ });
+});
diff --git a/packages/cli/src/ui/commands/setupGithubCommand.ts b/packages/cli/src/ui/commands/setupGithubCommand.ts
new file mode 100644
index 00000000..14314423
--- /dev/null
+++ b/packages/cli/src/ui/commands/setupGithubCommand.ts
@@ -0,0 +1,60 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import path from 'path';
+import { execSync } from 'child_process';
+import { isGitHubRepository } from '../../utils/gitUtils.js';
+
+import {
+ CommandKind,
+ SlashCommand,
+ SlashCommandActionReturn,
+} from './types.js';
+
+export const setupGithubCommand: SlashCommand = {
+ name: 'setup-github',
+ description: 'Set up GitHub Actions',
+ kind: CommandKind.BUILT_IN,
+ action: (): SlashCommandActionReturn => {
+ const gitRootRepo = execSync('git rev-parse --show-toplevel', {
+ encoding: 'utf-8',
+ }).trim();
+
+ if (!isGitHubRepository()) {
+ throw new Error('Unable to determine the Git root directory.');
+ }
+
+ // TODO(#5198): pin workflow versions for release controls
+ const version = 'main';
+ const workflowBaseUrl = `https://raw.githubusercontent.com/google-github-actions/run-gemini-cli/refs/heads/${version}/workflows/`;
+
+ const workflows = [
+ 'gemini-cli/gemini-cli.yml',
+ 'issue-triage/gemini-issue-automated-triage.yml',
+ 'issue-triage/gemini-issue-scheduled-triage.yml',
+ 'pr-review/gemini-pr-review.yml',
+ ];
+
+ const command = [
+ 'set -e',
+ `mkdir -p "${gitRootRepo}/.github/workflows"`,
+ ...workflows.map((workflow) => {
+ const fileName = path.basename(workflow);
+ return `curl -fsSL -o "${gitRootRepo}/.github/workflows/${fileName}" "${workflowBaseUrl}/${workflow}"`;
+ }),
+ 'echo "Workflows downloaded successfully."',
+ ].join(' && ');
+ return {
+ type: 'tool',
+ toolName: 'run_shell_command',
+ toolArgs: {
+ description:
+ 'Setting up GitHub Actions to triage issues and review PRs with Gemini.',
+ command,
+ },
+ };
+ },
+};
diff --git a/packages/cli/src/utils/gitUtils.ts b/packages/cli/src/utils/gitUtils.ts
new file mode 100644
index 00000000..d510008c
--- /dev/null
+++ b/packages/cli/src/utils/gitUtils.ts
@@ -0,0 +1,26 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { execSync } from 'child_process';
+
+/**
+ * Checks if a directory is within a git repository hosted on GitHub.
+ * @returns true if the directory is in a git repository with a github.com remote, false otherwise
+ */
+export function isGitHubRepository(): boolean {
+ try {
+ const remotes = execSync('git remote -v', {
+ encoding: 'utf-8',
+ });
+
+ const pattern = /github\.com/;
+
+ return pattern.test(remotes);
+ } catch (_error) {
+ // If any filesystem error occurs, assume not a git repo
+ return false;
+ }
+}