169 lines
5.2 KiB
TypeScript
169 lines
5.2 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import path from 'node:path';
|
|
import * as fs from 'node:fs';
|
|
import { Writable } from 'node:stream';
|
|
import { ProxyAgent } from 'undici';
|
|
|
|
import { CommandContext } from '../../ui/commands/types.js';
|
|
import {
|
|
getGitRepoRoot,
|
|
getLatestGitHubRelease,
|
|
isGitHubRepository,
|
|
getGitHubRepoInfo,
|
|
} from '../../utils/gitUtils.js';
|
|
|
|
import {
|
|
CommandKind,
|
|
SlashCommand,
|
|
SlashCommandActionReturn,
|
|
} from './types.js';
|
|
import { getUrlOpenCommand } from '../../ui/utils/commandUtils.js';
|
|
|
|
// Generate OS-specific commands to open the GitHub pages needed for setup.
|
|
function getOpenUrlsCommands(readmeUrl: string): string[] {
|
|
// Determine the OS-specific command to open URLs, ex: 'open', 'xdg-open', etc
|
|
const openCmd = getUrlOpenCommand();
|
|
|
|
// Build a list of URLs to open
|
|
const urlsToOpen = [readmeUrl];
|
|
|
|
const repoInfo = getGitHubRepoInfo();
|
|
if (repoInfo) {
|
|
urlsToOpen.push(
|
|
`https://github.com/${repoInfo.owner}/${repoInfo.repo}/settings/secrets/actions`,
|
|
);
|
|
}
|
|
|
|
// Create and join the individual commands
|
|
const commands = urlsToOpen.map((url) => `${openCmd} "${url}"`);
|
|
return commands;
|
|
}
|
|
|
|
export const setupGithubCommand: SlashCommand = {
|
|
name: 'setup-github',
|
|
description: 'Set up GitHub Actions',
|
|
kind: CommandKind.BUILT_IN,
|
|
action: async (
|
|
context: CommandContext,
|
|
): Promise<SlashCommandActionReturn> => {
|
|
const abortController = new AbortController();
|
|
|
|
if (!isGitHubRepository()) {
|
|
throw new Error(
|
|
'Unable to determine the GitHub repository. /setup-github must be run from a git repository.',
|
|
);
|
|
}
|
|
|
|
// Find the root directory of the repo
|
|
let gitRepoRoot: string;
|
|
try {
|
|
gitRepoRoot = getGitRepoRoot();
|
|
} catch (_error) {
|
|
console.debug(`Failed to get git repo root:`, _error);
|
|
throw new Error(
|
|
'Unable to determine the GitHub repository. /setup-github must be run from a git repository.',
|
|
);
|
|
}
|
|
|
|
// Get the latest release tag from GitHub
|
|
const proxy = context?.services?.config?.getProxy();
|
|
const releaseTag = await getLatestGitHubRelease(proxy);
|
|
const readmeUrl = `https://github.com/google-github-actions/run-gemini-cli/blob/${releaseTag}/README.md#quick-start`;
|
|
|
|
// Create the .github/workflows directory to download the files into
|
|
const githubWorkflowsDir = path.join(gitRepoRoot, '.github', 'workflows');
|
|
try {
|
|
await fs.promises.mkdir(githubWorkflowsDir, { recursive: true });
|
|
} catch (_error) {
|
|
console.debug(
|
|
`Failed to create ${githubWorkflowsDir} directory:`,
|
|
_error,
|
|
);
|
|
throw new Error(
|
|
`Unable to create ${githubWorkflowsDir} directory. Do you have file permissions in the current directory?`,
|
|
);
|
|
}
|
|
|
|
// Download each workflow in parallel - there aren't enough files to warrant
|
|
// a full workerpool model here.
|
|
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 downloads = [];
|
|
for (const workflow of workflows) {
|
|
downloads.push(
|
|
(async () => {
|
|
const endpoint = `https://raw.githubusercontent.com/google-github-actions/run-gemini-cli/refs/tags/${releaseTag}/examples/workflows/${workflow}`;
|
|
const response = await fetch(endpoint, {
|
|
method: 'GET',
|
|
dispatcher: proxy ? new ProxyAgent(proxy) : undefined,
|
|
signal: AbortSignal.any([
|
|
AbortSignal.timeout(30_000),
|
|
abortController.signal,
|
|
]),
|
|
} as RequestInit);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(
|
|
`Invalid response code downloading ${endpoint}: ${response.status} - ${response.statusText}`,
|
|
);
|
|
}
|
|
const body = response.body;
|
|
if (!body) {
|
|
throw new Error(
|
|
`Empty body while downloading ${endpoint}: ${response.status} - ${response.statusText}`,
|
|
);
|
|
}
|
|
|
|
const destination = path.resolve(
|
|
githubWorkflowsDir,
|
|
path.basename(workflow),
|
|
);
|
|
|
|
const fileStream = fs.createWriteStream(destination, {
|
|
mode: 0o644, // -rw-r--r--, user(rw), group(r), other(r)
|
|
flags: 'w', // write and overwrite
|
|
flush: true,
|
|
});
|
|
|
|
await body.pipeTo(Writable.toWeb(fileStream));
|
|
})(),
|
|
);
|
|
}
|
|
|
|
// Wait for all downloads to complete
|
|
await Promise.all(downloads).finally(() => {
|
|
// Stop existing downloads
|
|
abortController.abort();
|
|
});
|
|
|
|
// Print out a message
|
|
const commands = [];
|
|
commands.push('set -eEuo pipefail');
|
|
commands.push(
|
|
`echo "Successfully downloaded ${workflows.length} workflows. Follow the steps in ${readmeUrl} (skipping the /setup-github step) to complete setup."`,
|
|
);
|
|
commands.push(...getOpenUrlsCommands(readmeUrl));
|
|
|
|
const command = `(${commands.join(' && ')})`;
|
|
return {
|
|
type: 'tool',
|
|
toolName: 'run_shell_command',
|
|
toolArgs: {
|
|
description:
|
|
'Setting up GitHub Actions to triage issues and review PRs with Gemini.',
|
|
command,
|
|
},
|
|
};
|
|
},
|
|
};
|