feat: Implement /setup-github command (#5069)
This commit is contained in:
parent
f9a05401c1
commit
574015edd9
|
@ -31,6 +31,8 @@ import { statsCommand } from '../ui/commands/statsCommand.js';
|
||||||
import { themeCommand } from '../ui/commands/themeCommand.js';
|
import { themeCommand } from '../ui/commands/themeCommand.js';
|
||||||
import { toolsCommand } from '../ui/commands/toolsCommand.js';
|
import { toolsCommand } from '../ui/commands/toolsCommand.js';
|
||||||
import { vimCommand } from '../ui/commands/vimCommand.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
|
* Loads the core, hard-coded slash commands that are an integral part
|
||||||
|
@ -72,6 +74,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
|
||||||
themeCommand,
|
themeCommand,
|
||||||
toolsCommand,
|
toolsCommand,
|
||||||
vimCommand,
|
vimCommand,
|
||||||
|
...(isGitHubRepository() ? [setupGithubCommand] : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
return allDefinitions.filter((cmd): cmd is SlashCommand => cmd !== null);
|
return allDefinitions.filter((cmd): cmd is SlashCommand => cmd !== null);
|
||||||
|
|
|
@ -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.');
|
||||||
|
});
|
||||||
|
});
|
|
@ -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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue